diff --git a/src/authorization.rs b/src/authorization.rs new file mode 100644 index 0000000..3d178ad --- /dev/null +++ b/src/authorization.rs @@ -0,0 +1,500 @@ +use crate::core::CoreResponseType; +use crate::helpers::join_vec; +use crate::{ + AdditionalClaims, AuthDisplay, AuthPrompt, AuthenticationContextClass, CsrfToken, GenderClaim, + IdToken, JsonWebKeyType, JweContentEncryptionAlgorithm, JwsSigningAlgorithm, LanguageTag, + LoginHint, Nonce, PkceCodeChallenge, RedirectUrl, ResponseType, Scope, +}; + +use url::Url; + +use std::borrow::Cow; +use std::time::Duration; + +/// Authentication flow, which determines how the Authorization Server returns the OpenID Connect +/// ID token and OAuth2 access token to the Relying Party. +#[derive(Clone, Debug, PartialEq, Eq)] +#[non_exhaustive] +pub enum AuthenticationFlow { + /// Authorization Code Flow. + /// + /// The authorization server will return an OAuth2 authorization code. Clients must subsequently + /// call `Client::exchange_code()` with the authorization code in order to retrieve an + /// OpenID Connect ID token and OAuth2 access token. + AuthorizationCode, + /// Implicit Flow. + /// + /// Boolean value indicates whether an OAuth2 access token should also be returned. If `true`, + /// the Authorization Server will return both an OAuth2 access token and OpenID Connect ID + /// token. If `false`, it will return only an OpenID Connect ID token. + Implicit(bool), + /// Hybrid Flow. + /// + /// A hybrid flow according to [OAuth 2.0 Multiple Response Type Encoding Practices]( + /// https://openid.net/specs/oauth-v2-multiple-response-types-1_0.html). The enum value + /// contains the desired `response_type`s. See + /// [Section 3](https://openid.net/specs/openid-connect-core-1_0.html#Authentication) for + /// details. + Hybrid(Vec), +} + +/// A request to the authorization endpoint. +pub struct AuthorizationRequest<'a, AD, P, RT> +where + AD: AuthDisplay, + P: AuthPrompt, + RT: ResponseType, +{ + pub(crate) inner: oauth2::AuthorizationRequest<'a>, + pub(crate) acr_values: Vec, + pub(crate) authentication_flow: AuthenticationFlow, + pub(crate) claims_locales: Vec, + pub(crate) display: Option, + pub(crate) id_token_hint: Option, + pub(crate) login_hint: Option, + pub(crate) max_age: Option, + pub(crate) nonce: Nonce, + pub(crate) prompts: Vec

, + pub(crate) ui_locales: Vec, +} +impl<'a, AD, P, RT> AuthorizationRequest<'a, AD, P, RT> +where + AD: AuthDisplay, + P: AuthPrompt, + RT: ResponseType, +{ + /// Appends a new scope to the authorization URL. + pub fn add_scope(mut self, scope: Scope) -> Self { + self.inner = self.inner.add_scope(scope); + self + } + + /// Appends a collection of scopes to the authorization URL. + pub fn add_scopes(mut self, scopes: I) -> Self + where + I: IntoIterator, + { + self.inner = self.inner.add_scopes(scopes); + self + } + + /// Appends an extra param to the authorization URL. + /// + /// This method allows extensions to be used without direct support from + /// this crate. If `name` conflicts with a parameter managed by this crate, the + /// behavior is undefined. In particular, do not set parameters defined by + /// [RFC 6749](https://tools.ietf.org/html/rfc6749) or + /// [RFC 7636](https://tools.ietf.org/html/rfc7636). + /// + /// # Security Warning + /// + /// Callers should follow the security recommendations for any OAuth2 extensions used with + /// this function, which are beyond the scope of + /// [RFC 6749](https://tools.ietf.org/html/rfc6749). + pub fn add_extra_param(mut self, name: N, value: V) -> Self + where + N: Into>, + V: Into>, + { + self.inner = self.inner.add_extra_param(name, value); + self + } + + /// Enables the use of [Proof Key for Code Exchange](https://tools.ietf.org/html/rfc7636) + /// (PKCE). + /// + /// PKCE is *highly recommended* for all public clients (i.e., those for which there + /// is no client secret or for which the client secret is distributed with the client, + /// such as in a native, mobile app, or browser app). + pub fn set_pkce_challenge(mut self, pkce_code_challenge: PkceCodeChallenge) -> Self { + self.inner = self.inner.set_pkce_challenge(pkce_code_challenge); + self + } + + /// Requests Authentication Context Class Reference values. + /// + /// ACR values should be added in order of preference. The Authentication Context Class + /// satisfied by the authentication performed is accessible from the ID token via the + /// [`IdTokenClaims::auth_context_ref`] method. + pub fn add_auth_context_value(mut self, acr_value: AuthenticationContextClass) -> Self { + self.acr_values.push(acr_value); + self + } + + /// Requests the preferred languages for claims returned by the OpenID Connect Provider. + /// + /// Languages should be added in order of preference. + pub fn add_claims_locale(mut self, claims_locale: LanguageTag) -> Self { + self.claims_locales.push(claims_locale); + self + } + + // TODO: support 'claims' parameter + // https://openid.net/specs/openid-connect-core-1_0.html#ClaimsParameter + + /// Specifies how the OpenID Connect Provider displays the authentication and consent user + /// interfaces to the end user. + pub fn set_display(mut self, display: AD) -> Self { + self.display = Some(display); + self + } + + /// Provides an ID token previously issued by this OpenID Connect Provider as a hint about + /// the user's identity. + /// + /// This field should be set whenever [`core::CoreAuthPrompt::None`] is used (see + /// [`AuthorizationRequest::add_prompt`]), it but may be provided for any authorization + /// request. + pub fn set_id_token_hint( + mut self, + id_token_hint: &'a IdToken, + ) -> Self + where + AC: AdditionalClaims, + GC: GenderClaim, + JE: JweContentEncryptionAlgorithm, + JS: JwsSigningAlgorithm, + JT: JsonWebKeyType, + { + self.id_token_hint = Some(id_token_hint.to_string()); + self + } + + /// Provides the OpenID Connect Provider with a hint about the user's identity. + /// + /// The nature of this hint is specific to each provider. + pub fn set_login_hint(mut self, login_hint: LoginHint) -> Self { + self.login_hint = Some(login_hint); + self + } + + /// Sets a maximum amount of time since the user has last authenticated with the OpenID + /// Connect Provider. + /// + /// If more time has elapsed, the provider forces the user to re-authenticate. + pub fn set_max_age(mut self, max_age: Duration) -> Self { + self.max_age = Some(max_age); + self + } + + /// Specifies what level of authentication and consent prompts the OpenID Connect Provider + /// should present to the user. + pub fn add_prompt(mut self, prompt: P) -> Self { + self.prompts.push(prompt); + self + } + + /// Requests the preferred languages for the user interface presented by the OpenID Connect + /// Provider. + /// + /// Languages should be added in order of preference. + pub fn add_ui_locale(mut self, ui_locale: LanguageTag) -> Self { + self.ui_locales.push(ui_locale); + self + } + + /// Overrides the `redirect_url` to the one specified. + pub fn set_redirect_uri(mut self, redirect_url: Cow<'a, RedirectUrl>) -> Self { + self.inner = self.inner.set_redirect_uri(redirect_url); + self + } + + /// Returns the full authorization URL and CSRF state for this authorization + /// request. + pub fn url(self) -> (Url, CsrfToken, Nonce) { + let response_type = match self.authentication_flow { + AuthenticationFlow::AuthorizationCode => CoreResponseType::Code.to_oauth2(), + AuthenticationFlow::Implicit(include_token) => { + if include_token { + oauth2::ResponseType::new( + [CoreResponseType::IdToken, CoreResponseType::Token] + .iter() + .map(|response_type| response_type.as_ref()) + .collect::>() + .join(" "), + ) + } else { + CoreResponseType::IdToken.to_oauth2() + } + } + AuthenticationFlow::Hybrid(ref response_types) => oauth2::ResponseType::new( + response_types + .iter() + .map(|response_type| response_type.as_ref()) + .collect::>() + .join(" "), + ), + }; + let (mut inner, nonce) = ( + self.inner + .set_response_type(&response_type) + .add_extra_param("nonce", self.nonce.secret().clone()), + self.nonce, + ); + if !self.acr_values.is_empty() { + inner = inner.add_extra_param("acr_values", join_vec(&self.acr_values)); + } + if !self.claims_locales.is_empty() { + inner = inner.add_extra_param("claims_locales", join_vec(&self.claims_locales)); + } + if let Some(ref display) = self.display { + inner = inner.add_extra_param("display", display.as_ref()); + } + if let Some(ref id_token_hint) = self.id_token_hint { + inner = inner.add_extra_param("id_token_hint", id_token_hint); + } + if let Some(ref login_hint) = self.login_hint { + inner = inner.add_extra_param("login_hint", login_hint.secret()); + } + if let Some(max_age) = self.max_age { + inner = inner.add_extra_param("max_age", max_age.as_secs().to_string()); + } + if !self.prompts.is_empty() { + inner = inner.add_extra_param("prompt", join_vec(&self.prompts)); + } + if !self.ui_locales.is_empty() { + inner = inner.add_extra_param("ui_locales", join_vec(&self.ui_locales)); + } + + let (url, state) = inner.url(); + (url, state, nonce) + } +} + +#[cfg(test)] +mod tests { + use crate::core::CoreAuthenticationFlow; + use crate::core::{CoreAuthDisplay, CoreAuthPrompt, CoreClient, CoreIdToken, CoreResponseType}; + use crate::IssuerUrl; + use crate::{ + AuthUrl, AuthenticationContextClass, AuthenticationFlow, ClientId, ClientSecret, CsrfToken, + JsonWebKeySet, LanguageTag, LoginHint, Nonce, RedirectUrl, Scope, TokenUrl, + }; + + use std::borrow::Cow; + use std::time::Duration; + + fn new_client() -> CoreClient { + color_backtrace::install(); + CoreClient::new( + ClientId::new("aaa".to_string()), + Some(ClientSecret::new("bbb".to_string())), + IssuerUrl::new("https://example".to_string()).unwrap(), + AuthUrl::new("https://example/authorize".to_string()).unwrap(), + Some(TokenUrl::new("https://example/token".to_string()).unwrap()), + None, + JsonWebKeySet::default(), + ) + } + + #[test] + fn test_authorize_url_minimal() { + let client = new_client(); + + let (authorize_url, _, _) = client + .authorize_url( + AuthenticationFlow::AuthorizationCode::, + || CsrfToken::new("CSRF123".to_string()), + || Nonce::new("NONCE456".to_string()), + ) + .url(); + + assert_eq!( + "https://example/authorize?response_type=code&client_id=aaa&\ + state=CSRF123&scope=openid&nonce=NONCE456", + authorize_url.to_string() + ); + } + + #[test] + fn test_authorize_url_implicit_with_access_token() { + let client = new_client(); + + let (authorize_url, _, _) = client + .authorize_url( + AuthenticationFlow::::Implicit(true), + || CsrfToken::new("CSRF123".to_string()), + || Nonce::new("NONCE456".to_string()), + ) + .url(); + + assert_eq!( + "https://example/authorize?response_type=id_token+token&client_id=aaa&\ + state=CSRF123&scope=openid&nonce=NONCE456", + authorize_url.to_string() + ); + } + + #[test] + fn test_authorize_url_hybrid() { + let client = new_client(); + + let (authorize_url, _, _) = client + .authorize_url( + AuthenticationFlow::Hybrid(vec![ + CoreResponseType::Code, + CoreResponseType::Extension("other".to_string()), + ]), + || CsrfToken::new("CSRF123".to_string()), + || Nonce::new("NONCE456".to_string()), + ) + .url(); + + assert_eq!( + "https://example/authorize?response_type=code+other&client_id=aaa&\ + state=CSRF123&scope=openid&nonce=NONCE456", + authorize_url.to_string() + ); + } + + #[test] + fn test_authorize_url_full() { + let client = new_client() + .set_redirect_uri(RedirectUrl::new("http://localhost:8888/".to_string()).unwrap()); + + let flow = CoreAuthenticationFlow::AuthorizationCode; + + fn new_csrf() -> CsrfToken { + CsrfToken::new("CSRF123".to_string()) + } + fn new_nonce() -> Nonce { + Nonce::new("NONCE456".to_string()) + } + + let (authorize_url, _, _) = client + .authorize_url(flow.clone(), new_csrf, new_nonce) + .add_scope(Scope::new("email".to_string())) + .set_display(CoreAuthDisplay::Touch) + .add_prompt(CoreAuthPrompt::Login) + .add_prompt(CoreAuthPrompt::Consent) + .set_max_age(Duration::from_secs(1800)) + .add_ui_locale(LanguageTag::new("fr-CA".to_string())) + .add_ui_locale(LanguageTag::new("fr".to_string())) + .add_ui_locale(LanguageTag::new("en".to_string())) + .add_auth_context_value(AuthenticationContextClass::new( + "urn:mace:incommon:iap:silver".to_string(), + )) + .url(); + assert_eq!( + "https://example/authorize?response_type=code&client_id=aaa&\ + state=CSRF123&redirect_uri=http%3A%2F%2Flocalhost%3A8888%2F&scope=openid+email&\ + nonce=NONCE456&acr_values=urn%3Amace%3Aincommon%3Aiap%3Asilver&display=touch&\ + max_age=1800&prompt=login+consent&ui_locales=fr-CA+fr+en", + authorize_url.to_string() + ); + + let serialized_jwt = + "eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJodHRwczovL2V4YW1wbGUuY29tIiwiYXVkIjpbIm15X2NsaWVudCJdL\ + CJleHAiOjE1NDQ5MzIxNDksImlhdCI6MTU0NDkyODU0OSwiYXV0aF90aW1lIjoxNTQ0OTI4NTQ4LCJub25jZSI\ + 6InRoZV9ub25jZSIsImFjciI6InRoZV9hY3IiLCJzdWIiOiJzdWJqZWN0In0.gb5HuuyDMu-LvYvG-jJNIJPEZ\ + 823qNwvgNjdAtW0HJpgwJWhJq0hOHUuZz6lvf8ud5xbg5GOo0Q37v3Ke08TvGu6E1USWjecZzp1aYVm9BiMvw5\ + EBRUrwAaOCG2XFjuOKUVfglSMJnRnoNqVVIWpCAr1ETjZzRIbkU3n5GQRguC5CwN5n45I3dtjoKuNGc2Ni-IMl\ + J2nRiCJOl2FtStdgs-doc-A9DHtO01x-5HCwytXvcE28Snur1JnqpUgmWrQ8gZMGuijKirgNnze2Dd5BsZRHZ2\ + CLGIwBsCnauBrJy_NNlQg4hUcSlGsuTa0dmZY7mCf4BN2WCpyOh0wgtkAgQ"; + let id_token = serde_json::from_value::(serde_json::Value::String( + serialized_jwt.to_string(), + )) + .unwrap(); + + let (authorize_url, _, _) = client + .authorize_url(flow.clone(), new_csrf, new_nonce) + .add_scope(Scope::new("email".to_string())) + .set_display(CoreAuthDisplay::Touch) + .set_id_token_hint(&id_token) + .set_login_hint(LoginHint::new("foo@bar.com".to_string())) + .add_prompt(CoreAuthPrompt::Login) + .add_prompt(CoreAuthPrompt::Consent) + .set_max_age(Duration::from_secs(1800)) + .add_ui_locale(LanguageTag::new("fr-CA".to_string())) + .add_ui_locale(LanguageTag::new("fr".to_string())) + .add_ui_locale(LanguageTag::new("en".to_string())) + .add_auth_context_value(AuthenticationContextClass::new( + "urn:mace:incommon:iap:silver".to_string(), + )) + .add_extra_param("foo", "bar") + .url(); + assert_eq!( + format!( + "https://example/authorize?response_type=code&client_id=aaa&state=CSRF123&\ + redirect_uri=http%3A%2F%2Flocalhost%3A8888%2F&scope=openid+email&foo=bar&\ + nonce=NONCE456&acr_values=urn%3Amace%3Aincommon%3Aiap%3Asilver&display=touch&\ + id_token_hint={}&login_hint=foo%40bar.com&\ + max_age=1800&prompt=login+consent&ui_locales=fr-CA+fr+en", + serialized_jwt + ), + authorize_url.to_string() + ); + + let (authorize_url, _, _) = client + .authorize_url(flow, new_csrf, new_nonce) + .add_scopes(vec![ + Scope::new("email".to_string()), + Scope::new("profile".to_string()), + ]) + .set_display(CoreAuthDisplay::Touch) + .set_id_token_hint(&id_token) + .set_login_hint(LoginHint::new("foo@bar.com".to_string())) + .add_prompt(CoreAuthPrompt::Login) + .add_prompt(CoreAuthPrompt::Consent) + .set_max_age(Duration::from_secs(1800)) + .add_ui_locale(LanguageTag::new("fr-CA".to_string())) + .add_ui_locale(LanguageTag::new("fr".to_string())) + .add_ui_locale(LanguageTag::new("en".to_string())) + .add_auth_context_value(AuthenticationContextClass::new( + "urn:mace:incommon:iap:silver".to_string(), + )) + .add_extra_param("foo", "bar") + .url(); + assert_eq!( + format!( + "https://example/authorize?response_type=code&client_id=aaa&state=CSRF123&\ + redirect_uri=http%3A%2F%2Flocalhost%3A8888%2F&scope=openid+email+profile&foo=bar&\ + nonce=NONCE456&acr_values=urn%3Amace%3Aincommon%3Aiap%3Asilver&display=touch&\ + id_token_hint={}&login_hint=foo%40bar.com&\ + max_age=1800&prompt=login+consent&ui_locales=fr-CA+fr+en", + serialized_jwt + ), + authorize_url.to_string() + ); + } + + #[test] + fn test_authorize_url_redirect_url_override() { + let client = new_client() + .set_redirect_uri(RedirectUrl::new("http://localhost:8888/".to_string()).unwrap()); + + let flow = CoreAuthenticationFlow::AuthorizationCode; + + fn new_csrf() -> CsrfToken { + CsrfToken::new("CSRF123".to_string()) + } + fn new_nonce() -> Nonce { + Nonce::new("NONCE456".to_string()) + } + + let (authorize_url, _, _) = client + .authorize_url(flow, new_csrf, new_nonce) + .add_scope(Scope::new("email".to_string())) + .set_display(CoreAuthDisplay::Touch) + .add_prompt(CoreAuthPrompt::Login) + .add_prompt(CoreAuthPrompt::Consent) + .set_max_age(Duration::from_secs(1800)) + .add_ui_locale(LanguageTag::new("fr-CA".to_string())) + .add_ui_locale(LanguageTag::new("fr".to_string())) + .add_ui_locale(LanguageTag::new("en".to_string())) + .add_auth_context_value(AuthenticationContextClass::new( + "urn:mace:incommon:iap:silver".to_string(), + )) + .set_redirect_uri(Cow::Owned( + RedirectUrl::new("http://localhost:8888/alternative".to_string()).unwrap(), + )) + .url(); + assert_eq!( + "https://example/authorize?response_type=code&client_id=aaa&\ + state=CSRF123&redirect_uri=http%3A%2F%2Flocalhost%3A8888%2Falternative&scope=openid+email&\ + nonce=NONCE456&acr_values=urn%3Amace%3Aincommon%3Aiap%3Asilver&display=touch&\ + max_age=1800&prompt=login+consent&ui_locales=fr-CA+fr+en", + authorize_url.to_string() + ); + } +} diff --git a/src/claims.rs b/src/claims.rs index fe3aaf3..ecb854c 100644 --- a/src/claims.rs +++ b/src/claims.rs @@ -1,12 +1,11 @@ -use crate::helpers::FlattenFilter; -use crate::types::helpers::{split_language_tag_key, timestamp_to_utc, utc_to_seconds}; -use crate::types::{Boolean, LocalizedClaim, Timestamp}; +use crate::helpers::{timestamp_to_utc, utc_to_seconds, Boolean, FlattenFilter, Timestamp}; +use crate::types::localized::split_language_tag_key; use crate::{ AddressCountry, AddressLocality, AddressPostalCode, AddressRegion, EndUserBirthday, EndUserEmail, EndUserFamilyName, EndUserGivenName, EndUserMiddleName, EndUserName, EndUserNickname, EndUserPhoneNumber, EndUserPictureUrl, EndUserProfileUrl, EndUserTimezone, - EndUserUsername, EndUserWebsiteUrl, FormattedAddress, LanguageTag, StreetAddress, - SubjectIdentifier, + EndUserUsername, EndUserWebsiteUrl, FormattedAddress, LanguageTag, LocalizedClaim, + StreetAddress, SubjectIdentifier, }; use chrono::{DateTime, Utc}; diff --git a/src/client.rs b/src/client.rs new file mode 100644 index 0000000..011375b --- /dev/null +++ b/src/client.rs @@ -0,0 +1,493 @@ +use crate::{ + AccessToken, AdditionalClaims, AdditionalProviderMetadata, AuthDisplay, AuthPrompt, AuthType, + AuthUrl, AuthenticationFlow, AuthorizationCode, AuthorizationRequest, ClaimName, ClaimType, + ClientAuthMethod, ClientCredentialsTokenRequest, ClientId, ClientSecret, CodeTokenRequest, + ConfigurationError, CsrfToken, DeviceAccessTokenRequest, DeviceAuthorizationRequest, + DeviceAuthorizationResponse, DeviceAuthorizationUrl, ErrorResponse, + ExtraDeviceAuthorizationFields, GenderClaim, GrantType, IdTokenVerifier, IntrospectionRequest, + IntrospectionUrl, IssuerUrl, JsonWebKey, JsonWebKeySet, JsonWebKeyType, JsonWebKeyUse, + JweContentEncryptionAlgorithm, JweKeyManagementAlgorithm, JwsSigningAlgorithm, Nonce, + PasswordTokenRequest, ProviderMetadata, RedirectUrl, RefreshToken, RefreshTokenRequest, + ResourceOwnerPassword, ResourceOwnerUsername, ResponseMode, ResponseType, RevocableToken, + RevocationRequest, RevocationUrl, Scope, SubjectIdentifier, SubjectIdentifierType, + TokenIntrospectionResponse, TokenResponse, TokenType, TokenUrl, UserInfoRequest, + UserInfoResponseType, UserInfoUrl, UserInfoVerifier, +}; + +use std::marker::PhantomData; + +const OPENID_SCOPE: &str = "openid"; + +/// OpenID Connect client. +/// +/// # Error Types +/// +/// To enable compile time verification that only the correct and complete set of errors for the `Client` function being +/// invoked are exposed to the caller, the `Client` type is specialized on multiple implementations of the +/// [`ErrorResponse`] trait. The exact [`ErrorResponse`] implementation returned varies by the RFC that the invoked +/// `Client` function implements: +/// +/// - Generic type `TE` (aka Token Error) for errors defined by [RFC 6749 OAuth 2.0 Authorization Framework](https://tools.ietf.org/html/rfc6749). +/// - Generic type `TRE` (aka Token Revocation Error) for errors defined by [RFC 7009 OAuth 2.0 Token Revocation](https://tools.ietf.org/html/rfc7009). +/// +/// For example when revoking a token, error code `unsupported_token_type` (from RFC 7009) may be returned: +/// ```rust +/// # use http::status::StatusCode; +/// # use http::header::{HeaderValue, CONTENT_TYPE}; +/// # use openidconnect::core::CoreClient; +/// # use openidconnect::{ +/// # AccessToken, +/// # AuthUrl, +/// # ClientId, +/// # ClientSecret, +/// # HttpResponse, +/// # IssuerUrl, +/// # JsonWebKeySet, +/// # RequestTokenError, +/// # RevocationErrorResponseType, +/// # RevocationUrl, +/// # TokenUrl, +/// # }; +/// # use thiserror::Error; +/// # +/// # let client = CoreClient::new( +/// # ClientId::new("aaa".to_string()), +/// # Some(ClientSecret::new("bbb".to_string())), +/// # IssuerUrl::new("https://example".to_string()).unwrap(), +/// # AuthUrl::new("https://example/authorize".to_string()).unwrap(), +/// # Some(TokenUrl::new("https://example/token".to_string()).unwrap()), +/// # None, +/// # JsonWebKeySet::default(), +/// # ) +/// # .set_revocation_uri(RevocationUrl::new("https://revocation/url".to_string()).unwrap()); +/// # +/// # #[derive(Debug, Error)] +/// # enum FakeError { +/// # #[error("error")] +/// # Err, +/// # } +/// # +/// # let http_client = |_| -> Result { +/// # Ok(HttpResponse { +/// # status_code: StatusCode::BAD_REQUEST, +/// # headers: vec![( +/// # CONTENT_TYPE, +/// # HeaderValue::from_str("application/json").unwrap(), +/// # )] +/// # .into_iter() +/// # .collect(), +/// # body: "{\"error\": \"unsupported_token_type\", \"error_description\": \"stuff happened\", \ +/// # \"error_uri\": \"https://errors\"}" +/// # .to_string() +/// # .into_bytes(), +/// # }) +/// # }; +/// # +/// let res = client +/// .revoke_token(AccessToken::new("some token".to_string()).into()) +/// .unwrap() +/// .request(http_client); +/// +/// assert!(matches!(res, Err( +/// RequestTokenError::ServerResponse(err)) if matches!(err.error(), +/// RevocationErrorResponseType::UnsupportedTokenType))); +/// ``` +#[derive(Clone, Debug)] +pub struct Client +where + AC: AdditionalClaims, + AD: AuthDisplay, + GC: GenderClaim, + JE: JweContentEncryptionAlgorithm, + JS: JwsSigningAlgorithm, + JT: JsonWebKeyType, + JU: JsonWebKeyUse, + K: JsonWebKey, + P: AuthPrompt, + TE: ErrorResponse, + TR: TokenResponse, + TT: TokenType + 'static, + TIR: TokenIntrospectionResponse, + RT: RevocableToken, + TRE: ErrorResponse, +{ + oauth2_client: oauth2::Client, + client_id: ClientId, + client_secret: Option, + issuer: IssuerUrl, + userinfo_endpoint: Option, + jwks: JsonWebKeySet, + id_token_signing_algs: Option>, + use_openid_scope: bool, + _phantom: PhantomData<(AC, AD, GC, JE, P)>, +} +impl + Client +where + 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, +{ + /// Initializes an OpenID Connect client. + pub fn new( + client_id: ClientId, + client_secret: Option, + issuer: IssuerUrl, + auth_url: AuthUrl, + token_url: Option, + userinfo_endpoint: Option, + jwks: JsonWebKeySet, + ) -> Self { + Client { + oauth2_client: oauth2::Client::new( + client_id.clone(), + client_secret.clone(), + auth_url, + token_url, + ), + client_id, + client_secret, + issuer, + userinfo_endpoint, + jwks, + id_token_signing_algs: None, + use_openid_scope: true, + _phantom: PhantomData, + } + } + + /// Initializes an OpenID Connect client from OpenID Connect Discovery provider metadata. + /// + /// Use [`ProviderMetadata::discover`] or + /// [`ProviderMetadata::discover_async`] to fetch the provider metadata. + pub fn from_provider_metadata( + provider_metadata: ProviderMetadata, + client_id: ClientId, + client_secret: Option, + ) -> Self + where + A: AdditionalProviderMetadata, + CA: ClientAuthMethod, + CN: ClaimName, + CT: ClaimType, + G: GrantType, + JK: JweKeyManagementAlgorithm, + RM: ResponseMode, + RS: ResponseType, + S: SubjectIdentifierType, + { + Client { + oauth2_client: oauth2::Client::new( + client_id.clone(), + client_secret.clone(), + provider_metadata.authorization_endpoint().clone(), + provider_metadata.token_endpoint().cloned(), + ), + client_id, + client_secret, + issuer: provider_metadata.issuer().clone(), + userinfo_endpoint: provider_metadata.userinfo_endpoint().cloned(), + jwks: provider_metadata.jwks().to_owned(), + id_token_signing_algs: Some( + provider_metadata + .id_token_signing_alg_values_supported() + .to_owned(), + ), + use_openid_scope: true, + _phantom: PhantomData, + } + } + + /// Configures the type of client authentication used for communicating with the authorization + /// server. + /// + /// The default is to use HTTP Basic authentication, as recommended in + /// [Section 2.3.1 of RFC 6749](https://tools.ietf.org/html/rfc6749#section-2.3.1). Note that + /// if a client secret is omitted (i.e., `client_secret` is set to `None` when calling + /// [`Client::new`]), [`AuthType::RequestBody`] is used regardless of the `auth_type` passed to + /// this function. + pub fn set_auth_type(mut self, auth_type: AuthType) -> Self { + self.oauth2_client = self.oauth2_client.set_auth_type(auth_type); + self + } + + /// Sets the redirect URL used by the authorization endpoint. + pub fn set_redirect_uri(mut self, redirect_url: RedirectUrl) -> Self { + self.oauth2_client = self.oauth2_client.set_redirect_uri(redirect_url); + self + } + + /// Sets the introspection URL for contacting the ([RFC 7662](https://tools.ietf.org/html/rfc7662)) + /// introspection endpoint. + pub fn set_introspection_uri(mut self, introspection_url: IntrospectionUrl) -> Self { + self.oauth2_client = self.oauth2_client.set_introspection_uri(introspection_url); + self + } + + /// Sets the revocation URL for contacting the revocation endpoint ([RFC 7009](https://tools.ietf.org/html/rfc7009)). + /// + /// See: [`revoke_token()`](Self::revoke_token()) + pub fn set_revocation_uri(mut self, revocation_url: RevocationUrl) -> Self { + self.oauth2_client = self.oauth2_client.set_revocation_uri(revocation_url); + self + } + + /// Sets the device authorization URL for contacting the device authorization endpoint ([RFC 8628](https://tools.ietf.org/html/rfc8628)). + pub fn set_device_authorization_uri( + mut self, + device_authorization_url: DeviceAuthorizationUrl, + ) -> Self { + self.oauth2_client = self + .oauth2_client + .set_device_authorization_url(device_authorization_url); + self + } + + /// Enables the `openid` scope to be requested automatically. + /// + /// This scope is requested by default, so this function is only useful after previous calls to + /// [`disable_openid_scope`][Client::disable_openid_scope]. + pub fn enable_openid_scope(mut self) -> Self { + self.use_openid_scope = true; + self + } + + /// Disables the `openid` scope from being requested automatically. + pub fn disable_openid_scope(mut self) -> Self { + self.use_openid_scope = false; + self + } + + /// Returns an ID token verifier for use with the [`IdToken::claims`] method. + pub fn id_token_verifier(&self) -> IdTokenVerifier { + let verifier = if let Some(ref client_secret) = self.client_secret { + IdTokenVerifier::new_confidential_client( + self.client_id.clone(), + client_secret.clone(), + self.issuer.clone(), + self.jwks.clone(), + ) + } else { + IdTokenVerifier::new_public_client( + self.client_id.clone(), + self.issuer.clone(), + self.jwks.clone(), + ) + }; + + if let Some(id_token_signing_algs) = self.id_token_signing_algs.clone() { + verifier.set_allowed_algs(id_token_signing_algs) + } else { + verifier + } + } + + /// Generates an authorization URL for a new authorization request. + /// + /// NOTE: [Passing authorization request parameters as a JSON Web Token + /// ](https://openid.net/specs/openid-connect-core-1_0.html#JWTRequests) + /// instead of URL query parameters is not currently supported. The + /// [`claims` parameter](https://openid.net/specs/openid-connect-core-1_0.html#ClaimsParameter) + /// is also not directly supported, although the [`AuthorizationRequest::add_extra_param`] + /// method can be used to add custom parameters, including `claims`. + /// + /// # Arguments + /// + /// * `authentication_flow` - The authentication flow to use (code, implicit, or hybrid). + /// * `state_fn` - A function that returns an opaque value used by the client to maintain state + /// between the request and callback. The authorization server includes this value when + /// redirecting the user-agent back to the client. + /// * `nonce_fn` - Similar to `state_fn`, but used to generate an opaque nonce to be used + /// when verifying the ID token returned by the OpenID Connect Provider. + /// + /// # Security Warning + /// + /// Callers should use a fresh, unpredictable `state` for each authorization request and verify + /// that this value matches the `state` parameter passed by the authorization server to the + /// redirect URI. Doing so mitigates + /// [Cross-Site Request Forgery](https://tools.ietf.org/html/rfc6749#section-10.12) + /// attacks. + /// + /// Similarly, callers should use a fresh, unpredictable `nonce` to help protect against ID + /// token reuse and forgery. + pub fn authorize_url( + &self, + authentication_flow: AuthenticationFlow, + state_fn: SF, + nonce_fn: NF, + ) -> AuthorizationRequest + where + NF: FnOnce() -> Nonce + 'static, + RS: ResponseType, + SF: FnOnce() -> CsrfToken + 'static, + { + let request = AuthorizationRequest { + inner: self.oauth2_client.authorize_url(state_fn), + acr_values: Vec::new(), + authentication_flow, + claims_locales: Vec::new(), + display: None, + id_token_hint: None, + login_hint: None, + max_age: None, + nonce: nonce_fn(), + prompts: Vec::new(), + ui_locales: Vec::new(), + }; + if self.use_openid_scope { + request.add_scope(Scope::new(OPENID_SCOPE.to_string())) + } else { + request + } + } + + /// Creates a request builder for exchanging an authorization code for an access token. + /// + /// Acquires ownership of the `code` because authorization codes may only be used once to + /// retrieve an access token from the authorization server. + /// + /// See + pub fn exchange_code(&self, code: AuthorizationCode) -> CodeTokenRequest { + self.oauth2_client.exchange_code(code) + } + + /// Creates a request builder for device authorization. + /// + /// See + pub fn exchange_device_code( + &self, + ) -> Result, ConfigurationError> { + let request = self.oauth2_client.exchange_device_code(); + if self.use_openid_scope { + Ok(request?.add_scope(Scope::new(OPENID_SCOPE.to_string()))) + } else { + request + } + } + + /// Creates a request builder for exchanging a device code for an access token. + /// + /// See + pub fn exchange_device_access_token<'a, 'b, 'c, EF>( + &'a self, + auth_response: &'b DeviceAuthorizationResponse, + ) -> DeviceAccessTokenRequest<'b, 'c, TR, TT, EF> + where + 'a: 'b, + EF: ExtraDeviceAuthorizationFields, + { + self.oauth2_client + .exchange_device_access_token(auth_response) + } + + /// Creates a request builder for exchanging a refresh token for an access token. + /// + /// See + pub fn exchange_refresh_token<'a, 'b>( + &'a self, + refresh_token: &'b RefreshToken, + ) -> RefreshTokenRequest<'b, TE, TR, TT> + where + 'a: 'b, + { + self.oauth2_client.exchange_refresh_token(refresh_token) + } + + /// Creates a request builder for exchanging credentials for an access token. + /// + /// See + pub fn exchange_password<'a, 'b>( + &'a self, + username: &'b ResourceOwnerUsername, + password: &'b ResourceOwnerPassword, + ) -> PasswordTokenRequest<'b, TE, TR, TT> + where + 'a: 'b, + { + self.oauth2_client.exchange_password(username, password) + } + + /// Creates a request builder for exchanging client credentials for an access token. + /// + /// See + pub fn exchange_client_credentials<'a, 'b>( + &'a self, + ) -> ClientCredentialsTokenRequest<'b, TE, TR, TT> + where + 'a: 'b, + { + self.oauth2_client.exchange_client_credentials() + } + + /// Creates a request builder for info about the user associated with the given access token. + /// + /// This function requires that this [`Client`] be configured with a user info endpoint, + /// which is an optional feature for OpenID Connect Providers to implement. If this `Client` + /// does not know the provider's user info endpoint, it returns the [`ConfigurationError`] + /// error. + /// + /// To help protect against token substitution attacks, this function optionally allows clients + /// to provide the subject identifier whose user info they expect to receive. If provided and + /// the subject returned by the OpenID Connect Provider does not match, the + /// [`UserInfoRequest::request`] or [`UserInfoRequest::request_async`] functions will return + /// [`UserInfoError::ClaimsVerification`]. If set to `None`, any subject is accepted. + pub fn user_info( + &self, + access_token: AccessToken, + expected_subject: Option, + ) -> Result, ConfigurationError> { + Ok(UserInfoRequest { + url: self + .userinfo_endpoint + .as_ref() + .ok_or(ConfigurationError::MissingUrl("userinfo"))?, + access_token, + require_signed_response: false, + response_type: UserInfoResponseType::Json, + signed_response_verifier: UserInfoVerifier::new( + self.client_id.clone(), + self.issuer.clone(), + self.jwks.clone(), + expected_subject, + ), + }) + } + + /// Creates a request builder for obtaining metadata about a previously received token. + /// + /// See + pub fn introspect<'a>( + &'a self, + token: &'a AccessToken, + ) -> Result, ConfigurationError> { + self.oauth2_client.introspect(token) + } + + /// Creates a request builder for revoking a previously received token. + /// + /// Requires that [`set_revocation_uri()`](Self::set_revocation_uri()) have already been called to set the + /// revocation endpoint URL. + /// + /// Attempting to submit the generated request without calling [`set_revocation_uri()`](Self::set_revocation_uri()) + /// first will result in an error. + /// + /// See + pub fn revoke_token( + &self, + token: RT, + ) -> Result, ConfigurationError> { + self.oauth2_client.revoke_token(token) + } +} diff --git a/src/core/crypto.rs b/src/core/crypto.rs index e6b147a..c744371 100644 --- a/src/core/crypto.rs +++ b/src/core/crypto.rs @@ -1,6 +1,6 @@ use crate::core::jwk::CoreJsonCurveType; use crate::core::{CoreJsonWebKey, CoreJsonWebKeyType}; -use crate::types::Base64UrlEncodedBytes; +use crate::helpers::Base64UrlEncodedBytes; use crate::{JsonWebKey, SignatureVerificationError}; use std::ops::Deref; diff --git a/src/core/jwk.rs b/src/core/jwk.rs deleted file mode 100644 index 6550fee..0000000 --- a/src/core/jwk.rs +++ /dev/null @@ -1,1860 +0,0 @@ -use crate::core::{crypto, CoreJwsSigningAlgorithm}; -use crate::types::helpers::deserialize_option_or_none; -use crate::types::{check_key_compatibility, Base64UrlEncodedBytes, JsonCurveType}; -#[cfg(feature = "jwk-alg")] -use crate::{ - core::CoreJweContentEncryptionAlgorithm, jwt::JsonWebTokenAlgorithm, types::JsonWebKeyAlgorithm, -}; -use crate::{ - JsonWebKey, JsonWebKeyId, JsonWebKeyType, JsonWebKeyUse, PrivateSigningKey, - SignatureVerificationError, SigningError, -}; - -use ed25519_dalek::pkcs8::DecodePrivateKey; -use ed25519_dalek::Signer; -use rsa::pkcs1::DecodeRsaPrivateKey; -use serde::{Deserialize, Serialize}; -use sha2::Digest; - -// Other than the 'kty' (key type) parameter, which must be present in all JWKs, Section 4 of RFC -// 7517 states that "member names used for representing key parameters for different keys types -// need not be distinct." Therefore, it's possible that future or non-standard key types will supply -// some of the following parameters but with different types, causing deserialization to fail. To -// support such key types, we'll need to define a new impl for JsonWebKey. Deserializing the new -// impl would probably need to involve first deserializing the raw values to access the 'kty' -// parameter, and then deserializing the fields and types appropriate for that key type. -/// Public or symmetric key expressed as a JSON Web Key. -#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)] -pub struct CoreJsonWebKey { - pub(crate) kty: CoreJsonWebKeyType, - #[serde(rename = "use", skip_serializing_if = "Option::is_none")] - pub(crate) use_: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub(crate) kid: Option, - - /// The algorithm intended to be used with this key (see - /// [RFC 7517](https://www.rfc-editor.org/rfc/rfc7517#section-4.4)). - /// - /// It can either be an algorithm intended for use with JWS or JWE, or something different. - #[cfg(feature = "jwk-alg")] - #[serde(skip_serializing_if = "Option::is_none")] - pub(crate) alg: Option< - JsonWebTokenAlgorithm< - CoreJweContentEncryptionAlgorithm, - CoreJwsSigningAlgorithm, - CoreJsonWebKeyType, - >, - >, - - // From RFC 7517, Section 4: "Additional members can be present in the JWK; if not understood - // by implementations encountering them, they MUST be ignored. Member names used for - // representing key parameters for different keys types need not be distinct." - // Hence, we set fields we fail to deserialize (understand) as None. - #[serde( - default, - deserialize_with = "deserialize_option_or_none", - skip_serializing_if = "Option::is_none" - )] - pub(crate) n: Option, - #[serde( - default, - deserialize_with = "deserialize_option_or_none", - skip_serializing_if = "Option::is_none" - )] - pub(crate) e: Option, - - //Elliptic Curve - #[serde( - default, - deserialize_with = "deserialize_option_or_none", - skip_serializing_if = "Option::is_none" - )] - pub(crate) crv: Option, - #[serde( - default, - deserialize_with = "deserialize_option_or_none", - skip_serializing_if = "Option::is_none" - )] - pub(crate) x: Option, - #[serde( - default, - deserialize_with = "deserialize_option_or_none", - skip_serializing_if = "Option::is_none" - )] - pub(crate) y: Option, - - #[serde( - default, - deserialize_with = "deserialize_option_or_none", - skip_serializing_if = "Option::is_none" - )] - pub(crate) d: Option, - - // Used for symmetric keys, which we only generate internally from the client secret; these - // are never part of the JWK set. - #[serde( - default, - deserialize_with = "deserialize_option_or_none", - skip_serializing_if = "Option::is_none" - )] - pub(crate) k: Option, -} -impl CoreJsonWebKey { - /// Instantiate a new RSA public key from the raw modulus (`n`) and public exponent (`e`), - /// along with an optional (but recommended) key ID. - /// - /// The key ID is used for matching signed JSON Web Tokens with the keys used for verifying - /// their signatures. - pub fn new_rsa(n: Vec, e: Vec, kid: Option) -> Self { - Self { - kty: CoreJsonWebKeyType::RSA, - use_: Some(CoreJsonWebKeyUse::Signature), - kid, - n: Some(Base64UrlEncodedBytes::new(n)), - e: Some(Base64UrlEncodedBytes::new(e)), - k: None, - crv: None, - x: None, - y: None, - d: None, - #[cfg(feature = "jwk-alg")] - alg: None, - } - } - /// Instantiate a new EC public key from the raw x (`x`) and y(`y`) part of the curve, - /// along with an optional (but recommended) key ID. - /// - /// The key ID is used for matching signed JSON Web Tokens with the keys used for verifying - /// their signatures. - pub fn new_ec( - x: Vec, - y: Vec, - crv: CoreJsonCurveType, - kid: Option, - ) -> Self { - Self { - kty: CoreJsonWebKeyType::EllipticCurve, - use_: Some(CoreJsonWebKeyUse::Signature), - kid, - n: None, - e: None, - k: None, - crv: Some(crv), - x: Some(Base64UrlEncodedBytes::new(x)), - y: Some(Base64UrlEncodedBytes::new(y)), - d: None, - #[cfg(feature = "jwk-alg")] - alg: None, - } - } - - /// Instantiate a new Octet Key-Pair public key from the raw x (`x`) part of the curve, - /// along with an optional (but recommended) key ID. - /// - /// The key ID is used for matching signed JSON Web Tokens with the keys used for verifying - /// their signatures. - pub fn new_okp(x: Vec, crv: CoreJsonCurveType, kid: Option) -> Self { - Self { - kty: CoreJsonWebKeyType::OctetKeyPair, - use_: Some(CoreJsonWebKeyUse::Signature), - kid, - n: None, - e: None, - k: None, - crv: Some(crv), - x: Some(Base64UrlEncodedBytes::new(x)), - y: None, - d: None, - #[cfg(feature = "jwk-alg")] - alg: None, - } - } -} - -impl JsonWebKey for CoreJsonWebKey { - fn key_id(&self) -> Option<&JsonWebKeyId> { - self.kid.as_ref() - } - fn key_type(&self) -> &CoreJsonWebKeyType { - &self.kty - } - fn key_use(&self) -> Option<&CoreJsonWebKeyUse> { - self.use_.as_ref() - } - - fn new_symmetric(key: Vec) -> Self { - Self { - kty: CoreJsonWebKeyType::Symmetric, - use_: None, - kid: None, - n: None, - e: None, - k: Some(Base64UrlEncodedBytes::new(key)), - crv: None, - x: None, - y: None, - d: None, - #[cfg(feature = "jwk-alg")] - alg: None, - } - } - - fn verify_signature( - &self, - signature_alg: &CoreJwsSigningAlgorithm, - message: &[u8], - signature: &[u8], - ) -> Result<(), SignatureVerificationError> { - use hmac::Mac; - - check_key_compatibility(self, signature_alg) - .map_err(|e| SignatureVerificationError::InvalidKey(e.to_owned()))?; - - match *signature_alg { - CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256 => { - let message = { - let mut hasher = sha2::Sha256::new(); - hasher.update(message); - &hasher.finalize() - }; - crypto::verify_rsa_signature( - self, - rsa::Pkcs1v15Sign::new::(), - message, - signature, - ) - } - CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha384 => { - let message = { - let mut hasher = sha2::Sha384::new(); - hasher.update(message); - &hasher.finalize() - }; - crypto::verify_rsa_signature( - self, - rsa::Pkcs1v15Sign::new::(), - message, - signature, - ) - } - CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha512 => { - let message = { - let mut hasher = sha2::Sha512::new(); - hasher.update(message); - &hasher.finalize() - }; - crypto::verify_rsa_signature( - self, - rsa::Pkcs1v15Sign::new::(), - message, - signature, - ) - } - CoreJwsSigningAlgorithm::RsaSsaPssSha256 => { - let message = { - let mut hasher = sha2::Sha256::new(); - hasher.update(message); - &hasher.finalize() - }; - crypto::verify_rsa_signature( - self, - rsa::Pss::new::(), - message, - signature, - ) - } - CoreJwsSigningAlgorithm::RsaSsaPssSha384 => { - let message = { - let mut hasher = sha2::Sha384::new(); - hasher.update(message); - &hasher.finalize() - }; - crypto::verify_rsa_signature( - self, - rsa::Pss::new::(), - message, - signature, - ) - } - CoreJwsSigningAlgorithm::RsaSsaPssSha512 => { - let message = { - let mut hasher = sha2::Sha512::new(); - hasher.update(message); - &hasher.finalize() - }; - crypto::verify_rsa_signature( - self, - rsa::Pss::new::(), - message, - signature, - ) - } - CoreJwsSigningAlgorithm::HmacSha256 => { - let mut mac = hmac::Hmac::::new_from_slice( - self.k.as_ref().ok_or_else(|| { - SignatureVerificationError::InvalidKey( - "Symmetric key `k` is missing".to_string(), - ) - })?, - ) - .map_err(|e| { - SignatureVerificationError::Other(format!("Could not create key: {}", e)) - })?; - mac.update(message); - mac.verify(signature.into()) - .map_err(|_| SignatureVerificationError::CryptoError("bad HMAC".to_string())) - } - CoreJwsSigningAlgorithm::HmacSha384 => { - let mut mac = hmac::Hmac::::new_from_slice( - self.k.as_ref().ok_or_else(|| { - SignatureVerificationError::InvalidKey( - "Symmetric key `k` is missing".to_string(), - ) - })?, - ) - .map_err(|e| { - SignatureVerificationError::Other(format!("Could not create key: {}", e)) - })?; - mac.update(message); - mac.verify(signature.into()) - .map_err(|_| SignatureVerificationError::CryptoError("bad HMAC".to_string())) - } - CoreJwsSigningAlgorithm::HmacSha512 => { - let mut mac = hmac::Hmac::::new_from_slice( - self.k.as_ref().ok_or_else(|| { - SignatureVerificationError::InvalidKey( - "Symmetric key `k` is missing".to_string(), - ) - })?, - ) - .map_err(|e| { - SignatureVerificationError::Other(format!("Could not create key: {}", e)) - })?; - mac.update(message); - mac.verify(signature.into()) - .map_err(|_| SignatureVerificationError::CryptoError("bad HMAC".to_string())) - } - CoreJwsSigningAlgorithm::EcdsaP256Sha256 => { - if matches!(self.crv, Some(CoreJsonCurveType::P256)) { - crypto::verify_ec_signature(self, message, signature) - } else { - Err(SignatureVerificationError::InvalidKey( - "Key uses different CRV than JWT".to_string(), - )) - } - } - CoreJwsSigningAlgorithm::EcdsaP384Sha384 => { - if matches!(self.crv, Some(CoreJsonCurveType::P384)) { - crypto::verify_ec_signature(self, message, signature) - } else { - Err(SignatureVerificationError::InvalidKey( - "Key uses different CRV than JWT".to_string(), - )) - } - } - CoreJwsSigningAlgorithm::EdDsaEd25519 => { - if matches!(self.crv, Some(CoreJsonCurveType::Ed25519)) { - crypto::verify_ed_signature(self, message, signature) - } else { - Err(SignatureVerificationError::InvalidKey( - "Key uses different CRV than JWT".to_string(), - )) - } - } - ref other => Err(SignatureVerificationError::UnsupportedAlg( - serde_plain::to_string(other).unwrap_or_else(|err| { - panic!( - "signature alg {:?} failed to serialize to a string: {}", - other, err - ) - }), - )), - } - } - - #[cfg(feature = "jwk-alg")] - fn signing_alg(&self) -> JsonWebKeyAlgorithm<&CoreJwsSigningAlgorithm> { - match self.alg { - None => JsonWebKeyAlgorithm::Unspecified, - Some(JsonWebTokenAlgorithm::Signature(ref alg, _)) => { - JsonWebKeyAlgorithm::Algorithm(alg) - } - Some(_) => JsonWebKeyAlgorithm::Unsupported, - } - } -} - -/// HMAC secret key. -/// -/// This key can be used for signing messages, or converted to a `CoreJsonWebKey` for verifying -/// them. -#[derive(Clone)] -pub struct CoreHmacKey { - secret: Vec, -} -impl CoreHmacKey { - /// Instantiate a new key from the specified secret bytes. - pub fn new(secret: T) -> Self - where - T: Into>, - { - Self { - secret: secret.into(), - } - } -} -impl - PrivateSigningKey< - CoreJwsSigningAlgorithm, - CoreJsonWebKeyType, - CoreJsonWebKeyUse, - CoreJsonWebKey, - > for CoreHmacKey -{ - fn sign( - &self, - signature_alg: &CoreJwsSigningAlgorithm, - message: &[u8], - ) -> Result, SigningError> { - use hmac::Mac; - match *signature_alg { - CoreJwsSigningAlgorithm::HmacSha256 => { - let mut mac = hmac::Hmac::::new_from_slice(&self.secret) - .map_err(|e| SigningError::Other(format!("Could not create key: {}", e)))?; - mac.update(message); - let result = mac.finalize(); - Ok(result.into_bytes().as_slice().to_vec()) - } - CoreJwsSigningAlgorithm::HmacSha384 => { - let mut mac = hmac::Hmac::::new_from_slice(&self.secret) - .map_err(|e| SigningError::Other(format!("Could not create key: {}", e)))?; - mac.update(message); - let result = mac.finalize(); - Ok(result.into_bytes().as_slice().to_vec()) - } - CoreJwsSigningAlgorithm::HmacSha512 => { - let mut mac = hmac::Hmac::::new_from_slice(&self.secret) - .map_err(|e| SigningError::Other(format!("Could not create key: {}", e)))?; - mac.update(message); - let result = mac.finalize(); - Ok(result.into_bytes().as_slice().to_vec()) - } - ref other => Err(SigningError::UnsupportedAlg( - serde_plain::to_string(other).unwrap_or_else(|err| { - panic!( - "signature alg {:?} failed to serialize to a string: {}", - other, err - ) - }), - )), - } - } - - fn as_verification_key(&self) -> CoreJsonWebKey { - CoreJsonWebKey::new_symmetric(self.secret.clone()) - } -} - -enum EdDsaSigningKey { - Ed25519(ed25519_dalek::SigningKey), -} - -impl EdDsaSigningKey { - fn from_ed25519_pem(pem: &str) -> Result { - Ok(Self::Ed25519( - ed25519_dalek::SigningKey::from_pkcs8_pem(pem).map_err(|err| err.to_string())?, - )) - } - - fn sign(&self, message: &[u8]) -> Vec { - match self { - Self::Ed25519(key) => { - let signature = key.sign(message); - - signature.to_vec() - } - } - } -} - -/// EdDSA Private Key. -/// -/// This key can be used for signing messages, or converted to a `CoreJsonWebKey` for verifying -/// them. -pub struct CoreEdDsaPrivateSigningKey { - kid: Option, - key_pair: EdDsaSigningKey, -} -impl CoreEdDsaPrivateSigningKey { - /// Converts an EdDSA private key (in PEM format) to a JWK representing its public key. - pub fn from_ed25519_pem(pem: &str, kid: Option) -> Result { - Ok(Self { - kid, - key_pair: EdDsaSigningKey::from_ed25519_pem(pem)?, - }) - } -} -impl - PrivateSigningKey< - CoreJwsSigningAlgorithm, - CoreJsonWebKeyType, - CoreJsonWebKeyUse, - CoreJsonWebKey, - > for CoreEdDsaPrivateSigningKey -{ - fn sign( - &self, - signature_alg: &CoreJwsSigningAlgorithm, - message: &[u8], - ) -> Result, SigningError> { - match *signature_alg { - CoreJwsSigningAlgorithm::EdDsaEd25519 => Ok(self.key_pair.sign(message)), - ref other => Err(SigningError::UnsupportedAlg( - serde_plain::to_string(other).unwrap_or_else(|err| { - panic!( - "signature alg {:?} failed to serialize to a string: {}", - other, err - ) - }), - )), - } - } - - fn as_verification_key(&self) -> CoreJsonWebKey { - match &self.key_pair { - EdDsaSigningKey::Ed25519(key) => CoreJsonWebKey { - kty: CoreJsonWebKeyType::OctetKeyPair, - use_: Some(CoreJsonWebKeyUse::Signature), - kid: self.kid.clone(), - n: None, - e: None, - crv: Some(CoreJsonCurveType::Ed25519), - x: Some(Base64UrlEncodedBytes::new( - key.verifying_key().as_bytes().to_vec(), - )), - y: None, - d: None, - k: None, - #[cfg(feature = "jwk-alg")] - alg: None, - }, - } - } -} - -/// Trait used to allow testing with an alternative RNG. -/// Clone is necessary to get a mutable version of the RNG. -pub(crate) trait RngClone: dyn_clone::DynClone + rand::RngCore + rand::CryptoRng {} -dyn_clone::clone_trait_object!(RngClone); -impl RngClone for T where T: rand::RngCore + rand::CryptoRng + Clone {} - -/// RSA private key. -/// -/// This key can be used for signing messages, or converted to a `CoreJsonWebKey` for verifying -/// them. -pub struct CoreRsaPrivateSigningKey { - key_pair: rsa::RsaPrivateKey, - rng: Box, - kid: Option, -} -impl CoreRsaPrivateSigningKey { - /// Converts an RSA private key (in PEM format) to a JWK representing its public key. - pub fn from_pem(pem: &str, kid: Option) -> Result { - Self::from_pem_internal(pem, Box::new(rand::rngs::OsRng), kid) - } - - pub(crate) fn from_pem_internal( - pem: &str, - rng: Box, - kid: Option, - ) -> Result { - let key_pair = rsa::RsaPrivateKey::from_pkcs1_pem(pem).map_err(|err| err.to_string())?; - Ok(Self { key_pair, rng, kid }) - } -} -impl - PrivateSigningKey< - CoreJwsSigningAlgorithm, - CoreJsonWebKeyType, - CoreJsonWebKeyUse, - CoreJsonWebKey, - > for CoreRsaPrivateSigningKey -{ - fn sign( - &self, - signature_alg: &CoreJwsSigningAlgorithm, - msg: &[u8], - ) -> Result, SigningError> { - match *signature_alg { - CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256 => { - let mut hasher = sha2::Sha256::new(); - hasher.update(msg); - let hash = hasher.finalize().to_vec(); - - self.key_pair - .sign_with_rng( - &mut dyn_clone::clone_box(&self.rng), - rsa::Pkcs1v15Sign::new::(), - &hash, - ) - .map_err(|_| SigningError::CryptoError) - } - CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha384 => { - let mut hasher = sha2::Sha384::new(); - hasher.update(msg); - let hash = hasher.finalize().to_vec(); - - self.key_pair - .sign_with_rng( - &mut dyn_clone::clone_box(&self.rng), - rsa::Pkcs1v15Sign::new::(), - &hash, - ) - .map_err(|_| SigningError::CryptoError) - } - CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha512 => { - let mut hasher = sha2::Sha512::new(); - hasher.update(msg); - let hash = hasher.finalize().to_vec(); - - self.key_pair - .sign_with_rng( - &mut dyn_clone::clone_box(&self.rng), - rsa::Pkcs1v15Sign::new::(), - &hash, - ) - .map_err(|_| SigningError::CryptoError) - } - CoreJwsSigningAlgorithm::RsaSsaPssSha256 => { - let mut hasher = sha2::Sha256::new(); - hasher.update(msg); - let hash = hasher.finalize().to_vec(); - - self.key_pair - .sign_with_rng( - &mut dyn_clone::clone_box(&self.rng), - rsa::Pss::new_with_salt::(hash.len()), - &hash, - ) - .map_err(|_| SigningError::CryptoError) - } - CoreJwsSigningAlgorithm::RsaSsaPssSha384 => { - let mut hasher = sha2::Sha384::new(); - hasher.update(msg); - let hash = hasher.finalize().to_vec(); - - self.key_pair - .sign_with_rng( - &mut dyn_clone::clone_box(&self.rng), - rsa::Pss::new_with_salt::(hash.len()), - &hash, - ) - .map_err(|_| SigningError::CryptoError) - } - CoreJwsSigningAlgorithm::RsaSsaPssSha512 => { - let mut hasher = sha2::Sha512::new(); - hasher.update(msg); - let hash = hasher.finalize().to_vec(); - - self.key_pair - .sign_with_rng( - &mut dyn_clone::clone_box(&self.rng), - rsa::Pss::new_with_salt::(hash.len()), - &hash, - ) - .map_err(|_| SigningError::CryptoError) - } - ref other => Err(SigningError::UnsupportedAlg( - serde_plain::to_string(other).unwrap_or_else(|err| { - panic!( - "signature alg {:?} failed to serialize to a string: {}", - other, err - ) - }), - )), - } - } - - fn as_verification_key(&self) -> CoreJsonWebKey { - use rsa::traits::PublicKeyParts; - - let public_key = self.key_pair.to_public_key(); - CoreJsonWebKey { - kty: CoreJsonWebKeyType::RSA, - use_: Some(CoreJsonWebKeyUse::Signature), - kid: self.kid.clone(), - n: Some(Base64UrlEncodedBytes::new(public_key.n().to_bytes_be())), - e: Some(Base64UrlEncodedBytes::new(public_key.e().to_bytes_be())), - k: None, - crv: None, - x: None, - y: None, - d: None, - #[cfg(feature = "jwk-alg")] - alg: None, - } - } -} - -/// Type of JSON Web Key. -#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)] -#[non_exhaustive] -pub enum CoreJsonWebKeyType { - /// Elliptic Curve Cryptography (ECC) key. - /// - /// ECC algorithms such as ECDSA are currently unsupported. - #[serde(rename = "EC")] - EllipticCurve, - /// RSA key. - #[serde(rename = "RSA")] - RSA, - /// EdDSA key. - #[serde(rename = "OKP")] - OctetKeyPair, - /// Symmetric key. - #[serde(rename = "oct")] - Symmetric, -} -impl JsonWebKeyType for CoreJsonWebKeyType {} - -/// Type of EC-Curve -#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)] -#[non_exhaustive] -pub enum CoreJsonCurveType { - /// P-256 Curve - #[serde(rename = "P-256")] - P256, - /// P-384 Curve - #[serde(rename = "P-384")] - P384, - /// P-521 Curve (currently not supported) - #[serde(rename = "P-521")] - P521, - /// Ed25519 Curve - #[serde(rename = "Ed25519")] - Ed25519, -} -impl JsonCurveType for CoreJsonWebKeyType {} - -/// Usage restriction for a JSON Web key. -#[derive(Clone, Debug, PartialEq, Eq)] -#[non_exhaustive] -pub enum CoreJsonWebKeyUse { - /// Key may be used for digital signatures. - Signature, - - /// Key may be used for encryption. - Encryption, - - /// Fallback case for other key uses not understood by this library. - Other(String), -} -impl CoreJsonWebKeyUse { - fn from_str(s: &str) -> Self { - match s { - "sig" => Self::Signature, - "enc" => Self::Encryption, - other => Self::Other(other.to_string()), - } - } -} -impl AsRef for CoreJsonWebKeyUse { - fn as_ref(&self) -> &str { - match self { - CoreJsonWebKeyUse::Signature => "sig", - CoreJsonWebKeyUse::Encryption => "enc", - CoreJsonWebKeyUse::Other(other) => other.as_str(), - } - } -} -impl JsonWebKeyUse for CoreJsonWebKeyUse { - fn allows_signature(&self) -> bool { - matches!(*self, CoreJsonWebKeyUse::Signature) - } - fn allows_encryption(&self) -> bool { - matches!(*self, CoreJsonWebKeyUse::Encryption) - } -} -// FIXME: Once https://github.com/serde-rs/serde/issues/912 is resolved, use #[serde(other)] instead -// of custom serializer/deserializers. Right now this isn't possible because serde(other) only -// supports unit variants. -deserialize_from_str!(CoreJsonWebKeyUse); -serialize_as_str!(CoreJsonWebKeyUse); - -#[cfg(test)] -mod tests { - use crate::core::jwk::CoreJsonCurveType; - use crate::core::{ - CoreEdDsaPrivateSigningKey, CoreHmacKey, CoreJsonWebKey, CoreJsonWebKeySet, - CoreJsonWebKeyType, CoreJsonWebKeyUse, CoreJwsSigningAlgorithm, CoreRsaPrivateSigningKey, - }; - use crate::jwt::tests::{ - TEST_EC_PUB_KEY_P256, TEST_EC_PUB_KEY_P384, TEST_ED_PUB_KEY_ED25519, TEST_RSA_PUB_KEY, - }; - use crate::types::Base64UrlEncodedBytes; - use crate::types::{JsonWebKey, JsonWebKeyId}; - use crate::verification::SignatureVerificationError; - #[cfg(feature = "jwk-alg")] - use crate::{core::CoreJweContentEncryptionAlgorithm, jwt::JsonWebTokenAlgorithm}; - use crate::{PrivateSigningKey, SigningError}; - - use rand::rngs::mock::StepRng; - use rand::{CryptoRng, RngCore}; - use rsa::rand_core; - - #[test] - fn test_core_jwk_deserialization_rsa() { - let json = "{ - \"kty\": \"RSA\", - \"use\": \"sig\", - \"kid\": \"2011-04-29\", - \"n\": \"0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhD\ - R1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6C\ - f0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1\ - n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1\ - jF44-csFCur-kEgU8awapJzKnqDKgw\", - \"e\": \"AQAB\" - }"; - - let key: CoreJsonWebKey = serde_json::from_str(json).expect("deserialization failed"); - assert_eq!(key.kty, CoreJsonWebKeyType::RSA); - assert_eq!(key.use_, Some(CoreJsonWebKeyUse::Signature)); - assert_eq!(key.kid, Some(JsonWebKeyId::new("2011-04-29".to_string()))); - assert_eq!( - key.n, - Some(Base64UrlEncodedBytes::new(vec![ - 210, 252, 123, 106, 10, 30, 108, 103, 16, 74, 235, 143, 136, 178, 87, 102, 155, 77, - 246, 121, 221, 173, 9, 155, 92, 74, 108, 217, 168, 128, 21, 181, 161, 51, 191, 11, - 133, 108, 120, 113, 182, 223, 0, 11, 85, 79, 206, 179, 194, 237, 81, 43, 182, 143, - 20, 92, 110, 132, 52, 117, 47, 171, 82, 161, 207, 193, 36, 64, 143, 121, 181, 138, - 69, 120, 193, 100, 40, 133, 87, 137, 247, 162, 73, 227, 132, 203, 45, 159, 174, 45, - 103, 253, 150, 251, 146, 108, 25, 142, 7, 115, 153, 253, 200, 21, 192, 175, 9, 125, - 222, 90, 173, 239, 244, 77, 231, 14, 130, 127, 72, 120, 67, 36, 57, 191, 238, 185, - 96, 104, 208, 71, 79, 197, 13, 109, 144, 191, 58, 152, 223, 175, 16, 64, 200, 156, - 2, 214, 146, 171, 59, 60, 40, 150, 96, 157, 134, 253, 115, 183, 116, 206, 7, 64, - 100, 124, 238, 234, 163, 16, 189, 18, 249, 133, 168, 235, 159, 89, 253, 212, 38, - 206, 165, 178, 18, 15, 79, 42, 52, 188, 171, 118, 75, 126, 108, 84, 214, 132, 2, - 56, 188, 196, 5, 135, 165, 158, 102, 237, 31, 51, 137, 69, 119, 99, 92, 71, 10, - 247, 92, 249, 44, 32, 209, 218, 67, 225, 191, 196, 25, 226, 34, 166, 240, 208, 187, - 53, 140, 94, 56, 249, 203, 5, 10, 234, 254, 144, 72, 20, 241, 172, 26, 164, 156, - 202, 158, 160, 202, 131, - ])) - ); - assert_eq!(key.e, Some(Base64UrlEncodedBytes::new(vec![1, 0, 1]))); - assert_eq!(key.k, None); - } - #[test] - fn test_core_jwk_deserialization_ec() { - let json = "{ - \"kty\": \"EC\", - \"use\": \"sig\", - \"kid\": \"2011-04-29\", - \"crv\": \"P-256\", - \"x\": \"kXCGZIr3oI6sKbnT6rRsIdxFXw3_VbLk_cveajgqXk8\", - \"y\": \"StDvKIgXqAxJ6DuebREh-1vgvZRW3dfrOxSIKzBtRI0\" - }"; - - let key: CoreJsonWebKey = serde_json::from_str(json).expect("deserialization failed"); - assert_eq!(key.kty, CoreJsonWebKeyType::EllipticCurve); - assert_eq!(key.use_, Some(CoreJsonWebKeyUse::Signature)); - assert_eq!(key.kid, Some(JsonWebKeyId::new("2011-04-29".to_string()))); - assert_eq!(key.crv, Some(CoreJsonCurveType::P256)); - assert_eq!( - key.y, - Some(Base64UrlEncodedBytes::new(vec![ - 0x4a, 0xd0, 0xef, 0x28, 0x88, 0x17, 0xa8, 0x0c, 0x49, 0xe8, 0x3b, 0x9e, 0x6d, 0x11, - 0x21, 0xfb, 0x5b, 0xe0, 0xbd, 0x94, 0x56, 0xdd, 0xd7, 0xeb, 0x3b, 0x14, 0x88, 0x2b, - 0x30, 0x6d, 0x44, 0x8d - ])) - ); - assert_eq!( - key.x, - Some(Base64UrlEncodedBytes::new(vec![ - 0x91, 0x70, 0x86, 0x64, 0x8a, 0xf7, 0xa0, 0x8e, 0xac, 0x29, 0xb9, 0xd3, 0xea, 0xb4, - 0x6c, 0x21, 0xdc, 0x45, 0x5f, 0x0d, 0xff, 0x55, 0xb2, 0xe4, 0xfd, 0xcb, 0xde, 0x6a, - 0x38, 0x2a, 0x5e, 0x4f - ])) - ); - } - - #[test] - fn test_core_jwk_deserialization_ed() { - let json = "{ - \"alg\": \"Ed25519\", - \"crv\": \"Ed25519\", - \"kty\": \"OKP\", - \"use\": \"sig\", - \"x\": \"vZ3CX884r0qNJ18pgXUTvFufK3ZmDzQfvMROJz6CLBc\" - }"; - - let key: CoreJsonWebKey = serde_json::from_str(json).expect("deserialization failed"); - assert_eq!(key.kty, CoreJsonWebKeyType::OctetKeyPair); - assert_eq!(key.use_, Some(CoreJsonWebKeyUse::Signature)); - assert_eq!(key.crv, Some(CoreJsonCurveType::Ed25519)); - assert_eq!( - key.x, - Some(Base64UrlEncodedBytes::new(vec![ - 0xBD, 0x9D, 0xC2, 0x5F, 0xCF, 0x38, 0xAF, 0x4A, 0x8D, 0x27, 0x5F, 0x29, 0x81, 0x75, - 0x13, 0xBC, 0x5B, 0x9F, 0x2B, 0x76, 0x66, 0x0F, 0x34, 0x1F, 0xBC, 0xC4, 0x4E, 0x27, - 0x3E, 0x82, 0x2C, 0x17 - ])) - ); - } - - #[test] - fn test_core_jwk_deserialization_symmetric() { - let json = "{\ - \"kty\":\"oct\", - \"alg\":\"A128GCM\", - \"k\":\"GawgguFyGrWKav7AX4VKUg\" - }"; - - let key: CoreJsonWebKey = serde_json::from_str(json).expect("deserialization failed"); - assert_eq!(key.kty, CoreJsonWebKeyType::Symmetric); - assert_eq!(key.use_, None); - assert_eq!(key.kid, None); - assert_eq!(key.n, None); - assert_eq!(key.e, None); - #[cfg(feature = "jwk-alg")] - assert_eq!( - key.alg, - Some(JsonWebTokenAlgorithm::Encryption( - CoreJweContentEncryptionAlgorithm::Aes128Gcm - )) - ); - assert_eq!( - key.k, - Some(Base64UrlEncodedBytes::new(vec![ - 25, 172, 32, 130, 225, 114, 26, 181, 138, 106, 254, 192, 95, 133, 74, 82, - ])) - ); - } - - #[test] - fn test_core_jwk_deserialization_no_optional() { - let json = "{\"kty\":\"oct\"}"; - let key: CoreJsonWebKey = serde_json::from_str(json).expect("deserialization failed"); - assert_eq!(key.kty, CoreJsonWebKeyType::Symmetric); - assert_eq!(key.use_, None); - assert_eq!(key.kid, None); - assert_eq!(key.n, None); - assert_eq!(key.e, None); - assert_eq!(key.k, None); - } - - #[test] - fn test_core_jwk_deserialization_unrecognized() { - // Unrecognized fields should be ignored during deserialization - let json = "{\ - \"kty\": \"oct\", - \"unrecognized\": 1234 - }"; - let key: CoreJsonWebKey = serde_json::from_str(json).expect("deserialization failed"); - assert_eq!(key.kty, CoreJsonWebKeyType::Symmetric); - } - - #[test] - fn test_core_jwk_deserialization_dupe_fields() { - // From RFC 7517, Section 4: - // "The member names within a JWK MUST be unique; JWK parsers MUST either - // reject JWKs with duplicate member names or use a JSON parser that - // returns only the lexically last duplicate member name, as specified - // in Section 15.12 (The JSON Object) of ECMAScript 5.1 [ECMAScript]." - let json = "{\ - \"kty\":\"oct\", - \"k\":\"GawgguFyGrWKav7AX4VKUg\", - \"k\":\"GawgguFyGrWKav7AX4VKVg\" - }"; - - assert!(serde_json::from_str::(json) - .expect_err("deserialization must fail when duplicate fields are present") - .to_string() - // This is probably not ideal since the serde/serde_json contracts don't guarantee this - // error message. However, we want to be sure that this fails for the expected reason - // and not by happenstance, so this is fine for now. - .contains("duplicate field")); - } - - fn verify_signature( - key: &CoreJsonWebKey, - alg: &CoreJwsSigningAlgorithm, - signing_input: &str, - signature_base64: &str, - ) { - let signature = - base64::decode_config(signature_base64, crate::core::base64_url_safe_no_pad()) - .expect("failed to base64url decode"); - key.verify_signature(alg, signing_input.as_bytes(), &signature) - .expect("signature verification failed"); - match key - .verify_signature( - alg, - (signing_input.to_string() + "foobar").as_bytes(), - &signature, - ) - .expect_err("signature verification should fail") - { - SignatureVerificationError::CryptoError(_) => {} - other => panic!("unexpected error: {:?}", other), - } - } - - fn verify_invalid_signature( - key: &CoreJsonWebKey, - alg: &CoreJwsSigningAlgorithm, - signing_input: &str, - signature_base64: &str, - ) { - let signature = - base64::decode_config(signature_base64, crate::core::base64_url_safe_no_pad()) - .expect("failed to base64url decode"); - match key - .verify_signature(alg, signing_input.as_bytes(), &signature) - .expect_err("signature verification should fail") - { - SignatureVerificationError::CryptoError(_) => {} - other => panic!("unexpected error: {:?}", other), - } - } - - #[test] - fn test_eddsa_verification() { - let key_ed25519: CoreJsonWebKey = - serde_json::from_str(TEST_ED_PUB_KEY_ED25519).expect("deserialization failed"); - let pkcs1_signing_input = "eyJhbGciOiJFZDI1NTE5IiwidHlwIjoiSldUIn0.eyJpc3MiOiJqb2UifQ"; - let signature_ed25519 = "Augr7UH6hUbWVN0PHqSD5U0bb8y9UOw_eef09ZS5d5haUar_qAto8gyLJxUhNF5wHPoXhdvSGowkPvjiKsEsCQ"; - - let signature_ed25519_other = "xb4NH-q33sCaRXf1ZhnzQxd4o5ZkBWKd9vGibacqPMAblW_mIJLm9kGerqHX08SPoeDY-dYUmZQz9ls6csfvAw"; - let signature_ed448 = "xxXVMyaYYePdGfMOdU0nENuc70pKwP3vJuc_jBA0rCW-RtbvBLSsc0D9iCPzhrPmQ2X1nTjPkGiAXJ0_NslDBvy3sHu88N64YhnnYBWwwHttBU0jijn_ikbBUHzUwzGuasRFb1ESG_PwedhEcMi-YAwA"; - - // test ed25519 - verify_signature( - &key_ed25519, - &CoreJwsSigningAlgorithm::EdDsaEd25519, - pkcs1_signing_input, - signature_ed25519, - ); - - // signature from ed448 variant - verify_invalid_signature( - &key_ed25519, - &CoreJwsSigningAlgorithm::EdDsaEd25519, - pkcs1_signing_input, - signature_ed448, - ); - - // different signature - verify_invalid_signature( - &key_ed25519, - &CoreJwsSigningAlgorithm::EdDsaEd25519, - pkcs1_signing_input, - signature_ed25519_other, - ); - - // non-EdDsa key - if let Some(err) = key_ed25519 - .verify_signature( - &CoreJwsSigningAlgorithm::EcdsaP256Sha256, - pkcs1_signing_input.as_bytes(), - signature_ed25519.as_bytes(), - ) - .err() - { - let error_msg = "key type does not match signature algorithm".to_string(); - match err { - SignatureVerificationError::InvalidKey(msg) => { - if msg != error_msg { - panic!("The error should be about key type") - } - } - _ => panic!("We should fail before actual validation"), - } - } - } - - #[test] - fn test_ecdsa_verification() { - let key_p256: CoreJsonWebKey = - serde_json::from_str(TEST_EC_PUB_KEY_P256).expect("deserialization failed"); - let key_p384: CoreJsonWebKey = - serde_json::from_str(TEST_EC_PUB_KEY_P384).expect("deserialization failed"); - let pkcs1_signing_input = "eyJhbGciOiJSUzI1NiIsImtpZCI6ImJpbGJvLmJhZ2dpbnNAaG9iYml0b24uZX\ - hhbXBsZSJ9.\ - SXTigJlzIGEgZGFuZ2Vyb3VzIGJ1c2luZXNzLCBGcm9kbywgZ29pbmcgb3V0IH\ - lvdXIgZG9vci4gWW91IHN0ZXAgb250byB0aGUgcm9hZCwgYW5kIGlmIHlvdSBk\ - b24ndCBrZWVwIHlvdXIgZmVldCwgdGhlcmXigJlzIG5vIGtub3dpbmcgd2hlcm\ - UgeW91IG1pZ2h0IGJlIHN3ZXB0IG9mZiB0by4"; - let signature_p256 = "EnKCtAHhzhqxV2GTr1VEurse2kQ7oHpFoVqM66sYGlmahDRGSlfrVAsGCzdLv66OS2Qf1zt6OPHX-5ZAkMgzlA"; - let signature_p384 = "B_9oDAabMasZ2Yt_cnAS21owaN0uWSInQBPxTqqiM3N3XjkksBRMGqguJLV5WoSMcvqgXwHTTQtbHGuh0Uf4g6LEr7XtO1T2KCttQR27d5YbvVZdORrzCm0Nsm1zkV-i"; - - //test p256 - verify_signature( - &key_p256, - &CoreJwsSigningAlgorithm::EcdsaP256Sha256, - pkcs1_signing_input, - signature_p256, - ); - - //wrong algo should fail before ring validation - if let Some(err) = key_p256 - .verify_signature( - &CoreJwsSigningAlgorithm::EcdsaP384Sha384, - pkcs1_signing_input.as_bytes(), - signature_p256.as_bytes(), - ) - .err() - { - let error_msg = "Key uses different CRV than JWT".to_string(); - match err { - SignatureVerificationError::InvalidKey(msg) => { - if msg != error_msg { - panic!("The error should be about different CRVs") - } - } - _ => panic!("We should fail before actual validation"), - } - } - // suppose we have alg specified correctly, but the signature given is actually a p384 - verify_invalid_signature( - &key_p256, - &CoreJwsSigningAlgorithm::EcdsaP256Sha256, - pkcs1_signing_input, - signature_p384, - ); - - //test p384 - verify_signature( - &key_p384, - &CoreJwsSigningAlgorithm::EcdsaP384Sha384, - pkcs1_signing_input, - signature_p384, - ); - - // suppose we have alg specified correctly, but the signature given is actually a p256 - verify_invalid_signature( - &key_p384, - &CoreJwsSigningAlgorithm::EcdsaP384Sha384, - pkcs1_signing_input, - signature_p256, - ); - - //wrong algo should fail before ring validation - if let Some(err) = key_p384 - .verify_signature( - &CoreJwsSigningAlgorithm::EcdsaP256Sha256, - pkcs1_signing_input.as_bytes(), - signature_p384.as_bytes(), - ) - .err() - { - let error_msg = "Key uses different CRV than JWT".to_string(); - match err { - SignatureVerificationError::InvalidKey(msg) => { - if msg != error_msg { - panic!("The error should be about different CRVs") - } - } - _ => panic!("We should fail before actual validation"), - } - } - } - - #[test] - fn test_rsa_pkcs1_verification() { - let key: CoreJsonWebKey = - serde_json::from_str(TEST_RSA_PUB_KEY).expect("deserialization failed"); - - // Source: https://tools.ietf.org/html/rfc7520#section-4.1 - let pkcs1_signing_input = "eyJhbGciOiJSUzI1NiIsImtpZCI6ImJpbGJvLmJhZ2dpbnNAaG9iYml0b24uZX\ - hhbXBsZSJ9.\ - SXTigJlzIGEgZGFuZ2Vyb3VzIGJ1c2luZXNzLCBGcm9kbywgZ29pbmcgb3V0IH\ - lvdXIgZG9vci4gWW91IHN0ZXAgb250byB0aGUgcm9hZCwgYW5kIGlmIHlvdSBk\ - b24ndCBrZWVwIHlvdXIgZmVldCwgdGhlcmXigJlzIG5vIGtub3dpbmcgd2hlcm\ - UgeW91IG1pZ2h0IGJlIHN3ZXB0IG9mZiB0by4"; - - verify_signature( - &key, - &CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256, - pkcs1_signing_input, - "MRjdkly7_-oTPTS3AXP41iQIGKa80A0ZmTuV5MEaHoxnW2e5CZ5NlKtainoFmK\ - ZopdHM1O2U4mwzJdQx996ivp83xuglII7PNDi84wnB-BDkoBwA78185hX-Es4J\ - IwmDLJK3lfWRa-XtL0RnltuYv746iYTh_qHRD68BNt1uSNCrUCTJDt5aAE6x8w\ - W1Kt9eRo4QPocSadnHXFxnt8Is9UzpERV0ePPQdLuW3IS_de3xyIrDaLGdjluP\ - xUAhb6L2aXic1U12podGU0KLUQSE_oI-ZnmKJ3F4uOZDnd6QZWJushZ41Axf_f\ - cIe8u9ipH84ogoree7vjbU5y18kDquDg", - ); - - verify_signature( - &key, - &CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha384, - pkcs1_signing_input, - "dgTHNAePceEDFodrPybExGb2aF4fHb4bRpb_4bgYHq78fUdHFCScg0bZP51zjB\ - joH-4fr0P7Y8-Sns0GuXRy_itY2Yh0mEdXVn6HwZVOGIVRAuBkY0cAgSXGKU40\ - 1G-GhamiNyNDfN2bwHftPPvCdsChtsLeAUvhWUKSLgIfT-jvMr9iZ5d0SQrUvv\ - G1ReEoBDyKUzqGQehO3CNGJ-QkI8p-fBTa2KHQxct6cU5_anSXCd-kC2rtEQS9\ - E8AcMFLA2Bv9IXsURBRU_bwMgxTG8c6ATDJM8k-zJSSP5a44EFKHUtH1xspYFo\ - KV6Za-frCV8kcFCILMf-4ATlj5Z62o1A", - ); - - verify_signature( - &key, - &CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha512, - pkcs1_signing_input, - "hIRFVu3hlbIM9Xt2V9xldCoF_94BEDg-6kVetoceakgD-9hicX0BnOI3YxR-JQ\ - 0to4saNEdGP1ulvanfa5uK3PnltQr1sJ1l1x_TPNh8vdvZ5WmAtkQcZvRiK580\ - hliHV1l65yLyGH4ckDicOg5VF4BASkBw6sUO_LCB8pMJotK5jQxDbNkPmSGbFV\ - nzVXXy6QI_r6nqmguo5DMFlPeploS-aQ7ArfYqR3gKEp3l5gWWKn86lwVKRGjv\ - zeRMf3ubhKxvHUyU8cE5p1VPpOzTJ3cPwUe68s24Ehf2jpgZIIXb9XQv4L0Unf\ - GAXTBY7Rszx9LvGByoFx3eOpbMvtLQxA", - ); - - // Wrong key type - match key - .verify_signature( - &CoreJwsSigningAlgorithm::EcdsaP256Sha256, - pkcs1_signing_input.as_bytes(), - &Vec::new(), - ) - .expect_err("signature verification should fail") - { - SignatureVerificationError::InvalidKey(_) => {} - other => panic!("unexpected error: {:?}", other), - } - - // Wrong key usage - let enc_key_json = "{ - \"kty\": \"RSA\", - \"kid\": \"bilbo.baggins@hobbiton.example\", - \"use\": \"enc\", - \"n\": \"n4EPtAOCc9AlkeQHPzHStgAbgs7bTZLwUBZdR8_KuKPEHLd4rHVTeT\ - -O-XV2jRojdNhxJWTDvNd7nqQ0VEiZQHz_AJmSCpMaJMRBSFKrKb2wqV\ - wGU_NsYOYL-QtiWN2lbzcEe6XC0dApr5ydQLrHqkHHig3RBordaZ6Aj-\ - oBHqFEHYpPe7Tpe-OfVfHd1E6cS6M1FZcD1NNLYD5lFHpPI9bTwJlsde\ - 3uhGqC0ZCuEHg8lhzwOHrtIQbS0FVbb9k3-tVTU4fg_3L_vniUFAKwuC\ - LqKnS2BYwdq_mzSnbLY7h_qixoR7jig3__kRhuaxwUkRz5iaiQkqgc5g\ - HdrNP5zw\", - \"e\": \"AQAB\" - }"; - let enc_key: CoreJsonWebKey = - serde_json::from_str(enc_key_json).expect("deserialization failed"); - match enc_key - .verify_signature( - &CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256, - pkcs1_signing_input.as_bytes(), - &Vec::new(), - ) - .expect_err("signature verification should fail") - { - SignatureVerificationError::InvalidKey(_) => {} - other => panic!("unexpected error: {:?}", other), - } - - // Key without usage specified should work - let nousage_key_json = "{ - \"kty\": \"RSA\", - \"kid\": \"bilbo.baggins@hobbiton.example\", - \"n\": \"n4EPtAOCc9AlkeQHPzHStgAbgs7bTZLwUBZdR8_KuKPEHLd4rHVTeT\ - -O-XV2jRojdNhxJWTDvNd7nqQ0VEiZQHz_AJmSCpMaJMRBSFKrKb2wqV\ - wGU_NsYOYL-QtiWN2lbzcEe6XC0dApr5ydQLrHqkHHig3RBordaZ6Aj-\ - oBHqFEHYpPe7Tpe-OfVfHd1E6cS6M1FZcD1NNLYD5lFHpPI9bTwJlsde\ - 3uhGqC0ZCuEHg8lhzwOHrtIQbS0FVbb9k3-tVTU4fg_3L_vniUFAKwuC\ - LqKnS2BYwdq_mzSnbLY7h_qixoR7jig3__kRhuaxwUkRz5iaiQkqgc5g\ - HdrNP5zw\", - \"e\": \"AQAB\" - }"; - let nousage_key: CoreJsonWebKey = - serde_json::from_str(nousage_key_json).expect("deserialization failed"); - verify_signature( - &nousage_key, - &CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256, - pkcs1_signing_input, - "MRjdkly7_-oTPTS3AXP41iQIGKa80A0ZmTuV5MEaHoxnW2e5CZ5NlKtainoFmK\ - ZopdHM1O2U4mwzJdQx996ivp83xuglII7PNDi84wnB-BDkoBwA78185hX-Es4J\ - IwmDLJK3lfWRa-XtL0RnltuYv746iYTh_qHRD68BNt1uSNCrUCTJDt5aAE6x8w\ - W1Kt9eRo4QPocSadnHXFxnt8Is9UzpERV0ePPQdLuW3IS_de3xyIrDaLGdjluP\ - xUAhb6L2aXic1U12podGU0KLUQSE_oI-ZnmKJ3F4uOZDnd6QZWJushZ41Axf_f\ - cIe8u9ipH84ogoree7vjbU5y18kDquDg", - ); - } - - #[test] - fn test_rsa_pss_verification() { - let key: CoreJsonWebKey = - serde_json::from_str(TEST_RSA_PUB_KEY).expect("deserialization failed"); - // Source: https://tools.ietf.org/html/rfc7520#section-4.2 - let pss_signing_input = - "eyJhbGciOiJQUzM4NCIsImtpZCI6ImJpbGJvLmJhZ2dpbnNAaG9iYml0b24uZXhhbXBsZSJ9.\ - SXTigJlzIGEgZGFuZ2Vyb3VzIGJ1c2luZXNzLCBGcm9kbywgZ29pbmcgb3V0IH\ - lvdXIgZG9vci4gWW91IHN0ZXAgb250byB0aGUgcm9hZCwgYW5kIGlmIHlvdSBk\ - b24ndCBrZWVwIHlvdXIgZmVldCwgdGhlcmXigJlzIG5vIGtub3dpbmcgd2hlcm\ - UgeW91IG1pZ2h0IGJlIHN3ZXB0IG9mZiB0by4"; - - verify_signature( - &key, - &CoreJwsSigningAlgorithm::RsaSsaPssSha256, - pss_signing_input, - "Y62we_hs07d0qJ2cT_QpbrodwDhPK9rEpNX2b3GqLHFM18YtDlPCr40Xf_yLIosIrt\ - mMP4NgDSCkn2qOcRJBD8zrHumER4JIkGZbRIwU8gYms8xKX2HaveK9vrOjbHoWLjOU\ - nyNpprYUFGdRZ6oebT61bqU2CZrJG_GcqR87W8FOn7kqrCPI7B8oNHgliMke49hOpz\ - mluL20BKN5Mb3O42nwgmiONZK0Pjm2GTIAYRUvNQ741aCWVJ3rnWvo99qWhe86ap_H\ - v40SUSaMwJig5AqC-wHIzYaYU0PlQbi83Dgw7Zft9kL2dGB0vMWY_h2HDgZU0teAcK\ - SkhyH8ZDRyYQ", - ); - - verify_signature( - &key, - &CoreJwsSigningAlgorithm::RsaSsaPssSha384, - pss_signing_input, - "cu22eBqkYDKgIlTpzDXGvaFfz6WGoz7fUDcfT0kkOy42miAh2qyBzk1xEsnk2I\ - pN6-tPid6VrklHkqsGqDqHCdP6O8TTB5dDDItllVo6_1OLPpcbUrhiUSMxbbXU\ - vdvWXzg-UD8biiReQFlfz28zGWVsdiNAUf8ZnyPEgVFn442ZdNqiVJRmBqrYRX\ - e8P_ijQ7p8Vdz0TTrxUeT3lm8d9shnr2lfJT8ImUjvAA2Xez2Mlp8cBE5awDzT\ - 0qI0n6uiP1aCN_2_jLAeQTlqRHtfa64QQSUmFAAjVKPbByi7xho0uTOcbH510a\ - 6GYmJUAfmWjwZ6oD4ifKo8DYM-X72Eaw", - ); - - verify_signature( - &key, - &CoreJwsSigningAlgorithm::RsaSsaPssSha512, - pss_signing_input, - "G8vtysTFbSXht_PU6NdXeYDOSIQhxcp6zFWuvtx2NCtgsm-J22CKqlapp1zjPkXTo4\ - xrYlIgFjQVQZ9Cr7KWJXK7qYUkdfJNkB1E96EQR32ocx_9RQDS_eQNlGWjoDRduD9z\ - 2hKs-S0EhOy39wUeUYbcKA1MpkW71hUPI56Ou5kzclNbe22slB4mYd6Mx0dLOeFDF2\ - C7ZUDxso-cHMh4hU2E8vlp-TZUf9eqAri9T1F_pjRF8WNBj-vrqwy3bCROgIslYA8u\ - c_FEXn6fZ21up5mU9vg5_LdeBoSh4Idmz8HLn5rpVd57AsQ2PbLMsKXcpVUhwP_ID1\ - 7zsAFuCEFJqA", - ); - } - - #[test] - fn test_hmac_sha256_verification() { - // the original spec example also has alg=HS256, which was removed to test other signing algorithms - let key_json = "{ - \"kty\": \"oct\", - \"kid\": \"018c0ae5-4d9b-471b-bfd6-eef314bc7037\", - \"use\": \"sig\", - \"k\": \"hJtXIZ2uSN5kbQfbtTNWbpdmhkV8FJG-Onbc6mxCcYg\" - }"; - - let key: CoreJsonWebKey = serde_json::from_str(key_json).expect("deserialization failed"); - // Source: https://tools.ietf.org/html/rfc7520#section-4.4 - let signing_input = "eyJhbGciOiJIUzI1NiIsImtpZCI6IjAxOGMwYWU1LTRkOWItNDcxYi1iZmQ2LW\ - VlZjMxNGJjNzAzNyJ9.\ - SXTigJlzIGEgZGFuZ2Vyb3VzIGJ1c2luZXNzLCBGcm9kbywgZ29pbmcgb3V0IH\ - lvdXIgZG9vci4gWW91IHN0ZXAgb250byB0aGUgcm9hZCwgYW5kIGlmIHlvdSBk\ - b24ndCBrZWVwIHlvdXIgZmVldCwgdGhlcmXigJlzIG5vIGtub3dpbmcgd2hlcm\ - UgeW91IG1pZ2h0IGJlIHN3ZXB0IG9mZiB0by4"; - - verify_signature( - &key, - &CoreJwsSigningAlgorithm::HmacSha256, - signing_input, - "s0h6KThzkfBBBkLspW1h84VsJZFTsPPqMDA7g1Md7p0", - ); - - verify_signature( - &key, - &CoreJwsSigningAlgorithm::HmacSha384, - signing_input, - "O1jhTTHkuaiubwDZoIBLv6zjEarXHc22NNu05IdYh_yzIKGYXJQcaI2WnF4BCq7j", - ); - - verify_signature( - &key, - &CoreJwsSigningAlgorithm::HmacSha512, - signing_input, - "rdWYqzXuAJp4OW-exqIwrO8HJJQDYu0_fkTIUBHmyHMFJ0pVe7fjP7QtE7BaX-7FN5\ - YiyiM11MwIEAxzxBj6qw", - ); - } - - fn expect_hmac( - secret_key: &CoreHmacKey, - message: &[u8], - alg: &CoreJwsSigningAlgorithm, - expected_sig_base64: &str, - ) { - let sig = secret_key.sign(alg, message).unwrap(); - assert_eq!(expected_sig_base64, base64::encode(&sig)); - - secret_key - .as_verification_key() - .verify_signature(alg, message, &sig) - .unwrap(); - } - - #[test] - fn test_hmac_signing() { - let secret_key = CoreHmacKey::new("my_secret_key"); - let message = "hello HMAC".as_ref(); - expect_hmac( - &secret_key, - message, - &CoreJwsSigningAlgorithm::HmacSha256, - "Pm6UhOcfx6D8LeCG4taMQNQXDTHwnVOSEcB7tidkM2M=", - ); - - expect_hmac( - &secret_key, - message, - &CoreJwsSigningAlgorithm::HmacSha384, - "BiYrxF0XjImSnfqT2n+Tu3EspstKZmVtUHbK77LHerfKNwCikuClNJDAVwr2xMLp", - ); - - expect_hmac( - &secret_key, - message, - &CoreJwsSigningAlgorithm::HmacSha512, - "glKjDMXBhB6sSKGCdLW4QeBOJ3vOgOlbMJjbeus8/KQ3dk7dtsqtrpfoDoW8lrU+rncd2jBWaKnp1zKdpEfSn\ - A==", - ); - - assert_eq!( - secret_key.sign(&CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256, message), - Err(SigningError::UnsupportedAlg("RS256".to_string())), - ); - assert_eq!( - secret_key.sign(&CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha384, message), - Err(SigningError::UnsupportedAlg("RS384".to_string())), - ); - assert_eq!( - secret_key.sign(&CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha512, message), - Err(SigningError::UnsupportedAlg("RS512".to_string())), - ); - assert_eq!( - secret_key.sign(&CoreJwsSigningAlgorithm::RsaSsaPssSha256, message), - Err(SigningError::UnsupportedAlg("PS256".to_string())), - ); - assert_eq!( - secret_key.sign(&CoreJwsSigningAlgorithm::RsaSsaPssSha384, message), - Err(SigningError::UnsupportedAlg("PS384".to_string())), - ); - assert_eq!( - secret_key.sign(&CoreJwsSigningAlgorithm::RsaSsaPssSha512, message), - Err(SigningError::UnsupportedAlg("PS512".to_string())), - ); - assert_eq!( - secret_key.sign(&CoreJwsSigningAlgorithm::EcdsaP256Sha256, message), - Err(SigningError::UnsupportedAlg("ES256".to_string())), - ); - assert_eq!( - secret_key.sign(&CoreJwsSigningAlgorithm::EcdsaP384Sha384, message), - Err(SigningError::UnsupportedAlg("ES384".to_string())), - ); - assert_eq!( - secret_key.sign(&CoreJwsSigningAlgorithm::EcdsaP521Sha512, message), - Err(SigningError::UnsupportedAlg("ES512".to_string())), - ); - assert_eq!( - secret_key.sign(&CoreJwsSigningAlgorithm::None, message), - Err(SigningError::UnsupportedAlg("none".to_string())), - ); - } - - const TEST_ED25519_KEY: &str = "\ - -----BEGIN PRIVATE KEY-----\n\ - MC4CAQAwBQYDK2VwBCIEICWeYPLxoZKHZlQ6rkBi11E9JwchynXtljATLqym/XS9\n\ - -----END PRIVATE KEY-----\ - "; - - // This is just a test key that isn't used for anything else. - const TEST_RSA_KEY: &str = "\ - -----BEGIN RSA PRIVATE KEY-----\n\ - MIIEowIBAAKCAQEAsRMj0YYjy7du6v1gWyKSTJx3YjBzZTG0XotRP0IaObw0k+68\n\ - 30dXadjL5jVhSWNdcg9OyMyTGWfdNqfdrS6ppBqlQNgjZJdloIqL9zOLBZrDm7G4\n\ - +qN4KeZ4/5TyEilq2zOHHGFEzXpOq/UxqVnm3J4fhjqCNaS2nKd7HVVXGBQQ+4+F\n\ - dVT+MyJXemw5maz2F/h324TQi6XoUPEwUddxBwLQFSOlzWnHYMc4/lcyZJ8MpTXC\n\ - MPe/YJFNtb9CaikKUdf8x4mzwH7usSf8s2d6R4dQITzKrjrEJ0u3w3eGkBBapoMV\n\ - FBGPjP3Haz5FsVtHc5VEN3FZVIDF6HrbJH1C4QIDAQABAoIBAHSS3izM+3nc7Bel\n\ - 8S5uRxRKmcm5je6b11u6qiVUFkHWJmMRc6QmqmSThkCq+b4/vUAe1cYZ7+l02Exo\n\ - HOcrZiEULaDP6hUKGqyjKVv3wdlRtt8kFFxlC/HBufzAiNDuFVvzw0oquwnvMCXC\n\ - yQvtlK+/JY/PqvM32cSt+b4o9apySsHqAtdsoHHohK82jsQqIfCi1v8XYV/xRBJB\n\ - cQMCaA0Ls3tFpmJv3JdikyyQxio4kZ5tswghC63znCp1iL+qDq1wjjKzjick9MDb\n\ - Qzb95X09QQP201l1FPWN7Kbhj4ybg6PJGz/VHQcvILcBCoYIc0UY/OMSBt9VN9yD\n\ - wr1WlbECgYEA37difsTMcLmUEN57sicFe1q4lxH6eqnUBjmoKBflx4oMIIyRnfjF\n\ - Jwsu9yIiBkJfBCP85nl2tZdcV0wfZLf6amxB/KMtdfW6r8eoTDzE472OYxSIg1F5\n\ - dI4qn2nBI0Dou0g58xj+Kv0iLaym0pxtyJkSg/rxZGwKb9a+x5WAs50CgYEAyqC0\n\ - NcZs2BRIiT5kEOF6+MeUvarbKh1mangKHKcTdXRrvoJ+Z5izm7FifBixo/79MYpt\n\ - 0VofW0IzYKtAI9KZDq2JcozEbZ+lt/ZPH5QEXO4T39QbDoAG8BbOmEP7l+6m+7QO\n\ - PiQ0WSNjDnwk3W7Zihgg31DH7hyxsxQCapKLcxUCgYAwERXPiPcoDSd8DGFlYK7z\n\ - 1wUsKEe6DT0p7T9tBd1v5wA+ChXLbETn46Y+oQ3QbHg/yn+vAU/5KkFD3G4uVL0w\n\ - Gnx/DIxa+OYYmHxXjQL8r6ClNycxl9LRsS4FPFKsAWk/u///dFI/6E1spNjfDY8k\n\ - 94ab5tHwsqn3Z5tsBHo3nQKBgFUmxbSXh2Qi2fy6+GhTqU7k6G/wXhvLsR9rBKzX\n\ - 1YiVfTXZNu+oL0ptd/q4keZeIN7x0oaY/fZm0pp8PP8Q4HtXmBxIZb+/yG+Pld6q\n\ - YE8BSd7VDu3ABapdm0JHx3Iou4mpOBcLNeiDw3vx1bgsfkTXMPFHzE0XR+H+tak9\n\ - nlalAoGBALAmAF7WBGdOt43Rj8hPaKOM/ahj+6z3CNwVreToNsVBHoyNmiO8q7MC\n\ - +tRo4jgdrzk1pzs66OIHfbx5P1mXKPtgPZhvI5omAY8WqXEgeNqSL1Ksp6LZ2ql/\n\ - ouZns5xwKc9+aRL+GWoAGNzwzcjE8cP52sBy/r0rYXTs/sZo5kgV\n\ - -----END RSA PRIVATE KEY-----\ - "; - - fn expect_ed_sig( - private_key: &CoreEdDsaPrivateSigningKey, - message: &[u8], - alg: &CoreJwsSigningAlgorithm, - expected_sig_base64: &str, - ) { - let sig = private_key.sign(alg, message).unwrap(); - assert_eq!(expected_sig_base64, base64::encode(&sig)); - - let public_key = private_key.as_verification_key(); - public_key.verify_signature(alg, message, &sig).unwrap(); - } - - fn expect_rsa_sig( - private_key: &CoreRsaPrivateSigningKey, - message: &[u8], - alg: &CoreJwsSigningAlgorithm, - expected_sig_base64: &str, - ) { - let sig = private_key.sign(alg, message).unwrap(); - assert_eq!(expected_sig_base64, base64::encode(&sig)); - - let public_key = private_key.as_verification_key(); - public_key.verify_signature(alg, message, &sig).unwrap(); - } - - #[derive(Clone)] - struct TestRng(StepRng); - - impl CryptoRng for TestRng {} - impl RngCore for TestRng { - fn next_u32(&mut self) -> u32 { - self.0.next_u32() - } - fn next_u64(&mut self) -> u64 { - self.0.next_u64() - } - fn fill_bytes(&mut self, dest: &mut [u8]) { - self.0.fill_bytes(dest) - } - fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), rand_core::Error> { - self.0.try_fill_bytes(dest) - } - } - - #[test] - fn test_ed_signing() { - let private_key = CoreEdDsaPrivateSigningKey::from_ed25519_pem( - TEST_ED25519_KEY, - Some(JsonWebKeyId::new("test_key".to_string())), - ) - .unwrap(); - - let public_key_jwk = private_key.as_verification_key(); - let public_key_jwk_str = serde_json::to_string(&public_key_jwk).unwrap(); - - assert_eq!( - "{\ - \"kty\":\"OKP\",\ - \"use\":\"sig\",\ - \"kid\":\"test_key\",\ - \"crv\":\"Ed25519\",\ - \"x\":\"E6lXdyel1n9C1lcr3FK8OsfsfO2ZgcWhPflJ6yIf7e8\"\ - }", - public_key_jwk_str - ); - - let message = "hello EdDsa".as_ref(); - expect_ed_sig( - &private_key, - message, - &CoreJwsSigningAlgorithm::EdDsaEd25519, - "XqP8sXaPrQa37+2lw+aiXv+6pegjioYUgo1/ShcX6kRhD2Vxh8DrQUbQlaGbljLJTNNc453E2Axp+Mxm+4OVAQ==" - ); - - assert_eq!( - private_key.sign(&CoreJwsSigningAlgorithm::HmacSha256, message), - Err(SigningError::UnsupportedAlg("HS256".to_string())), - ); - - assert_eq!( - private_key.sign(&CoreJwsSigningAlgorithm::None, message), - Err(SigningError::UnsupportedAlg("none".to_string())), - ); - } - - #[test] - fn test_rsa_signing() { - let private_key = CoreRsaPrivateSigningKey::from_pem_internal( - TEST_RSA_KEY, - // Constant salt used for PSS test vectors below. - Box::new(TestRng(StepRng::new(127, 0))), - Some(JsonWebKeyId::new("test_key".to_string())), - ) - .unwrap(); - - let public_key_jwk = private_key.as_verification_key(); - let public_key_jwk_str = serde_json::to_string(&public_key_jwk).unwrap(); - assert_eq!( - "{\ - \"kty\":\"RSA\",\ - \"use\":\"sig\",\ - \"kid\":\"test_key\",\ - \"n\":\"sRMj0YYjy7du6v1gWyKSTJx3YjBzZTG0XotRP0IaObw0k-6830dXadjL5jVhSWNdcg9OyMyTGWfdNq\ - fdrS6ppBqlQNgjZJdloIqL9zOLBZrDm7G4-qN4KeZ4_5TyEilq2zOHHGFEzXpOq_UxqVnm3J4fhjqCNaS2nKd7\ - HVVXGBQQ-4-FdVT-MyJXemw5maz2F_h324TQi6XoUPEwUddxBwLQFSOlzWnHYMc4_lcyZJ8MpTXCMPe_YJFNtb\ - 9CaikKUdf8x4mzwH7usSf8s2d6R4dQITzKrjrEJ0u3w3eGkBBapoMVFBGPjP3Haz5FsVtHc5VEN3FZVIDF6Hrb\ - JH1C4Q\",\ - \"e\":\"AQAB\"\ - }", - public_key_jwk_str - ); - - let message = "hello RSA".as_ref(); - expect_rsa_sig( - &private_key, - message, - &CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256, - "KBvV+F7Xofg4i4qUA0JEqfhQQdjZ7ralUYTPKRIitaKL4a6ni+abagsHs5V63+bmQF5t6DM4aRH2ZC943Tonkr\ - AUY1mpaqic2vqtrtWk3cyrcHtkPCLNKzFf/6xvHPjeKH1Bu/qTQ0mn+hN6taOgw3ORbm6P9MkelX1RVEia98uwB\ - Zn2BxKeqNYm11vqKDyS5ZFzHwpPrC4rri/uTIcXsQEXB+Lbb+naDpQn8qJqP+S+uM2LGWIXp5ExAJ55A111nIqE\ - Ap0aKwf2U8Q81DWI8lbHbL1dd7FRDtZKm+ainO5ck4L/axtH7C4GIZd+TiXL3iYpiWmNkqlwv9WsNPe8Rg==", - ); - - expect_rsa_sig( - &private_key, - message, - &CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha384, - "YsyhW9DkIoNJPqTNY7pidJi5wWtQGr4xety+2Zt1DKNMG0ENFkxCGPLCYcL9vGSS9kfkrPtQ3Eve7g9DKfg1fg\ - 071SXJHxAlK0iC8mVYfQrxxyFlQDIPEhvCJx6VkWVm2jJhN+vByGRJLTo2n3gtYtMREfz+c5xnXWeIy+JQ0LXOy\ - SyOZl0qHxn1VteczH79uCK0Vv8ZH3IfbQMU+2HjbVeUYRzCoAhlT4V2GY4U1pCrZBlfEyhr0ncHz90FRvvhLT3y\ - SlHa7yY7CRJ+z1CLBOzBiH1Eko4tIJKy/qO9M6EGeFtXhqd4td5g2oY/mUZYjHYjgcDO+wAXrZ9lP/ZVUg==", - ); - - expect_rsa_sig( - &private_key, - message, - &CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha512, - "GXN3rmZhlJw46FHoqiuELcIi6iUr3cVC0HZpjBJhrTVfta/8a4PpzmLbQxjdb1cdU/56XDXkqDSNvzRn4PwAOL\ - 460n0Eg8d8mxwPRHQuyypze9240FEw3lyjp5uPJHn5PrmeelczJ1Xseinmp+JwpKHiHhmxp7FjgJc/o3J/hlz0n\ - G1cgTndtrlp5JPJOJNt4XfgIgqoobH5Wk3ML35C50mD93Ld6V6nn6rK72wgecK1SDXeF4ztQUAjg4VTEojLm5VF\ - kfR7kXV0dIbAvZXDa1uuIOlVDIRfF93rxme1Ze46Dywan+zfsGCcpFfFAsnGLsgNDmATB8IS1lTf1SGMoA==", - ); - - expect_rsa_sig( - &private_key, - message, - &CoreJwsSigningAlgorithm::RsaSsaPssSha256, - "sPwTRDAOGOZr0ofAL/lMSXaR5L5aoFANLei4bqAQFHiFxDDrWZZ28MZiunwCGakOM8Itwas+sIX6vo3VTy9d5Bh\ - Cotb/gY5DhMX/iZJubfS8U1fB8rFWXDpREeAegGvXPjBd0A4M6z9it9Tss43dYqO12mYEpz6rFvbHJKSR9Hnmak4\ - F8TcuZswYtjhHhiib05PGjeJTo/5F15WtR7RYK4slYGOU4mf5wzZSpfgifJ2XjyQQe6oCFvVXftVtiQhEljHiEGv\ - GZH5y4FA7h06PqkHMwQEnRsBhVm4pUqRi641tglZ3HZcoxYUE8AdMX7ltJzy/vpOew2bVGoF3mUnlZw==", - ); - - expect_rsa_sig( - &private_key, - message, - &CoreJwsSigningAlgorithm::RsaSsaPssSha384, - "m0EsYFpNa5YjvEYPcfUpXPMqAWmWmkTaQiyK2HZ9Ejt+cBO/S5jcVqd0y2rCDMV1DpSb/JI8uhwp+qYm/2YKpIa\ - zp+u9PpjlL3jvYn19WbwJTCztJ9XSjcEbtkf1fS/d/BU7FgQzYIE0k++QqHjgzkTI5+2XLYX2WP5dc0r67Or5xaF\ - 0ixL1edpEDKfgF3jiKuLmR2dv4MWHPLYRb1I0zm5C/E7g57DfJT4uNzmLX9gTGr4xe6CxVEYy4eFdE+q1O5J6RXd\ - FZnl4qFK9+x1pk0dhWkpIEaKhweI7YP79iFPnAiUnRM6BsdY+puwjGlaaGtYVFcuPO4uXEXtB1AnsEQ==", - ); - - expect_rsa_sig( - &private_key, - message, - &CoreJwsSigningAlgorithm::RsaSsaPssSha512, - "N8BMNKm1dMOm0/BLzAjtnzvRlVtzgO8fUeKnfvUtK8XWeII5nk74hE3AoAJNPLuTninYtfaF68Supu5CsCJAqO9\ - 1JnVvG8P1DX19iCTzJ83o69+kluBIz7x0l796RysDhqcjybGC+fj0M5MpgkNNcKlNwRixus3sfgCgh3mEB+E1Q11\ - hQKjCTdyOcqzGoima+Na17VBWzU3XXLvB328UfkV2nswBlLUsZMT3I4n/aIziENQCLVPlLdX8z+1NjHSAgd9rZMf\ - gfy0eMsjNuQpqPzVW3mbxlCMMVWpd8LKBprfa291xEk1wwvJCuU9EK7QmQPmYa1HAh+E+R2Dw3ibHdA==", - ); - - assert_eq!( - private_key.sign(&CoreJwsSigningAlgorithm::HmacSha256, message), - Err(SigningError::UnsupportedAlg("HS256".to_string())), - ); - assert_eq!( - private_key.sign(&CoreJwsSigningAlgorithm::HmacSha384, message), - Err(SigningError::UnsupportedAlg("HS384".to_string())), - ); - assert_eq!( - private_key.sign(&CoreJwsSigningAlgorithm::HmacSha512, message), - Err(SigningError::UnsupportedAlg("HS512".to_string())), - ); - assert_eq!( - private_key.sign(&CoreJwsSigningAlgorithm::EcdsaP256Sha256, message), - Err(SigningError::UnsupportedAlg("ES256".to_string())), - ); - assert_eq!( - private_key.sign(&CoreJwsSigningAlgorithm::EcdsaP384Sha384, message), - Err(SigningError::UnsupportedAlg("ES384".to_string())), - ); - assert_eq!( - private_key.sign(&CoreJwsSigningAlgorithm::EcdsaP521Sha512, message), - Err(SigningError::UnsupportedAlg("ES512".to_string())), - ); - assert_eq!( - private_key.sign(&CoreJwsSigningAlgorithm::None, message), - Err(SigningError::UnsupportedAlg("none".to_string())), - ); - } - - #[test] - fn test_rsa_pss_signing() { - let private_key = CoreRsaPrivateSigningKey::from_pem(TEST_RSA_KEY, None).unwrap(); - - const MESSAGE: &str = "This is a probabilistic signature scheme"; - let sig1 = private_key - .sign( - &CoreJwsSigningAlgorithm::RsaSsaPssSha256, - MESSAGE.as_bytes(), - ) - .unwrap(); - let sig2 = private_key - .sign( - &CoreJwsSigningAlgorithm::RsaSsaPssSha256, - MESSAGE.as_bytes(), - ) - .unwrap(); - - assert_ne!(sig1, sig2); - } - - // Tests that JsonWebKeySet ignores unsupported keys during deserialization so that clients can - // use providers that include unsupported keys as long as they only use supported ones to sign - // payloads. - #[test] - fn test_jwks_unsupported_key() { - let jwks_json = "{ - \"keys\": [ - { - \"kty\": \"RSA\", - \"use\": \"sig\", - \"kid\": \"2011-04-29\", - \"n\": \"0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhD\ - R1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6C\ - f0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1\ - n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1\ - jF44-csFCur-kEgU8awapJzKnqDKgw\", - \"e\": \"AQAB\" - }, - { - \"kty\": \"MAGIC\", - \"use\": \"sig\", - \"kid\": \"2040-01-01\", - \"magic\": \"magic\" - }, - { - \"kty\": \"EC\", - \"use\": \"sig\", - \"kid\": \"2011-05-01\", - \"crv\": \"P-256\", - \"x\": \"kXCGZIr3oI6sKbnT6rRsIdxFXw3_VbLk_cveajgqXk8\", - \"y\": \"StDvKIgXqAxJ6DuebREh-1vgvZRW3dfrOxSIKzBtRI0\" - } - ] - }"; - let jwks = serde_json::from_str::(jwks_json) - .expect("deserialization should succeed"); - - assert_eq!(jwks.keys().len(), 2); - - assert_eq!(jwks.keys()[0].kty, CoreJsonWebKeyType::RSA); - assert_eq!(jwks.keys()[0].use_, Some(CoreJsonWebKeyUse::Signature)); - assert_eq!( - jwks.keys()[0].kid, - Some(JsonWebKeyId::new("2011-04-29".to_string())) - ); - - assert_eq!(jwks.keys()[1].kty, CoreJsonWebKeyType::EllipticCurve); - assert_eq!(jwks.keys()[1].use_, Some(CoreJsonWebKeyUse::Signature)); - assert_eq!( - jwks.keys()[1].kid, - Some(JsonWebKeyId::new("2011-05-01".to_string())) - ); - assert_eq!(jwks.keys()[1].crv, Some(CoreJsonCurveType::P256)); - } - - // Tests that JsonWebKeySet ignores keys with unsupported algorithms - #[cfg(feature = "jwk-alg")] - #[test] - fn test_jwks_unsupported_alg() { - let jwks_json = "{ - \"keys\": [ - { - \"kty\": \"EC\", - \"alg\": \"MAGIC\", - \"crv\": \"P-256\", - \"x\": \"kXCGZIr3oI6sKbnT6rRsIdxFXw3_VbLk_cveajgqXk8\", - \"y\": \"StDvKIgXqAxJ6DuebREh-1vgvZRW3dfrOxSIKzBtRI0\" - }, - { - \"kty\": \"EC\", - \"alg\": \"ES256\", - \"kid\": \"2011-05-01\", - \"crv\": \"P-256\", - \"x\": \"kXCGZIr3oI6sKbnT6rRsIdxFXw3_VbLk_cveajgqXk8\", - \"y\": \"StDvKIgXqAxJ6DuebREh-1vgvZRW3dfrOxSIKzBtRI0\" - } - ] - }"; - let jwks = serde_json::from_str::(jwks_json) - .expect("deserialization should succeed"); - assert_eq!(jwks.keys().len(), 1); - let key = &jwks.keys()[0]; - assert_eq!(&key.kid, &Some(JsonWebKeyId::new("2011-05-01".to_string()))); - } - - // Test filtering keys by algorithm - #[cfg(feature = "jwk-alg")] - #[test] - fn test_jwks_same_kid_different_alg() { - let jwks_json = "{ - \"keys\": [ - { - \"kty\": \"RSA\", - \"use\": \"sig\", - \"kid\": \"2011-04-29\", - \"alg\": \"PS256\", - \"n\": \"0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhD\ - R1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6C\ - f0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1\ - n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1\ - jF44-csFCur-kEgU8awapJzKnqDKgw\", - \"e\": \"AQAB\" - }, - { - \"kty\": \"RSA\", - \"use\": \"sig\", - \"kid\": \"2011-04-29\", - \"alg\": \"PS384\", - \"n\": \"0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhD\ - R1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6C\ - f0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1\ - n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1\ - jF44-csFCur-kEgU8awapJzKnqDKgw\", - \"e\": \"AQAB\" - } - ] - }"; - let jwks = serde_json::from_str::(jwks_json) - .expect("deserialization should succeed"); - assert_eq!(jwks.keys().len(), 2); - - { - let keys = jwks.filter_keys( - &Some(JsonWebKeyId::new("2011-04-29".to_string())), - &CoreJwsSigningAlgorithm::RsaSsaPssSha384, - ); - assert_eq!(keys.len(), 1); - assert_eq!( - keys[0].alg, - Some(crate::jwt::JsonWebTokenAlgorithm::Signature( - CoreJwsSigningAlgorithm::RsaSsaPssSha384, - std::marker::PhantomData - )) - ); - } - - { - let keys = jwks.filter_keys( - &Some(JsonWebKeyId::new("2011-04-29".to_string())), - &CoreJwsSigningAlgorithm::RsaSsaPssSha512, - ); - assert_eq!(keys.len(), 0); - } - } -} diff --git a/src/core/jwk/mod.rs b/src/core/jwk/mod.rs new file mode 100644 index 0000000..53f93f4 --- /dev/null +++ b/src/core/jwk/mod.rs @@ -0,0 +1,784 @@ +use crate::core::{crypto, CoreJwsSigningAlgorithm}; +use crate::helpers::{deserialize_option_or_none, Base64UrlEncodedBytes}; +use crate::types::jwk::JsonCurveType; +use crate::types::jwks::check_key_compatibility; +#[cfg(feature = "jwk-alg")] +use crate::{core::CoreJweContentEncryptionAlgorithm, JsonWebKeyAlgorithm, JsonWebTokenAlgorithm}; +use crate::{ + JsonWebKey, JsonWebKeyId, JsonWebKeyType, JsonWebKeyUse, PrivateSigningKey, + SignatureVerificationError, SigningError, +}; + +use ed25519_dalek::pkcs8::DecodePrivateKey; +use ed25519_dalek::Signer; +use rsa::pkcs1::DecodeRsaPrivateKey; +use serde::{Deserialize, Serialize}; +use sha2::Digest; + +#[cfg(test)] +mod tests; + +// Other than the 'kty' (key type) parameter, which must be present in all JWKs, Section 4 of RFC +// 7517 states that "member names used for representing key parameters for different keys types +// need not be distinct." Therefore, it's possible that future or non-standard key types will supply +// some of the following parameters but with different types, causing deserialization to fail. To +// support such key types, we'll need to define a new impl for JsonWebKey. Deserializing the new +// impl would probably need to involve first deserializing the raw values to access the 'kty' +// parameter, and then deserializing the fields and types appropriate for that key type. +/// Public or symmetric key expressed as a JSON Web Key. +#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)] +pub struct CoreJsonWebKey { + pub(crate) kty: CoreJsonWebKeyType, + #[serde(rename = "use", skip_serializing_if = "Option::is_none")] + pub(crate) use_: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) kid: Option, + + /// The algorithm intended to be used with this key (see + /// [RFC 7517](https://www.rfc-editor.org/rfc/rfc7517#section-4.4)). + /// + /// It can either be an algorithm intended for use with JWS or JWE, or something different. + #[cfg(feature = "jwk-alg")] + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) alg: Option< + JsonWebTokenAlgorithm< + CoreJweContentEncryptionAlgorithm, + CoreJwsSigningAlgorithm, + CoreJsonWebKeyType, + >, + >, + + // From RFC 7517, Section 4: "Additional members can be present in the JWK; if not understood + // by implementations encountering them, they MUST be ignored. Member names used for + // representing key parameters for different keys types need not be distinct." + // Hence, we set fields we fail to deserialize (understand) as None. + #[serde( + default, + deserialize_with = "deserialize_option_or_none", + skip_serializing_if = "Option::is_none" + )] + pub(crate) n: Option, + #[serde( + default, + deserialize_with = "deserialize_option_or_none", + skip_serializing_if = "Option::is_none" + )] + pub(crate) e: Option, + + //Elliptic Curve + #[serde( + default, + deserialize_with = "deserialize_option_or_none", + skip_serializing_if = "Option::is_none" + )] + pub(crate) crv: Option, + #[serde( + default, + deserialize_with = "deserialize_option_or_none", + skip_serializing_if = "Option::is_none" + )] + pub(crate) x: Option, + #[serde( + default, + deserialize_with = "deserialize_option_or_none", + skip_serializing_if = "Option::is_none" + )] + pub(crate) y: Option, + + #[serde( + default, + deserialize_with = "deserialize_option_or_none", + skip_serializing_if = "Option::is_none" + )] + pub(crate) d: Option, + + // Used for symmetric keys, which we only generate internally from the client secret; these + // are never part of the JWK set. + #[serde( + default, + deserialize_with = "deserialize_option_or_none", + skip_serializing_if = "Option::is_none" + )] + pub(crate) k: Option, +} +impl CoreJsonWebKey { + /// Instantiate a new RSA public key from the raw modulus (`n`) and public exponent (`e`), + /// along with an optional (but recommended) key ID. + /// + /// The key ID is used for matching signed JSON Web Tokens with the keys used for verifying + /// their signatures. + pub fn new_rsa(n: Vec, e: Vec, kid: Option) -> Self { + Self { + kty: CoreJsonWebKeyType::RSA, + use_: Some(CoreJsonWebKeyUse::Signature), + kid, + n: Some(Base64UrlEncodedBytes::new(n)), + e: Some(Base64UrlEncodedBytes::new(e)), + k: None, + crv: None, + x: None, + y: None, + d: None, + #[cfg(feature = "jwk-alg")] + alg: None, + } + } + /// Instantiate a new EC public key from the raw x (`x`) and y(`y`) part of the curve, + /// along with an optional (but recommended) key ID. + /// + /// The key ID is used for matching signed JSON Web Tokens with the keys used for verifying + /// their signatures. + pub fn new_ec( + x: Vec, + y: Vec, + crv: CoreJsonCurveType, + kid: Option, + ) -> Self { + Self { + kty: CoreJsonWebKeyType::EllipticCurve, + use_: Some(CoreJsonWebKeyUse::Signature), + kid, + n: None, + e: None, + k: None, + crv: Some(crv), + x: Some(Base64UrlEncodedBytes::new(x)), + y: Some(Base64UrlEncodedBytes::new(y)), + d: None, + #[cfg(feature = "jwk-alg")] + alg: None, + } + } + + /// Instantiate a new Octet Key-Pair public key from the raw x (`x`) part of the curve, + /// along with an optional (but recommended) key ID. + /// + /// The key ID is used for matching signed JSON Web Tokens with the keys used for verifying + /// their signatures. + pub fn new_okp(x: Vec, crv: CoreJsonCurveType, kid: Option) -> Self { + Self { + kty: CoreJsonWebKeyType::OctetKeyPair, + use_: Some(CoreJsonWebKeyUse::Signature), + kid, + n: None, + e: None, + k: None, + crv: Some(crv), + x: Some(Base64UrlEncodedBytes::new(x)), + y: None, + d: None, + #[cfg(feature = "jwk-alg")] + alg: None, + } + } +} + +impl JsonWebKey for CoreJsonWebKey { + fn key_id(&self) -> Option<&JsonWebKeyId> { + self.kid.as_ref() + } + fn key_type(&self) -> &CoreJsonWebKeyType { + &self.kty + } + fn key_use(&self) -> Option<&CoreJsonWebKeyUse> { + self.use_.as_ref() + } + + fn new_symmetric(key: Vec) -> Self { + Self { + kty: CoreJsonWebKeyType::Symmetric, + use_: None, + kid: None, + n: None, + e: None, + k: Some(Base64UrlEncodedBytes::new(key)), + crv: None, + x: None, + y: None, + d: None, + #[cfg(feature = "jwk-alg")] + alg: None, + } + } + + fn verify_signature( + &self, + signature_alg: &CoreJwsSigningAlgorithm, + message: &[u8], + signature: &[u8], + ) -> Result<(), SignatureVerificationError> { + use hmac::Mac; + + check_key_compatibility(self, signature_alg) + .map_err(|e| SignatureVerificationError::InvalidKey(e.to_owned()))?; + + match *signature_alg { + CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256 => { + let message = { + let mut hasher = sha2::Sha256::new(); + hasher.update(message); + &hasher.finalize() + }; + crypto::verify_rsa_signature( + self, + rsa::Pkcs1v15Sign::new::(), + message, + signature, + ) + } + CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha384 => { + let message = { + let mut hasher = sha2::Sha384::new(); + hasher.update(message); + &hasher.finalize() + }; + crypto::verify_rsa_signature( + self, + rsa::Pkcs1v15Sign::new::(), + message, + signature, + ) + } + CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha512 => { + let message = { + let mut hasher = sha2::Sha512::new(); + hasher.update(message); + &hasher.finalize() + }; + crypto::verify_rsa_signature( + self, + rsa::Pkcs1v15Sign::new::(), + message, + signature, + ) + } + CoreJwsSigningAlgorithm::RsaSsaPssSha256 => { + let message = { + let mut hasher = sha2::Sha256::new(); + hasher.update(message); + &hasher.finalize() + }; + crypto::verify_rsa_signature( + self, + rsa::Pss::new::(), + message, + signature, + ) + } + CoreJwsSigningAlgorithm::RsaSsaPssSha384 => { + let message = { + let mut hasher = sha2::Sha384::new(); + hasher.update(message); + &hasher.finalize() + }; + crypto::verify_rsa_signature( + self, + rsa::Pss::new::(), + message, + signature, + ) + } + CoreJwsSigningAlgorithm::RsaSsaPssSha512 => { + let message = { + let mut hasher = sha2::Sha512::new(); + hasher.update(message); + &hasher.finalize() + }; + crypto::verify_rsa_signature( + self, + rsa::Pss::new::(), + message, + signature, + ) + } + CoreJwsSigningAlgorithm::HmacSha256 => { + let mut mac = hmac::Hmac::::new_from_slice( + self.k.as_ref().ok_or_else(|| { + SignatureVerificationError::InvalidKey( + "Symmetric key `k` is missing".to_string(), + ) + })?, + ) + .map_err(|e| { + SignatureVerificationError::Other(format!("Could not create key: {}", e)) + })?; + mac.update(message); + mac.verify(signature.into()) + .map_err(|_| SignatureVerificationError::CryptoError("bad HMAC".to_string())) + } + CoreJwsSigningAlgorithm::HmacSha384 => { + let mut mac = hmac::Hmac::::new_from_slice( + self.k.as_ref().ok_or_else(|| { + SignatureVerificationError::InvalidKey( + "Symmetric key `k` is missing".to_string(), + ) + })?, + ) + .map_err(|e| { + SignatureVerificationError::Other(format!("Could not create key: {}", e)) + })?; + mac.update(message); + mac.verify(signature.into()) + .map_err(|_| SignatureVerificationError::CryptoError("bad HMAC".to_string())) + } + CoreJwsSigningAlgorithm::HmacSha512 => { + let mut mac = hmac::Hmac::::new_from_slice( + self.k.as_ref().ok_or_else(|| { + SignatureVerificationError::InvalidKey( + "Symmetric key `k` is missing".to_string(), + ) + })?, + ) + .map_err(|e| { + SignatureVerificationError::Other(format!("Could not create key: {}", e)) + })?; + mac.update(message); + mac.verify(signature.into()) + .map_err(|_| SignatureVerificationError::CryptoError("bad HMAC".to_string())) + } + CoreJwsSigningAlgorithm::EcdsaP256Sha256 => { + if matches!(self.crv, Some(CoreJsonCurveType::P256)) { + crypto::verify_ec_signature(self, message, signature) + } else { + Err(SignatureVerificationError::InvalidKey( + "Key uses different CRV than JWT".to_string(), + )) + } + } + CoreJwsSigningAlgorithm::EcdsaP384Sha384 => { + if matches!(self.crv, Some(CoreJsonCurveType::P384)) { + crypto::verify_ec_signature(self, message, signature) + } else { + Err(SignatureVerificationError::InvalidKey( + "Key uses different CRV than JWT".to_string(), + )) + } + } + CoreJwsSigningAlgorithm::EdDsaEd25519 => { + if matches!(self.crv, Some(CoreJsonCurveType::Ed25519)) { + crypto::verify_ed_signature(self, message, signature) + } else { + Err(SignatureVerificationError::InvalidKey( + "Key uses different CRV than JWT".to_string(), + )) + } + } + ref other => Err(SignatureVerificationError::UnsupportedAlg( + serde_plain::to_string(other).unwrap_or_else(|err| { + panic!( + "signature alg {:?} failed to serialize to a string: {}", + other, err + ) + }), + )), + } + } + + #[cfg(feature = "jwk-alg")] + fn signing_alg(&self) -> JsonWebKeyAlgorithm<&CoreJwsSigningAlgorithm> { + match self.alg { + None => JsonWebKeyAlgorithm::Unspecified, + Some(JsonWebTokenAlgorithm::Signature(ref alg, _)) => { + JsonWebKeyAlgorithm::Algorithm(alg) + } + Some(_) => JsonWebKeyAlgorithm::Unsupported, + } + } +} + +/// HMAC secret key. +/// +/// This key can be used for signing messages, or converted to a `CoreJsonWebKey` for verifying +/// them. +#[derive(Clone)] +pub struct CoreHmacKey { + secret: Vec, +} +impl CoreHmacKey { + /// Instantiate a new key from the specified secret bytes. + pub fn new(secret: T) -> Self + where + T: Into>, + { + Self { + secret: secret.into(), + } + } +} +impl + PrivateSigningKey< + CoreJwsSigningAlgorithm, + CoreJsonWebKeyType, + CoreJsonWebKeyUse, + CoreJsonWebKey, + > for CoreHmacKey +{ + fn sign( + &self, + signature_alg: &CoreJwsSigningAlgorithm, + message: &[u8], + ) -> Result, SigningError> { + use hmac::Mac; + match *signature_alg { + CoreJwsSigningAlgorithm::HmacSha256 => { + let mut mac = hmac::Hmac::::new_from_slice(&self.secret) + .map_err(|e| SigningError::Other(format!("Could not create key: {}", e)))?; + mac.update(message); + let result = mac.finalize(); + Ok(result.into_bytes().as_slice().to_vec()) + } + CoreJwsSigningAlgorithm::HmacSha384 => { + let mut mac = hmac::Hmac::::new_from_slice(&self.secret) + .map_err(|e| SigningError::Other(format!("Could not create key: {}", e)))?; + mac.update(message); + let result = mac.finalize(); + Ok(result.into_bytes().as_slice().to_vec()) + } + CoreJwsSigningAlgorithm::HmacSha512 => { + let mut mac = hmac::Hmac::::new_from_slice(&self.secret) + .map_err(|e| SigningError::Other(format!("Could not create key: {}", e)))?; + mac.update(message); + let result = mac.finalize(); + Ok(result.into_bytes().as_slice().to_vec()) + } + ref other => Err(SigningError::UnsupportedAlg( + serde_plain::to_string(other).unwrap_or_else(|err| { + panic!( + "signature alg {:?} failed to serialize to a string: {}", + other, err + ) + }), + )), + } + } + + fn as_verification_key(&self) -> CoreJsonWebKey { + CoreJsonWebKey::new_symmetric(self.secret.clone()) + } +} + +enum EdDsaSigningKey { + Ed25519(ed25519_dalek::SigningKey), +} + +impl EdDsaSigningKey { + fn from_ed25519_pem(pem: &str) -> Result { + Ok(Self::Ed25519( + ed25519_dalek::SigningKey::from_pkcs8_pem(pem).map_err(|err| err.to_string())?, + )) + } + + fn sign(&self, message: &[u8]) -> Vec { + match self { + Self::Ed25519(key) => { + let signature = key.sign(message); + + signature.to_vec() + } + } + } +} + +/// EdDSA Private Key. +/// +/// This key can be used for signing messages, or converted to a `CoreJsonWebKey` for verifying +/// them. +pub struct CoreEdDsaPrivateSigningKey { + kid: Option, + key_pair: EdDsaSigningKey, +} +impl CoreEdDsaPrivateSigningKey { + /// Converts an EdDSA private key (in PEM format) to a JWK representing its public key. + pub fn from_ed25519_pem(pem: &str, kid: Option) -> Result { + Ok(Self { + kid, + key_pair: EdDsaSigningKey::from_ed25519_pem(pem)?, + }) + } +} +impl + PrivateSigningKey< + CoreJwsSigningAlgorithm, + CoreJsonWebKeyType, + CoreJsonWebKeyUse, + CoreJsonWebKey, + > for CoreEdDsaPrivateSigningKey +{ + fn sign( + &self, + signature_alg: &CoreJwsSigningAlgorithm, + message: &[u8], + ) -> Result, SigningError> { + match *signature_alg { + CoreJwsSigningAlgorithm::EdDsaEd25519 => Ok(self.key_pair.sign(message)), + ref other => Err(SigningError::UnsupportedAlg( + serde_plain::to_string(other).unwrap_or_else(|err| { + panic!( + "signature alg {:?} failed to serialize to a string: {}", + other, err + ) + }), + )), + } + } + + fn as_verification_key(&self) -> CoreJsonWebKey { + match &self.key_pair { + EdDsaSigningKey::Ed25519(key) => CoreJsonWebKey { + kty: CoreJsonWebKeyType::OctetKeyPair, + use_: Some(CoreJsonWebKeyUse::Signature), + kid: self.kid.clone(), + n: None, + e: None, + crv: Some(CoreJsonCurveType::Ed25519), + x: Some(Base64UrlEncodedBytes::new( + key.verifying_key().as_bytes().to_vec(), + )), + y: None, + d: None, + k: None, + #[cfg(feature = "jwk-alg")] + alg: None, + }, + } + } +} + +/// Trait used to allow testing with an alternative RNG. +/// Clone is necessary to get a mutable version of the RNG. +pub(crate) trait RngClone: dyn_clone::DynClone + rand::RngCore + rand::CryptoRng {} +dyn_clone::clone_trait_object!(RngClone); +impl RngClone for T where T: rand::RngCore + rand::CryptoRng + Clone {} + +/// RSA private key. +/// +/// This key can be used for signing messages, or converted to a `CoreJsonWebKey` for verifying +/// them. +pub struct CoreRsaPrivateSigningKey { + key_pair: rsa::RsaPrivateKey, + rng: Box, + kid: Option, +} +impl CoreRsaPrivateSigningKey { + /// Converts an RSA private key (in PEM format) to a JWK representing its public key. + pub fn from_pem(pem: &str, kid: Option) -> Result { + Self::from_pem_internal(pem, Box::new(rand::rngs::OsRng), kid) + } + + pub(crate) fn from_pem_internal( + pem: &str, + rng: Box, + kid: Option, + ) -> Result { + let key_pair = rsa::RsaPrivateKey::from_pkcs1_pem(pem).map_err(|err| err.to_string())?; + Ok(Self { key_pair, rng, kid }) + } +} +impl + PrivateSigningKey< + CoreJwsSigningAlgorithm, + CoreJsonWebKeyType, + CoreJsonWebKeyUse, + CoreJsonWebKey, + > for CoreRsaPrivateSigningKey +{ + fn sign( + &self, + signature_alg: &CoreJwsSigningAlgorithm, + msg: &[u8], + ) -> Result, SigningError> { + match *signature_alg { + CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256 => { + let mut hasher = sha2::Sha256::new(); + hasher.update(msg); + let hash = hasher.finalize().to_vec(); + + self.key_pair + .sign_with_rng( + &mut dyn_clone::clone_box(&self.rng), + rsa::Pkcs1v15Sign::new::(), + &hash, + ) + .map_err(|_| SigningError::CryptoError) + } + CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha384 => { + let mut hasher = sha2::Sha384::new(); + hasher.update(msg); + let hash = hasher.finalize().to_vec(); + + self.key_pair + .sign_with_rng( + &mut dyn_clone::clone_box(&self.rng), + rsa::Pkcs1v15Sign::new::(), + &hash, + ) + .map_err(|_| SigningError::CryptoError) + } + CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha512 => { + let mut hasher = sha2::Sha512::new(); + hasher.update(msg); + let hash = hasher.finalize().to_vec(); + + self.key_pair + .sign_with_rng( + &mut dyn_clone::clone_box(&self.rng), + rsa::Pkcs1v15Sign::new::(), + &hash, + ) + .map_err(|_| SigningError::CryptoError) + } + CoreJwsSigningAlgorithm::RsaSsaPssSha256 => { + let mut hasher = sha2::Sha256::new(); + hasher.update(msg); + let hash = hasher.finalize().to_vec(); + + self.key_pair + .sign_with_rng( + &mut dyn_clone::clone_box(&self.rng), + rsa::Pss::new_with_salt::(hash.len()), + &hash, + ) + .map_err(|_| SigningError::CryptoError) + } + CoreJwsSigningAlgorithm::RsaSsaPssSha384 => { + let mut hasher = sha2::Sha384::new(); + hasher.update(msg); + let hash = hasher.finalize().to_vec(); + + self.key_pair + .sign_with_rng( + &mut dyn_clone::clone_box(&self.rng), + rsa::Pss::new_with_salt::(hash.len()), + &hash, + ) + .map_err(|_| SigningError::CryptoError) + } + CoreJwsSigningAlgorithm::RsaSsaPssSha512 => { + let mut hasher = sha2::Sha512::new(); + hasher.update(msg); + let hash = hasher.finalize().to_vec(); + + self.key_pair + .sign_with_rng( + &mut dyn_clone::clone_box(&self.rng), + rsa::Pss::new_with_salt::(hash.len()), + &hash, + ) + .map_err(|_| SigningError::CryptoError) + } + ref other => Err(SigningError::UnsupportedAlg( + serde_plain::to_string(other).unwrap_or_else(|err| { + panic!( + "signature alg {:?} failed to serialize to a string: {}", + other, err + ) + }), + )), + } + } + + fn as_verification_key(&self) -> CoreJsonWebKey { + use rsa::traits::PublicKeyParts; + + let public_key = self.key_pair.to_public_key(); + CoreJsonWebKey { + kty: CoreJsonWebKeyType::RSA, + use_: Some(CoreJsonWebKeyUse::Signature), + kid: self.kid.clone(), + n: Some(Base64UrlEncodedBytes::new(public_key.n().to_bytes_be())), + e: Some(Base64UrlEncodedBytes::new(public_key.e().to_bytes_be())), + k: None, + crv: None, + x: None, + y: None, + d: None, + #[cfg(feature = "jwk-alg")] + alg: None, + } + } +} + +/// Type of JSON Web Key. +#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)] +#[non_exhaustive] +pub enum CoreJsonWebKeyType { + /// Elliptic Curve Cryptography (ECC) key. + /// + /// ECC algorithms such as ECDSA are currently unsupported. + #[serde(rename = "EC")] + EllipticCurve, + /// RSA key. + #[serde(rename = "RSA")] + RSA, + /// EdDSA key. + #[serde(rename = "OKP")] + OctetKeyPair, + /// Symmetric key. + #[serde(rename = "oct")] + Symmetric, +} +impl JsonWebKeyType for CoreJsonWebKeyType {} + +/// Type of EC-Curve +#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)] +#[non_exhaustive] +pub enum CoreJsonCurveType { + /// P-256 Curve + #[serde(rename = "P-256")] + P256, + /// P-384 Curve + #[serde(rename = "P-384")] + P384, + /// P-521 Curve (currently not supported) + #[serde(rename = "P-521")] + P521, + /// Ed25519 Curve + #[serde(rename = "Ed25519")] + Ed25519, +} +impl JsonCurveType for CoreJsonWebKeyType {} + +/// Usage restriction for a JSON Web key. +#[derive(Clone, Debug, PartialEq, Eq)] +#[non_exhaustive] +pub enum CoreJsonWebKeyUse { + /// Key may be used for digital signatures. + Signature, + + /// Key may be used for encryption. + Encryption, + + /// Fallback case for other key uses not understood by this library. + Other(String), +} +impl CoreJsonWebKeyUse { + fn from_str(s: &str) -> Self { + match s { + "sig" => Self::Signature, + "enc" => Self::Encryption, + other => Self::Other(other.to_string()), + } + } +} +impl AsRef for CoreJsonWebKeyUse { + fn as_ref(&self) -> &str { + match self { + CoreJsonWebKeyUse::Signature => "sig", + CoreJsonWebKeyUse::Encryption => "enc", + CoreJsonWebKeyUse::Other(other) => other.as_str(), + } + } +} +impl JsonWebKeyUse for CoreJsonWebKeyUse { + fn allows_signature(&self) -> bool { + matches!(*self, CoreJsonWebKeyUse::Signature) + } + fn allows_encryption(&self) -> bool { + matches!(*self, CoreJsonWebKeyUse::Encryption) + } +} +// FIXME: Once https://github.com/serde-rs/serde/issues/912 is resolved, use #[serde(other)] instead +// of custom serializer/deserializers. Right now this isn't possible because serde(other) only +// supports unit variants. +deserialize_from_str!(CoreJsonWebKeyUse); +serialize_as_str!(CoreJsonWebKeyUse); diff --git a/src/core/jwk/tests.rs b/src/core/jwk/tests.rs new file mode 100644 index 0000000..fe3f898 --- /dev/null +++ b/src/core/jwk/tests.rs @@ -0,0 +1,1073 @@ +use crate::core::jwk::CoreJsonCurveType; +use crate::core::{ + CoreEdDsaPrivateSigningKey, CoreHmacKey, CoreJsonWebKey, CoreJsonWebKeySet, CoreJsonWebKeyType, + CoreJsonWebKeyUse, CoreJwsSigningAlgorithm, CoreRsaPrivateSigningKey, +}; +use crate::helpers::Base64UrlEncodedBytes; +use crate::jwt::tests::{ + TEST_EC_PUB_KEY_P256, TEST_EC_PUB_KEY_P384, TEST_ED_PUB_KEY_ED25519, TEST_RSA_PUB_KEY, +}; +use crate::verification::SignatureVerificationError; +#[cfg(feature = "jwk-alg")] +use crate::{core::CoreJweContentEncryptionAlgorithm, JsonWebTokenAlgorithm}; +use crate::{JsonWebKey, JsonWebKeyId, PrivateSigningKey, SigningError}; + +use rand::rngs::mock::StepRng; +use rand::{CryptoRng, RngCore}; +use rsa::rand_core; + +#[test] +fn test_core_jwk_deserialization_rsa() { + let json = "{ + \"kty\": \"RSA\", + \"use\": \"sig\", + \"kid\": \"2011-04-29\", + \"n\": \"0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhD\ + R1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6C\ + f0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1\ + n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1\ + jF44-csFCur-kEgU8awapJzKnqDKgw\", + \"e\": \"AQAB\" + }"; + + let key: CoreJsonWebKey = serde_json::from_str(json).expect("deserialization failed"); + assert_eq!(key.kty, CoreJsonWebKeyType::RSA); + assert_eq!(key.use_, Some(CoreJsonWebKeyUse::Signature)); + assert_eq!(key.kid, Some(JsonWebKeyId::new("2011-04-29".to_string()))); + assert_eq!( + key.n, + Some(Base64UrlEncodedBytes::new(vec![ + 210, 252, 123, 106, 10, 30, 108, 103, 16, 74, 235, 143, 136, 178, 87, 102, 155, 77, + 246, 121, 221, 173, 9, 155, 92, 74, 108, 217, 168, 128, 21, 181, 161, 51, 191, 11, 133, + 108, 120, 113, 182, 223, 0, 11, 85, 79, 206, 179, 194, 237, 81, 43, 182, 143, 20, 92, + 110, 132, 52, 117, 47, 171, 82, 161, 207, 193, 36, 64, 143, 121, 181, 138, 69, 120, + 193, 100, 40, 133, 87, 137, 247, 162, 73, 227, 132, 203, 45, 159, 174, 45, 103, 253, + 150, 251, 146, 108, 25, 142, 7, 115, 153, 253, 200, 21, 192, 175, 9, 125, 222, 90, 173, + 239, 244, 77, 231, 14, 130, 127, 72, 120, 67, 36, 57, 191, 238, 185, 96, 104, 208, 71, + 79, 197, 13, 109, 144, 191, 58, 152, 223, 175, 16, 64, 200, 156, 2, 214, 146, 171, 59, + 60, 40, 150, 96, 157, 134, 253, 115, 183, 116, 206, 7, 64, 100, 124, 238, 234, 163, 16, + 189, 18, 249, 133, 168, 235, 159, 89, 253, 212, 38, 206, 165, 178, 18, 15, 79, 42, 52, + 188, 171, 118, 75, 126, 108, 84, 214, 132, 2, 56, 188, 196, 5, 135, 165, 158, 102, 237, + 31, 51, 137, 69, 119, 99, 92, 71, 10, 247, 92, 249, 44, 32, 209, 218, 67, 225, 191, + 196, 25, 226, 34, 166, 240, 208, 187, 53, 140, 94, 56, 249, 203, 5, 10, 234, 254, 144, + 72, 20, 241, 172, 26, 164, 156, 202, 158, 160, 202, 131, + ])) + ); + assert_eq!(key.e, Some(Base64UrlEncodedBytes::new(vec![1, 0, 1]))); + assert_eq!(key.k, None); +} +#[test] +fn test_core_jwk_deserialization_ec() { + let json = "{ + \"kty\": \"EC\", + \"use\": \"sig\", + \"kid\": \"2011-04-29\", + \"crv\": \"P-256\", + \"x\": \"kXCGZIr3oI6sKbnT6rRsIdxFXw3_VbLk_cveajgqXk8\", + \"y\": \"StDvKIgXqAxJ6DuebREh-1vgvZRW3dfrOxSIKzBtRI0\" + }"; + + let key: CoreJsonWebKey = serde_json::from_str(json).expect("deserialization failed"); + assert_eq!(key.kty, CoreJsonWebKeyType::EllipticCurve); + assert_eq!(key.use_, Some(CoreJsonWebKeyUse::Signature)); + assert_eq!(key.kid, Some(JsonWebKeyId::new("2011-04-29".to_string()))); + assert_eq!(key.crv, Some(CoreJsonCurveType::P256)); + assert_eq!( + key.y, + Some(Base64UrlEncodedBytes::new(vec![ + 0x4a, 0xd0, 0xef, 0x28, 0x88, 0x17, 0xa8, 0x0c, 0x49, 0xe8, 0x3b, 0x9e, 0x6d, 0x11, + 0x21, 0xfb, 0x5b, 0xe0, 0xbd, 0x94, 0x56, 0xdd, 0xd7, 0xeb, 0x3b, 0x14, 0x88, 0x2b, + 0x30, 0x6d, 0x44, 0x8d + ])) + ); + assert_eq!( + key.x, + Some(Base64UrlEncodedBytes::new(vec![ + 0x91, 0x70, 0x86, 0x64, 0x8a, 0xf7, 0xa0, 0x8e, 0xac, 0x29, 0xb9, 0xd3, 0xea, 0xb4, + 0x6c, 0x21, 0xdc, 0x45, 0x5f, 0x0d, 0xff, 0x55, 0xb2, 0xe4, 0xfd, 0xcb, 0xde, 0x6a, + 0x38, 0x2a, 0x5e, 0x4f + ])) + ); +} + +#[test] +fn test_core_jwk_deserialization_ed() { + let json = "{ + \"alg\": \"Ed25519\", + \"crv\": \"Ed25519\", + \"kty\": \"OKP\", + \"use\": \"sig\", + \"x\": \"vZ3CX884r0qNJ18pgXUTvFufK3ZmDzQfvMROJz6CLBc\" + }"; + + let key: CoreJsonWebKey = serde_json::from_str(json).expect("deserialization failed"); + assert_eq!(key.kty, CoreJsonWebKeyType::OctetKeyPair); + assert_eq!(key.use_, Some(CoreJsonWebKeyUse::Signature)); + assert_eq!(key.crv, Some(CoreJsonCurveType::Ed25519)); + assert_eq!( + key.x, + Some(Base64UrlEncodedBytes::new(vec![ + 0xBD, 0x9D, 0xC2, 0x5F, 0xCF, 0x38, 0xAF, 0x4A, 0x8D, 0x27, 0x5F, 0x29, 0x81, 0x75, + 0x13, 0xBC, 0x5B, 0x9F, 0x2B, 0x76, 0x66, 0x0F, 0x34, 0x1F, 0xBC, 0xC4, 0x4E, 0x27, + 0x3E, 0x82, 0x2C, 0x17 + ])) + ); +} + +#[test] +fn test_core_jwk_deserialization_symmetric() { + let json = "{\ + \"kty\":\"oct\", + \"alg\":\"A128GCM\", + \"k\":\"GawgguFyGrWKav7AX4VKUg\" + }"; + + let key: CoreJsonWebKey = serde_json::from_str(json).expect("deserialization failed"); + assert_eq!(key.kty, CoreJsonWebKeyType::Symmetric); + assert_eq!(key.use_, None); + assert_eq!(key.kid, None); + assert_eq!(key.n, None); + assert_eq!(key.e, None); + #[cfg(feature = "jwk-alg")] + assert_eq!( + key.alg, + Some(JsonWebTokenAlgorithm::Encryption( + CoreJweContentEncryptionAlgorithm::Aes128Gcm + )) + ); + assert_eq!( + key.k, + Some(Base64UrlEncodedBytes::new(vec![ + 25, 172, 32, 130, 225, 114, 26, 181, 138, 106, 254, 192, 95, 133, 74, 82, + ])) + ); +} + +#[test] +fn test_core_jwk_deserialization_no_optional() { + let json = "{\"kty\":\"oct\"}"; + let key: CoreJsonWebKey = serde_json::from_str(json).expect("deserialization failed"); + assert_eq!(key.kty, CoreJsonWebKeyType::Symmetric); + assert_eq!(key.use_, None); + assert_eq!(key.kid, None); + assert_eq!(key.n, None); + assert_eq!(key.e, None); + assert_eq!(key.k, None); +} + +#[test] +fn test_core_jwk_deserialization_unrecognized() { + // Unrecognized fields should be ignored during deserialization + let json = "{\ + \"kty\": \"oct\", + \"unrecognized\": 1234 + }"; + let key: CoreJsonWebKey = serde_json::from_str(json).expect("deserialization failed"); + assert_eq!(key.kty, CoreJsonWebKeyType::Symmetric); +} + +#[test] +fn test_core_jwk_deserialization_dupe_fields() { + // From RFC 7517, Section 4: + // "The member names within a JWK MUST be unique; JWK parsers MUST either + // reject JWKs with duplicate member names or use a JSON parser that + // returns only the lexically last duplicate member name, as specified + // in Section 15.12 (The JSON Object) of ECMAScript 5.1 [ECMAScript]." + let json = "{\ + \"kty\":\"oct\", + \"k\":\"GawgguFyGrWKav7AX4VKUg\", + \"k\":\"GawgguFyGrWKav7AX4VKVg\" + }"; + + assert!(serde_json::from_str::(json) + .expect_err("deserialization must fail when duplicate fields are present") + .to_string() + // This is probably not ideal since the serde/serde_json contracts don't guarantee this + // error message. However, we want to be sure that this fails for the expected reason + // and not by happenstance, so this is fine for now. + .contains("duplicate field")); +} + +fn verify_signature( + key: &CoreJsonWebKey, + alg: &CoreJwsSigningAlgorithm, + signing_input: &str, + signature_base64: &str, +) { + let signature = base64::decode_config(signature_base64, crate::core::base64_url_safe_no_pad()) + .expect("failed to base64url decode"); + key.verify_signature(alg, signing_input.as_bytes(), &signature) + .expect("signature verification failed"); + match key + .verify_signature( + alg, + (signing_input.to_string() + "foobar").as_bytes(), + &signature, + ) + .expect_err("signature verification should fail") + { + SignatureVerificationError::CryptoError(_) => {} + other => panic!("unexpected error: {:?}", other), + } +} + +fn verify_invalid_signature( + key: &CoreJsonWebKey, + alg: &CoreJwsSigningAlgorithm, + signing_input: &str, + signature_base64: &str, +) { + let signature = base64::decode_config(signature_base64, crate::core::base64_url_safe_no_pad()) + .expect("failed to base64url decode"); + match key + .verify_signature(alg, signing_input.as_bytes(), &signature) + .expect_err("signature verification should fail") + { + SignatureVerificationError::CryptoError(_) => {} + other => panic!("unexpected error: {:?}", other), + } +} + +#[test] +fn test_eddsa_verification() { + let key_ed25519: CoreJsonWebKey = + serde_json::from_str(TEST_ED_PUB_KEY_ED25519).expect("deserialization failed"); + let pkcs1_signing_input = "eyJhbGciOiJFZDI1NTE5IiwidHlwIjoiSldUIn0.eyJpc3MiOiJqb2UifQ"; + let signature_ed25519 = + "Augr7UH6hUbWVN0PHqSD5U0bb8y9UOw_eef09ZS5d5haUar_qAto8gyLJxUhNF5wHPoXhdvSGowkPvjiKsEsCQ"; + + let signature_ed25519_other = + "xb4NH-q33sCaRXf1ZhnzQxd4o5ZkBWKd9vGibacqPMAblW_mIJLm9kGerqHX08SPoeDY-dYUmZQz9ls6csfvAw"; + let signature_ed448 = "xxXVMyaYYePdGfMOdU0nENuc70pKwP3vJuc_jBA0rCW-RtbvBLSsc0D9iCPzhrPmQ2X1nTjPkGiAXJ0_NslDBvy3sHu88N64YhnnYBWwwHttBU0jijn_ikbBUHzUwzGuasRFb1ESG_PwedhEcMi-YAwA"; + + // test ed25519 + verify_signature( + &key_ed25519, + &CoreJwsSigningAlgorithm::EdDsaEd25519, + pkcs1_signing_input, + signature_ed25519, + ); + + // signature from ed448 variant + verify_invalid_signature( + &key_ed25519, + &CoreJwsSigningAlgorithm::EdDsaEd25519, + pkcs1_signing_input, + signature_ed448, + ); + + // different signature + verify_invalid_signature( + &key_ed25519, + &CoreJwsSigningAlgorithm::EdDsaEd25519, + pkcs1_signing_input, + signature_ed25519_other, + ); + + // non-EdDsa key + if let Some(err) = key_ed25519 + .verify_signature( + &CoreJwsSigningAlgorithm::EcdsaP256Sha256, + pkcs1_signing_input.as_bytes(), + signature_ed25519.as_bytes(), + ) + .err() + { + let error_msg = "key type does not match signature algorithm".to_string(); + match err { + SignatureVerificationError::InvalidKey(msg) => { + if msg != error_msg { + panic!("The error should be about key type") + } + } + _ => panic!("We should fail before actual validation"), + } + } +} + +#[test] +fn test_ecdsa_verification() { + let key_p256: CoreJsonWebKey = + serde_json::from_str(TEST_EC_PUB_KEY_P256).expect("deserialization failed"); + let key_p384: CoreJsonWebKey = + serde_json::from_str(TEST_EC_PUB_KEY_P384).expect("deserialization failed"); + let pkcs1_signing_input = "eyJhbGciOiJSUzI1NiIsImtpZCI6ImJpbGJvLmJhZ2dpbnNAaG9iYml0b24uZX\ + hhbXBsZSJ9.\ + SXTigJlzIGEgZGFuZ2Vyb3VzIGJ1c2luZXNzLCBGcm9kbywgZ29pbmcgb3V0IH\ + lvdXIgZG9vci4gWW91IHN0ZXAgb250byB0aGUgcm9hZCwgYW5kIGlmIHlvdSBk\ + b24ndCBrZWVwIHlvdXIgZmVldCwgdGhlcmXigJlzIG5vIGtub3dpbmcgd2hlcm\ + UgeW91IG1pZ2h0IGJlIHN3ZXB0IG9mZiB0by4"; + let signature_p256 = + "EnKCtAHhzhqxV2GTr1VEurse2kQ7oHpFoVqM66sYGlmahDRGSlfrVAsGCzdLv66OS2Qf1zt6OPHX-5ZAkMgzlA"; + let signature_p384 = "B_9oDAabMasZ2Yt_cnAS21owaN0uWSInQBPxTqqiM3N3XjkksBRMGqguJLV5WoSMcvqgXwHTTQtbHGuh0Uf4g6LEr7XtO1T2KCttQR27d5YbvVZdORrzCm0Nsm1zkV-i"; + + //test p256 + verify_signature( + &key_p256, + &CoreJwsSigningAlgorithm::EcdsaP256Sha256, + pkcs1_signing_input, + signature_p256, + ); + + //wrong algo should fail before ring validation + if let Some(err) = key_p256 + .verify_signature( + &CoreJwsSigningAlgorithm::EcdsaP384Sha384, + pkcs1_signing_input.as_bytes(), + signature_p256.as_bytes(), + ) + .err() + { + let error_msg = "Key uses different CRV than JWT".to_string(); + match err { + SignatureVerificationError::InvalidKey(msg) => { + if msg != error_msg { + panic!("The error should be about different CRVs") + } + } + _ => panic!("We should fail before actual validation"), + } + } + // suppose we have alg specified correctly, but the signature given is actually a p384 + verify_invalid_signature( + &key_p256, + &CoreJwsSigningAlgorithm::EcdsaP256Sha256, + pkcs1_signing_input, + signature_p384, + ); + + //test p384 + verify_signature( + &key_p384, + &CoreJwsSigningAlgorithm::EcdsaP384Sha384, + pkcs1_signing_input, + signature_p384, + ); + + // suppose we have alg specified correctly, but the signature given is actually a p256 + verify_invalid_signature( + &key_p384, + &CoreJwsSigningAlgorithm::EcdsaP384Sha384, + pkcs1_signing_input, + signature_p256, + ); + + //wrong algo should fail before ring validation + if let Some(err) = key_p384 + .verify_signature( + &CoreJwsSigningAlgorithm::EcdsaP256Sha256, + pkcs1_signing_input.as_bytes(), + signature_p384.as_bytes(), + ) + .err() + { + let error_msg = "Key uses different CRV than JWT".to_string(); + match err { + SignatureVerificationError::InvalidKey(msg) => { + if msg != error_msg { + panic!("The error should be about different CRVs") + } + } + _ => panic!("We should fail before actual validation"), + } + } +} + +#[test] +fn test_rsa_pkcs1_verification() { + let key: CoreJsonWebKey = + serde_json::from_str(TEST_RSA_PUB_KEY).expect("deserialization failed"); + + // Source: https://tools.ietf.org/html/rfc7520#section-4.1 + let pkcs1_signing_input = "eyJhbGciOiJSUzI1NiIsImtpZCI6ImJpbGJvLmJhZ2dpbnNAaG9iYml0b24uZX\ + hhbXBsZSJ9.\ + SXTigJlzIGEgZGFuZ2Vyb3VzIGJ1c2luZXNzLCBGcm9kbywgZ29pbmcgb3V0IH\ + lvdXIgZG9vci4gWW91IHN0ZXAgb250byB0aGUgcm9hZCwgYW5kIGlmIHlvdSBk\ + b24ndCBrZWVwIHlvdXIgZmVldCwgdGhlcmXigJlzIG5vIGtub3dpbmcgd2hlcm\ + UgeW91IG1pZ2h0IGJlIHN3ZXB0IG9mZiB0by4"; + + verify_signature( + &key, + &CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256, + pkcs1_signing_input, + "MRjdkly7_-oTPTS3AXP41iQIGKa80A0ZmTuV5MEaHoxnW2e5CZ5NlKtainoFmK\ + ZopdHM1O2U4mwzJdQx996ivp83xuglII7PNDi84wnB-BDkoBwA78185hX-Es4J\ + IwmDLJK3lfWRa-XtL0RnltuYv746iYTh_qHRD68BNt1uSNCrUCTJDt5aAE6x8w\ + W1Kt9eRo4QPocSadnHXFxnt8Is9UzpERV0ePPQdLuW3IS_de3xyIrDaLGdjluP\ + xUAhb6L2aXic1U12podGU0KLUQSE_oI-ZnmKJ3F4uOZDnd6QZWJushZ41Axf_f\ + cIe8u9ipH84ogoree7vjbU5y18kDquDg", + ); + + verify_signature( + &key, + &CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha384, + pkcs1_signing_input, + "dgTHNAePceEDFodrPybExGb2aF4fHb4bRpb_4bgYHq78fUdHFCScg0bZP51zjB\ + joH-4fr0P7Y8-Sns0GuXRy_itY2Yh0mEdXVn6HwZVOGIVRAuBkY0cAgSXGKU40\ + 1G-GhamiNyNDfN2bwHftPPvCdsChtsLeAUvhWUKSLgIfT-jvMr9iZ5d0SQrUvv\ + G1ReEoBDyKUzqGQehO3CNGJ-QkI8p-fBTa2KHQxct6cU5_anSXCd-kC2rtEQS9\ + E8AcMFLA2Bv9IXsURBRU_bwMgxTG8c6ATDJM8k-zJSSP5a44EFKHUtH1xspYFo\ + KV6Za-frCV8kcFCILMf-4ATlj5Z62o1A", + ); + + verify_signature( + &key, + &CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha512, + pkcs1_signing_input, + "hIRFVu3hlbIM9Xt2V9xldCoF_94BEDg-6kVetoceakgD-9hicX0BnOI3YxR-JQ\ + 0to4saNEdGP1ulvanfa5uK3PnltQr1sJ1l1x_TPNh8vdvZ5WmAtkQcZvRiK580\ + hliHV1l65yLyGH4ckDicOg5VF4BASkBw6sUO_LCB8pMJotK5jQxDbNkPmSGbFV\ + nzVXXy6QI_r6nqmguo5DMFlPeploS-aQ7ArfYqR3gKEp3l5gWWKn86lwVKRGjv\ + zeRMf3ubhKxvHUyU8cE5p1VPpOzTJ3cPwUe68s24Ehf2jpgZIIXb9XQv4L0Unf\ + GAXTBY7Rszx9LvGByoFx3eOpbMvtLQxA", + ); + + // Wrong key type + match key + .verify_signature( + &CoreJwsSigningAlgorithm::EcdsaP256Sha256, + pkcs1_signing_input.as_bytes(), + &Vec::new(), + ) + .expect_err("signature verification should fail") + { + SignatureVerificationError::InvalidKey(_) => {} + other => panic!("unexpected error: {:?}", other), + } + + // Wrong key usage + let enc_key_json = "{ + \"kty\": \"RSA\", + \"kid\": \"bilbo.baggins@hobbiton.example\", + \"use\": \"enc\", + \"n\": \"n4EPtAOCc9AlkeQHPzHStgAbgs7bTZLwUBZdR8_KuKPEHLd4rHVTeT\ + -O-XV2jRojdNhxJWTDvNd7nqQ0VEiZQHz_AJmSCpMaJMRBSFKrKb2wqV\ + wGU_NsYOYL-QtiWN2lbzcEe6XC0dApr5ydQLrHqkHHig3RBordaZ6Aj-\ + oBHqFEHYpPe7Tpe-OfVfHd1E6cS6M1FZcD1NNLYD5lFHpPI9bTwJlsde\ + 3uhGqC0ZCuEHg8lhzwOHrtIQbS0FVbb9k3-tVTU4fg_3L_vniUFAKwuC\ + LqKnS2BYwdq_mzSnbLY7h_qixoR7jig3__kRhuaxwUkRz5iaiQkqgc5g\ + HdrNP5zw\", + \"e\": \"AQAB\" + }"; + let enc_key: CoreJsonWebKey = + serde_json::from_str(enc_key_json).expect("deserialization failed"); + match enc_key + .verify_signature( + &CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256, + pkcs1_signing_input.as_bytes(), + &Vec::new(), + ) + .expect_err("signature verification should fail") + { + SignatureVerificationError::InvalidKey(_) => {} + other => panic!("unexpected error: {:?}", other), + } + + // Key without usage specified should work + let nousage_key_json = "{ + \"kty\": \"RSA\", + \"kid\": \"bilbo.baggins@hobbiton.example\", + \"n\": \"n4EPtAOCc9AlkeQHPzHStgAbgs7bTZLwUBZdR8_KuKPEHLd4rHVTeT\ + -O-XV2jRojdNhxJWTDvNd7nqQ0VEiZQHz_AJmSCpMaJMRBSFKrKb2wqV\ + wGU_NsYOYL-QtiWN2lbzcEe6XC0dApr5ydQLrHqkHHig3RBordaZ6Aj-\ + oBHqFEHYpPe7Tpe-OfVfHd1E6cS6M1FZcD1NNLYD5lFHpPI9bTwJlsde\ + 3uhGqC0ZCuEHg8lhzwOHrtIQbS0FVbb9k3-tVTU4fg_3L_vniUFAKwuC\ + LqKnS2BYwdq_mzSnbLY7h_qixoR7jig3__kRhuaxwUkRz5iaiQkqgc5g\ + HdrNP5zw\", + \"e\": \"AQAB\" + }"; + let nousage_key: CoreJsonWebKey = + serde_json::from_str(nousage_key_json).expect("deserialization failed"); + verify_signature( + &nousage_key, + &CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256, + pkcs1_signing_input, + "MRjdkly7_-oTPTS3AXP41iQIGKa80A0ZmTuV5MEaHoxnW2e5CZ5NlKtainoFmK\ + ZopdHM1O2U4mwzJdQx996ivp83xuglII7PNDi84wnB-BDkoBwA78185hX-Es4J\ + IwmDLJK3lfWRa-XtL0RnltuYv746iYTh_qHRD68BNt1uSNCrUCTJDt5aAE6x8w\ + W1Kt9eRo4QPocSadnHXFxnt8Is9UzpERV0ePPQdLuW3IS_de3xyIrDaLGdjluP\ + xUAhb6L2aXic1U12podGU0KLUQSE_oI-ZnmKJ3F4uOZDnd6QZWJushZ41Axf_f\ + cIe8u9ipH84ogoree7vjbU5y18kDquDg", + ); +} + +#[test] +fn test_rsa_pss_verification() { + let key: CoreJsonWebKey = + serde_json::from_str(TEST_RSA_PUB_KEY).expect("deserialization failed"); + // Source: https://tools.ietf.org/html/rfc7520#section-4.2 + let pss_signing_input = + "eyJhbGciOiJQUzM4NCIsImtpZCI6ImJpbGJvLmJhZ2dpbnNAaG9iYml0b24uZXhhbXBsZSJ9.\ + SXTigJlzIGEgZGFuZ2Vyb3VzIGJ1c2luZXNzLCBGcm9kbywgZ29pbmcgb3V0IH\ + lvdXIgZG9vci4gWW91IHN0ZXAgb250byB0aGUgcm9hZCwgYW5kIGlmIHlvdSBk\ + b24ndCBrZWVwIHlvdXIgZmVldCwgdGhlcmXigJlzIG5vIGtub3dpbmcgd2hlcm\ + UgeW91IG1pZ2h0IGJlIHN3ZXB0IG9mZiB0by4"; + + verify_signature( + &key, + &CoreJwsSigningAlgorithm::RsaSsaPssSha256, + pss_signing_input, + "Y62we_hs07d0qJ2cT_QpbrodwDhPK9rEpNX2b3GqLHFM18YtDlPCr40Xf_yLIosIrt\ + mMP4NgDSCkn2qOcRJBD8zrHumER4JIkGZbRIwU8gYms8xKX2HaveK9vrOjbHoWLjOU\ + nyNpprYUFGdRZ6oebT61bqU2CZrJG_GcqR87W8FOn7kqrCPI7B8oNHgliMke49hOpz\ + mluL20BKN5Mb3O42nwgmiONZK0Pjm2GTIAYRUvNQ741aCWVJ3rnWvo99qWhe86ap_H\ + v40SUSaMwJig5AqC-wHIzYaYU0PlQbi83Dgw7Zft9kL2dGB0vMWY_h2HDgZU0teAcK\ + SkhyH8ZDRyYQ", + ); + + verify_signature( + &key, + &CoreJwsSigningAlgorithm::RsaSsaPssSha384, + pss_signing_input, + "cu22eBqkYDKgIlTpzDXGvaFfz6WGoz7fUDcfT0kkOy42miAh2qyBzk1xEsnk2I\ + pN6-tPid6VrklHkqsGqDqHCdP6O8TTB5dDDItllVo6_1OLPpcbUrhiUSMxbbXU\ + vdvWXzg-UD8biiReQFlfz28zGWVsdiNAUf8ZnyPEgVFn442ZdNqiVJRmBqrYRX\ + e8P_ijQ7p8Vdz0TTrxUeT3lm8d9shnr2lfJT8ImUjvAA2Xez2Mlp8cBE5awDzT\ + 0qI0n6uiP1aCN_2_jLAeQTlqRHtfa64QQSUmFAAjVKPbByi7xho0uTOcbH510a\ + 6GYmJUAfmWjwZ6oD4ifKo8DYM-X72Eaw", + ); + + verify_signature( + &key, + &CoreJwsSigningAlgorithm::RsaSsaPssSha512, + pss_signing_input, + "G8vtysTFbSXht_PU6NdXeYDOSIQhxcp6zFWuvtx2NCtgsm-J22CKqlapp1zjPkXTo4\ + xrYlIgFjQVQZ9Cr7KWJXK7qYUkdfJNkB1E96EQR32ocx_9RQDS_eQNlGWjoDRduD9z\ + 2hKs-S0EhOy39wUeUYbcKA1MpkW71hUPI56Ou5kzclNbe22slB4mYd6Mx0dLOeFDF2\ + C7ZUDxso-cHMh4hU2E8vlp-TZUf9eqAri9T1F_pjRF8WNBj-vrqwy3bCROgIslYA8u\ + c_FEXn6fZ21up5mU9vg5_LdeBoSh4Idmz8HLn5rpVd57AsQ2PbLMsKXcpVUhwP_ID1\ + 7zsAFuCEFJqA", + ); +} + +#[test] +fn test_hmac_sha256_verification() { + // the original spec example also has alg=HS256, which was removed to test other signing algorithms + let key_json = "{ + \"kty\": \"oct\", + \"kid\": \"018c0ae5-4d9b-471b-bfd6-eef314bc7037\", + \"use\": \"sig\", + \"k\": \"hJtXIZ2uSN5kbQfbtTNWbpdmhkV8FJG-Onbc6mxCcYg\" + }"; + + let key: CoreJsonWebKey = serde_json::from_str(key_json).expect("deserialization failed"); + // Source: https://tools.ietf.org/html/rfc7520#section-4.4 + let signing_input = "eyJhbGciOiJIUzI1NiIsImtpZCI6IjAxOGMwYWU1LTRkOWItNDcxYi1iZmQ2LW\ + VlZjMxNGJjNzAzNyJ9.\ + SXTigJlzIGEgZGFuZ2Vyb3VzIGJ1c2luZXNzLCBGcm9kbywgZ29pbmcgb3V0IH\ + lvdXIgZG9vci4gWW91IHN0ZXAgb250byB0aGUgcm9hZCwgYW5kIGlmIHlvdSBk\ + b24ndCBrZWVwIHlvdXIgZmVldCwgdGhlcmXigJlzIG5vIGtub3dpbmcgd2hlcm\ + UgeW91IG1pZ2h0IGJlIHN3ZXB0IG9mZiB0by4"; + + verify_signature( + &key, + &CoreJwsSigningAlgorithm::HmacSha256, + signing_input, + "s0h6KThzkfBBBkLspW1h84VsJZFTsPPqMDA7g1Md7p0", + ); + + verify_signature( + &key, + &CoreJwsSigningAlgorithm::HmacSha384, + signing_input, + "O1jhTTHkuaiubwDZoIBLv6zjEarXHc22NNu05IdYh_yzIKGYXJQcaI2WnF4BCq7j", + ); + + verify_signature( + &key, + &CoreJwsSigningAlgorithm::HmacSha512, + signing_input, + "rdWYqzXuAJp4OW-exqIwrO8HJJQDYu0_fkTIUBHmyHMFJ0pVe7fjP7QtE7BaX-7FN5\ + YiyiM11MwIEAxzxBj6qw", + ); +} + +fn expect_hmac( + secret_key: &CoreHmacKey, + message: &[u8], + alg: &CoreJwsSigningAlgorithm, + expected_sig_base64: &str, +) { + let sig = secret_key.sign(alg, message).unwrap(); + assert_eq!(expected_sig_base64, base64::encode(&sig)); + + secret_key + .as_verification_key() + .verify_signature(alg, message, &sig) + .unwrap(); +} + +#[test] +fn test_hmac_signing() { + let secret_key = CoreHmacKey::new("my_secret_key"); + let message = "hello HMAC".as_ref(); + expect_hmac( + &secret_key, + message, + &CoreJwsSigningAlgorithm::HmacSha256, + "Pm6UhOcfx6D8LeCG4taMQNQXDTHwnVOSEcB7tidkM2M=", + ); + + expect_hmac( + &secret_key, + message, + &CoreJwsSigningAlgorithm::HmacSha384, + "BiYrxF0XjImSnfqT2n+Tu3EspstKZmVtUHbK77LHerfKNwCikuClNJDAVwr2xMLp", + ); + + expect_hmac( + &secret_key, + message, + &CoreJwsSigningAlgorithm::HmacSha512, + "glKjDMXBhB6sSKGCdLW4QeBOJ3vOgOlbMJjbeus8/KQ3dk7dtsqtrpfoDoW8lrU+rncd2jBWaKnp1zKdpEfSn\ + A==", + ); + + assert_eq!( + secret_key.sign(&CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256, message), + Err(SigningError::UnsupportedAlg("RS256".to_string())), + ); + assert_eq!( + secret_key.sign(&CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha384, message), + Err(SigningError::UnsupportedAlg("RS384".to_string())), + ); + assert_eq!( + secret_key.sign(&CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha512, message), + Err(SigningError::UnsupportedAlg("RS512".to_string())), + ); + assert_eq!( + secret_key.sign(&CoreJwsSigningAlgorithm::RsaSsaPssSha256, message), + Err(SigningError::UnsupportedAlg("PS256".to_string())), + ); + assert_eq!( + secret_key.sign(&CoreJwsSigningAlgorithm::RsaSsaPssSha384, message), + Err(SigningError::UnsupportedAlg("PS384".to_string())), + ); + assert_eq!( + secret_key.sign(&CoreJwsSigningAlgorithm::RsaSsaPssSha512, message), + Err(SigningError::UnsupportedAlg("PS512".to_string())), + ); + assert_eq!( + secret_key.sign(&CoreJwsSigningAlgorithm::EcdsaP256Sha256, message), + Err(SigningError::UnsupportedAlg("ES256".to_string())), + ); + assert_eq!( + secret_key.sign(&CoreJwsSigningAlgorithm::EcdsaP384Sha384, message), + Err(SigningError::UnsupportedAlg("ES384".to_string())), + ); + assert_eq!( + secret_key.sign(&CoreJwsSigningAlgorithm::EcdsaP521Sha512, message), + Err(SigningError::UnsupportedAlg("ES512".to_string())), + ); + assert_eq!( + secret_key.sign(&CoreJwsSigningAlgorithm::None, message), + Err(SigningError::UnsupportedAlg("none".to_string())), + ); +} + +const TEST_ED25519_KEY: &str = "\ + -----BEGIN PRIVATE KEY-----\n\ + MC4CAQAwBQYDK2VwBCIEICWeYPLxoZKHZlQ6rkBi11E9JwchynXtljATLqym/XS9\n\ + -----END PRIVATE KEY-----\ + "; + +// This is just a test key that isn't used for anything else. +const TEST_RSA_KEY: &str = "\ + -----BEGIN RSA PRIVATE KEY-----\n\ + MIIEowIBAAKCAQEAsRMj0YYjy7du6v1gWyKSTJx3YjBzZTG0XotRP0IaObw0k+68\n\ + 30dXadjL5jVhSWNdcg9OyMyTGWfdNqfdrS6ppBqlQNgjZJdloIqL9zOLBZrDm7G4\n\ + +qN4KeZ4/5TyEilq2zOHHGFEzXpOq/UxqVnm3J4fhjqCNaS2nKd7HVVXGBQQ+4+F\n\ + dVT+MyJXemw5maz2F/h324TQi6XoUPEwUddxBwLQFSOlzWnHYMc4/lcyZJ8MpTXC\n\ + MPe/YJFNtb9CaikKUdf8x4mzwH7usSf8s2d6R4dQITzKrjrEJ0u3w3eGkBBapoMV\n\ + FBGPjP3Haz5FsVtHc5VEN3FZVIDF6HrbJH1C4QIDAQABAoIBAHSS3izM+3nc7Bel\n\ + 8S5uRxRKmcm5je6b11u6qiVUFkHWJmMRc6QmqmSThkCq+b4/vUAe1cYZ7+l02Exo\n\ + HOcrZiEULaDP6hUKGqyjKVv3wdlRtt8kFFxlC/HBufzAiNDuFVvzw0oquwnvMCXC\n\ + yQvtlK+/JY/PqvM32cSt+b4o9apySsHqAtdsoHHohK82jsQqIfCi1v8XYV/xRBJB\n\ + cQMCaA0Ls3tFpmJv3JdikyyQxio4kZ5tswghC63znCp1iL+qDq1wjjKzjick9MDb\n\ + Qzb95X09QQP201l1FPWN7Kbhj4ybg6PJGz/VHQcvILcBCoYIc0UY/OMSBt9VN9yD\n\ + wr1WlbECgYEA37difsTMcLmUEN57sicFe1q4lxH6eqnUBjmoKBflx4oMIIyRnfjF\n\ + Jwsu9yIiBkJfBCP85nl2tZdcV0wfZLf6amxB/KMtdfW6r8eoTDzE472OYxSIg1F5\n\ + dI4qn2nBI0Dou0g58xj+Kv0iLaym0pxtyJkSg/rxZGwKb9a+x5WAs50CgYEAyqC0\n\ + NcZs2BRIiT5kEOF6+MeUvarbKh1mangKHKcTdXRrvoJ+Z5izm7FifBixo/79MYpt\n\ + 0VofW0IzYKtAI9KZDq2JcozEbZ+lt/ZPH5QEXO4T39QbDoAG8BbOmEP7l+6m+7QO\n\ + PiQ0WSNjDnwk3W7Zihgg31DH7hyxsxQCapKLcxUCgYAwERXPiPcoDSd8DGFlYK7z\n\ + 1wUsKEe6DT0p7T9tBd1v5wA+ChXLbETn46Y+oQ3QbHg/yn+vAU/5KkFD3G4uVL0w\n\ + Gnx/DIxa+OYYmHxXjQL8r6ClNycxl9LRsS4FPFKsAWk/u///dFI/6E1spNjfDY8k\n\ + 94ab5tHwsqn3Z5tsBHo3nQKBgFUmxbSXh2Qi2fy6+GhTqU7k6G/wXhvLsR9rBKzX\n\ + 1YiVfTXZNu+oL0ptd/q4keZeIN7x0oaY/fZm0pp8PP8Q4HtXmBxIZb+/yG+Pld6q\n\ + YE8BSd7VDu3ABapdm0JHx3Iou4mpOBcLNeiDw3vx1bgsfkTXMPFHzE0XR+H+tak9\n\ + nlalAoGBALAmAF7WBGdOt43Rj8hPaKOM/ahj+6z3CNwVreToNsVBHoyNmiO8q7MC\n\ + +tRo4jgdrzk1pzs66OIHfbx5P1mXKPtgPZhvI5omAY8WqXEgeNqSL1Ksp6LZ2ql/\n\ + ouZns5xwKc9+aRL+GWoAGNzwzcjE8cP52sBy/r0rYXTs/sZo5kgV\n\ + -----END RSA PRIVATE KEY-----\ + "; + +fn expect_ed_sig( + private_key: &CoreEdDsaPrivateSigningKey, + message: &[u8], + alg: &CoreJwsSigningAlgorithm, + expected_sig_base64: &str, +) { + let sig = private_key.sign(alg, message).unwrap(); + assert_eq!(expected_sig_base64, base64::encode(&sig)); + + let public_key = private_key.as_verification_key(); + public_key.verify_signature(alg, message, &sig).unwrap(); +} + +fn expect_rsa_sig( + private_key: &CoreRsaPrivateSigningKey, + message: &[u8], + alg: &CoreJwsSigningAlgorithm, + expected_sig_base64: &str, +) { + let sig = private_key.sign(alg, message).unwrap(); + assert_eq!(expected_sig_base64, base64::encode(&sig)); + + let public_key = private_key.as_verification_key(); + public_key.verify_signature(alg, message, &sig).unwrap(); +} + +#[derive(Clone)] +struct TestRng(StepRng); + +impl CryptoRng for TestRng {} +impl RngCore for TestRng { + fn next_u32(&mut self) -> u32 { + self.0.next_u32() + } + fn next_u64(&mut self) -> u64 { + self.0.next_u64() + } + fn fill_bytes(&mut self, dest: &mut [u8]) { + self.0.fill_bytes(dest) + } + fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), rand_core::Error> { + self.0.try_fill_bytes(dest) + } +} + +#[test] +fn test_ed_signing() { + let private_key = CoreEdDsaPrivateSigningKey::from_ed25519_pem( + TEST_ED25519_KEY, + Some(JsonWebKeyId::new("test_key".to_string())), + ) + .unwrap(); + + let public_key_jwk = private_key.as_verification_key(); + let public_key_jwk_str = serde_json::to_string(&public_key_jwk).unwrap(); + + assert_eq!( + "{\ + \"kty\":\"OKP\",\ + \"use\":\"sig\",\ + \"kid\":\"test_key\",\ + \"crv\":\"Ed25519\",\ + \"x\":\"E6lXdyel1n9C1lcr3FK8OsfsfO2ZgcWhPflJ6yIf7e8\"\ + }", + public_key_jwk_str + ); + + let message = "hello EdDsa".as_ref(); + expect_ed_sig( + &private_key, + message, + &CoreJwsSigningAlgorithm::EdDsaEd25519, + "XqP8sXaPrQa37+2lw+aiXv+6pegjioYUgo1/ShcX6kRhD2Vxh8DrQUbQlaGbljLJTNNc453E2Axp+Mxm+4OVAQ==", + ); + + assert_eq!( + private_key.sign(&CoreJwsSigningAlgorithm::HmacSha256, message), + Err(SigningError::UnsupportedAlg("HS256".to_string())), + ); + + assert_eq!( + private_key.sign(&CoreJwsSigningAlgorithm::None, message), + Err(SigningError::UnsupportedAlg("none".to_string())), + ); +} + +#[test] +fn test_rsa_signing() { + let private_key = CoreRsaPrivateSigningKey::from_pem_internal( + TEST_RSA_KEY, + // Constant salt used for PSS test vectors below. + Box::new(TestRng(StepRng::new(127, 0))), + Some(JsonWebKeyId::new("test_key".to_string())), + ) + .unwrap(); + + let public_key_jwk = private_key.as_verification_key(); + let public_key_jwk_str = serde_json::to_string(&public_key_jwk).unwrap(); + assert_eq!( + "{\ + \"kty\":\"RSA\",\ + \"use\":\"sig\",\ + \"kid\":\"test_key\",\ + \"n\":\"sRMj0YYjy7du6v1gWyKSTJx3YjBzZTG0XotRP0IaObw0k-6830dXadjL5jVhSWNdcg9OyMyTGWfdNq\ + fdrS6ppBqlQNgjZJdloIqL9zOLBZrDm7G4-qN4KeZ4_5TyEilq2zOHHGFEzXpOq_UxqVnm3J4fhjqCNaS2nKd7\ + HVVXGBQQ-4-FdVT-MyJXemw5maz2F_h324TQi6XoUPEwUddxBwLQFSOlzWnHYMc4_lcyZJ8MpTXCMPe_YJFNtb\ + 9CaikKUdf8x4mzwH7usSf8s2d6R4dQITzKrjrEJ0u3w3eGkBBapoMVFBGPjP3Haz5FsVtHc5VEN3FZVIDF6Hrb\ + JH1C4Q\",\ + \"e\":\"AQAB\"\ + }", + public_key_jwk_str + ); + + let message = "hello RSA".as_ref(); + expect_rsa_sig( + &private_key, + message, + &CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256, + "KBvV+F7Xofg4i4qUA0JEqfhQQdjZ7ralUYTPKRIitaKL4a6ni+abagsHs5V63+bmQF5t6DM4aRH2ZC943Tonkr\ + AUY1mpaqic2vqtrtWk3cyrcHtkPCLNKzFf/6xvHPjeKH1Bu/qTQ0mn+hN6taOgw3ORbm6P9MkelX1RVEia98uwB\ + Zn2BxKeqNYm11vqKDyS5ZFzHwpPrC4rri/uTIcXsQEXB+Lbb+naDpQn8qJqP+S+uM2LGWIXp5ExAJ55A111nIqE\ + Ap0aKwf2U8Q81DWI8lbHbL1dd7FRDtZKm+ainO5ck4L/axtH7C4GIZd+TiXL3iYpiWmNkqlwv9WsNPe8Rg==", + ); + + expect_rsa_sig( + &private_key, + message, + &CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha384, + "YsyhW9DkIoNJPqTNY7pidJi5wWtQGr4xety+2Zt1DKNMG0ENFkxCGPLCYcL9vGSS9kfkrPtQ3Eve7g9DKfg1fg\ + 071SXJHxAlK0iC8mVYfQrxxyFlQDIPEhvCJx6VkWVm2jJhN+vByGRJLTo2n3gtYtMREfz+c5xnXWeIy+JQ0LXOy\ + SyOZl0qHxn1VteczH79uCK0Vv8ZH3IfbQMU+2HjbVeUYRzCoAhlT4V2GY4U1pCrZBlfEyhr0ncHz90FRvvhLT3y\ + SlHa7yY7CRJ+z1CLBOzBiH1Eko4tIJKy/qO9M6EGeFtXhqd4td5g2oY/mUZYjHYjgcDO+wAXrZ9lP/ZVUg==", + ); + + expect_rsa_sig( + &private_key, + message, + &CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha512, + "GXN3rmZhlJw46FHoqiuELcIi6iUr3cVC0HZpjBJhrTVfta/8a4PpzmLbQxjdb1cdU/56XDXkqDSNvzRn4PwAOL\ + 460n0Eg8d8mxwPRHQuyypze9240FEw3lyjp5uPJHn5PrmeelczJ1Xseinmp+JwpKHiHhmxp7FjgJc/o3J/hlz0n\ + G1cgTndtrlp5JPJOJNt4XfgIgqoobH5Wk3ML35C50mD93Ld6V6nn6rK72wgecK1SDXeF4ztQUAjg4VTEojLm5VF\ + kfR7kXV0dIbAvZXDa1uuIOlVDIRfF93rxme1Ze46Dywan+zfsGCcpFfFAsnGLsgNDmATB8IS1lTf1SGMoA==", + ); + + expect_rsa_sig( + &private_key, + message, + &CoreJwsSigningAlgorithm::RsaSsaPssSha256, + "sPwTRDAOGOZr0ofAL/lMSXaR5L5aoFANLei4bqAQFHiFxDDrWZZ28MZiunwCGakOM8Itwas+sIX6vo3VTy9d5Bh\ + Cotb/gY5DhMX/iZJubfS8U1fB8rFWXDpREeAegGvXPjBd0A4M6z9it9Tss43dYqO12mYEpz6rFvbHJKSR9Hnmak4\ + F8TcuZswYtjhHhiib05PGjeJTo/5F15WtR7RYK4slYGOU4mf5wzZSpfgifJ2XjyQQe6oCFvVXftVtiQhEljHiEGv\ + GZH5y4FA7h06PqkHMwQEnRsBhVm4pUqRi641tglZ3HZcoxYUE8AdMX7ltJzy/vpOew2bVGoF3mUnlZw==", + ); + + expect_rsa_sig( + &private_key, + message, + &CoreJwsSigningAlgorithm::RsaSsaPssSha384, + "m0EsYFpNa5YjvEYPcfUpXPMqAWmWmkTaQiyK2HZ9Ejt+cBO/S5jcVqd0y2rCDMV1DpSb/JI8uhwp+qYm/2YKpIa\ + zp+u9PpjlL3jvYn19WbwJTCztJ9XSjcEbtkf1fS/d/BU7FgQzYIE0k++QqHjgzkTI5+2XLYX2WP5dc0r67Or5xaF\ + 0ixL1edpEDKfgF3jiKuLmR2dv4MWHPLYRb1I0zm5C/E7g57DfJT4uNzmLX9gTGr4xe6CxVEYy4eFdE+q1O5J6RXd\ + FZnl4qFK9+x1pk0dhWkpIEaKhweI7YP79iFPnAiUnRM6BsdY+puwjGlaaGtYVFcuPO4uXEXtB1AnsEQ==", + ); + + expect_rsa_sig( + &private_key, + message, + &CoreJwsSigningAlgorithm::RsaSsaPssSha512, + "N8BMNKm1dMOm0/BLzAjtnzvRlVtzgO8fUeKnfvUtK8XWeII5nk74hE3AoAJNPLuTninYtfaF68Supu5CsCJAqO9\ + 1JnVvG8P1DX19iCTzJ83o69+kluBIz7x0l796RysDhqcjybGC+fj0M5MpgkNNcKlNwRixus3sfgCgh3mEB+E1Q11\ + hQKjCTdyOcqzGoima+Na17VBWzU3XXLvB328UfkV2nswBlLUsZMT3I4n/aIziENQCLVPlLdX8z+1NjHSAgd9rZMf\ + gfy0eMsjNuQpqPzVW3mbxlCMMVWpd8LKBprfa291xEk1wwvJCuU9EK7QmQPmYa1HAh+E+R2Dw3ibHdA==", + ); + + assert_eq!( + private_key.sign(&CoreJwsSigningAlgorithm::HmacSha256, message), + Err(SigningError::UnsupportedAlg("HS256".to_string())), + ); + assert_eq!( + private_key.sign(&CoreJwsSigningAlgorithm::HmacSha384, message), + Err(SigningError::UnsupportedAlg("HS384".to_string())), + ); + assert_eq!( + private_key.sign(&CoreJwsSigningAlgorithm::HmacSha512, message), + Err(SigningError::UnsupportedAlg("HS512".to_string())), + ); + assert_eq!( + private_key.sign(&CoreJwsSigningAlgorithm::EcdsaP256Sha256, message), + Err(SigningError::UnsupportedAlg("ES256".to_string())), + ); + assert_eq!( + private_key.sign(&CoreJwsSigningAlgorithm::EcdsaP384Sha384, message), + Err(SigningError::UnsupportedAlg("ES384".to_string())), + ); + assert_eq!( + private_key.sign(&CoreJwsSigningAlgorithm::EcdsaP521Sha512, message), + Err(SigningError::UnsupportedAlg("ES512".to_string())), + ); + assert_eq!( + private_key.sign(&CoreJwsSigningAlgorithm::None, message), + Err(SigningError::UnsupportedAlg("none".to_string())), + ); +} + +#[test] +fn test_rsa_pss_signing() { + let private_key = CoreRsaPrivateSigningKey::from_pem(TEST_RSA_KEY, None).unwrap(); + + const MESSAGE: &str = "This is a probabilistic signature scheme"; + let sig1 = private_key + .sign( + &CoreJwsSigningAlgorithm::RsaSsaPssSha256, + MESSAGE.as_bytes(), + ) + .unwrap(); + let sig2 = private_key + .sign( + &CoreJwsSigningAlgorithm::RsaSsaPssSha256, + MESSAGE.as_bytes(), + ) + .unwrap(); + + assert_ne!(sig1, sig2); +} + +// Tests that JsonWebKeySet ignores unsupported keys during deserialization so that clients can +// use providers that include unsupported keys as long as they only use supported ones to sign +// payloads. +#[test] +fn test_jwks_unsupported_key() { + let jwks_json = "{ + \"keys\": [ + { + \"kty\": \"RSA\", + \"use\": \"sig\", + \"kid\": \"2011-04-29\", + \"n\": \"0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhD\ + R1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6C\ + f0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1\ + n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1\ + jF44-csFCur-kEgU8awapJzKnqDKgw\", + \"e\": \"AQAB\" + }, + { + \"kty\": \"MAGIC\", + \"use\": \"sig\", + \"kid\": \"2040-01-01\", + \"magic\": \"magic\" + }, + { + \"kty\": \"EC\", + \"use\": \"sig\", + \"kid\": \"2011-05-01\", + \"crv\": \"P-256\", + \"x\": \"kXCGZIr3oI6sKbnT6rRsIdxFXw3_VbLk_cveajgqXk8\", + \"y\": \"StDvKIgXqAxJ6DuebREh-1vgvZRW3dfrOxSIKzBtRI0\" + } + ] + }"; + let jwks = serde_json::from_str::(jwks_json) + .expect("deserialization should succeed"); + + assert_eq!(jwks.keys().len(), 2); + + assert_eq!(jwks.keys()[0].kty, CoreJsonWebKeyType::RSA); + assert_eq!(jwks.keys()[0].use_, Some(CoreJsonWebKeyUse::Signature)); + assert_eq!( + jwks.keys()[0].kid, + Some(JsonWebKeyId::new("2011-04-29".to_string())) + ); + + assert_eq!(jwks.keys()[1].kty, CoreJsonWebKeyType::EllipticCurve); + assert_eq!(jwks.keys()[1].use_, Some(CoreJsonWebKeyUse::Signature)); + assert_eq!( + jwks.keys()[1].kid, + Some(JsonWebKeyId::new("2011-05-01".to_string())) + ); + assert_eq!(jwks.keys()[1].crv, Some(CoreJsonCurveType::P256)); +} + +// Tests that JsonWebKeySet ignores keys with unsupported algorithms +#[cfg(feature = "jwk-alg")] +#[test] +fn test_jwks_unsupported_alg() { + let jwks_json = "{ + \"keys\": [ + { + \"kty\": \"EC\", + \"alg\": \"MAGIC\", + \"crv\": \"P-256\", + \"x\": \"kXCGZIr3oI6sKbnT6rRsIdxFXw3_VbLk_cveajgqXk8\", + \"y\": \"StDvKIgXqAxJ6DuebREh-1vgvZRW3dfrOxSIKzBtRI0\" + }, + { + \"kty\": \"EC\", + \"alg\": \"ES256\", + \"kid\": \"2011-05-01\", + \"crv\": \"P-256\", + \"x\": \"kXCGZIr3oI6sKbnT6rRsIdxFXw3_VbLk_cveajgqXk8\", + \"y\": \"StDvKIgXqAxJ6DuebREh-1vgvZRW3dfrOxSIKzBtRI0\" + } + ] + }"; + let jwks = serde_json::from_str::(jwks_json) + .expect("deserialization should succeed"); + assert_eq!(jwks.keys().len(), 1); + let key = &jwks.keys()[0]; + assert_eq!(&key.kid, &Some(JsonWebKeyId::new("2011-05-01".to_string()))); +} + +// Test filtering keys by algorithm +#[cfg(feature = "jwk-alg")] +#[test] +fn test_jwks_same_kid_different_alg() { + let jwks_json = "{ + \"keys\": [ + { + \"kty\": \"RSA\", + \"use\": \"sig\", + \"kid\": \"2011-04-29\", + \"alg\": \"PS256\", + \"n\": \"0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhD\ + R1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6C\ + f0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1\ + n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1\ + jF44-csFCur-kEgU8awapJzKnqDKgw\", + \"e\": \"AQAB\" + }, + { + \"kty\": \"RSA\", + \"use\": \"sig\", + \"kid\": \"2011-04-29\", + \"alg\": \"PS384\", + \"n\": \"0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhD\ + R1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6C\ + f0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1\ + n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1\ + jF44-csFCur-kEgU8awapJzKnqDKgw\", + \"e\": \"AQAB\" + } + ] + }"; + let jwks = serde_json::from_str::(jwks_json) + .expect("deserialization should succeed"); + assert_eq!(jwks.keys().len(), 2); + + { + let keys = jwks.filter_keys( + &Some(JsonWebKeyId::new("2011-04-29".to_string())), + &CoreJwsSigningAlgorithm::RsaSsaPssSha384, + ); + assert_eq!(keys.len(), 1); + assert_eq!( + keys[0].alg, + Some(crate::jwt::JsonWebTokenAlgorithm::Signature( + CoreJwsSigningAlgorithm::RsaSsaPssSha384, + std::marker::PhantomData + )) + ); + } + + { + let keys = jwks.filter_keys( + &Some(JsonWebKeyId::new("2011-04-29".to_string())), + &CoreJwsSigningAlgorithm::RsaSsaPssSha512, + ); + assert_eq!(keys.len(), 0); + } +} diff --git a/src/core/mod.rs b/src/core/mod.rs index 11b5682..4407748 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -17,8 +17,7 @@ use crate::{ use oauth2::ResponseType as OAuth2ResponseType; use serde::{Deserialize, Serialize}; -use std::fmt::{Display, Error as FormatterError, Formatter}; -use std::ops::Deref; +use std::fmt::Display; pub use crate::core::jwk::{ CoreEdDsaPrivateSigningKey, CoreHmacKey, CoreJsonWebKey, CoreJsonWebKeyType, CoreJsonWebKeyUse, @@ -290,7 +289,7 @@ impl AsRef for CoreAuthDisplay { } impl AuthDisplay for CoreAuthDisplay {} impl Display for CoreAuthDisplay { - fn fmt(&self, f: &mut Formatter) -> Result<(), FormatterError> { + fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { write!(f, "{}", self.as_ref()) } } @@ -353,7 +352,7 @@ impl AsRef for CoreAuthPrompt { impl AuthPrompt for CoreAuthPrompt {} impl Display for CoreAuthPrompt { - fn fmt(&self, f: &mut Formatter) -> Result<(), FormatterError> { + fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { write!(f, "{}", self.as_ref()) } } @@ -892,7 +891,7 @@ impl AsRef for CoreRegisterErrorResponseType { impl ErrorResponseType for CoreRegisterErrorResponseType {} impl RegisterErrorResponseType for CoreRegisterErrorResponseType {} impl Display for CoreRegisterErrorResponseType { - fn fmt(&self, f: &mut Formatter) -> Result<(), FormatterError> { + fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { write!(f, "{}", self.as_ref()) } } diff --git a/src/discovery.rs b/src/discovery.rs deleted file mode 100644 index baf693b..0000000 --- a/src/discovery.rs +++ /dev/null @@ -1,1591 +0,0 @@ -use crate::http_utils::{check_content_type, MIME_TYPE_JSON}; -use crate::types::{ - AuthDisplay, AuthenticationContextClass, ClaimName, ClaimType, ClientAuthMethod, GrantType, - IssuerUrl, JsonWebKey, JsonWebKeySet, JsonWebKeySetUrl, JsonWebKeyType, JsonWebKeyUse, - JweContentEncryptionAlgorithm, JweKeyManagementAlgorithm, JwsSigningAlgorithm, LanguageTag, - OpPolicyUrl, OpTosUrl, RegistrationUrl, ResponseMode, ResponseType, ResponseTypes, - ServiceDocUrl, SubjectIdentifierType, -}; -use crate::{AuthUrl, HttpRequest, HttpResponse, Scope, TokenUrl, UserInfoUrl, CONFIG_URL_SUFFIX}; - -use http::header::{HeaderValue, ACCEPT}; -use http::method::Method; -use http::status::StatusCode; -use serde::de::DeserializeOwned; -use serde::{Deserialize, Serialize}; -use serde_with::{serde_as, skip_serializing_none, VecSkipError}; -use thiserror::Error; - -use std::fmt::Debug; -use std::future::Future; -use std::marker::PhantomData; - -/// Trait for adding extra fields to [`ProviderMetadata`]. -pub trait AdditionalProviderMetadata: Clone + Debug + DeserializeOwned + Serialize {} - -// In order to support serde flatten, this must be an empty struct rather than an empty -// tuple struct. -/// Empty (default) extra [`ProviderMetadata`] fields. -#[derive(Clone, Debug, Default, Deserialize, PartialEq, Eq, Serialize)] -pub struct EmptyAdditionalProviderMetadata {} -impl AdditionalProviderMetadata for EmptyAdditionalProviderMetadata {} - -/// Provider metadata returned by [OpenID Connect Discovery]( -/// https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata). -#[serde_as] -#[skip_serializing_none] -#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)] -#[allow(clippy::type_complexity)] -pub struct ProviderMetadata -where - A: AdditionalProviderMetadata, - AD: AuthDisplay, - CA: ClientAuthMethod, - CN: ClaimName, - CT: ClaimType, - G: GrantType, - JE: JweContentEncryptionAlgorithm, - JK: JweKeyManagementAlgorithm, - JS: JwsSigningAlgorithm, - JT: JsonWebKeyType, - JU: JsonWebKeyUse, - K: JsonWebKey, - RM: ResponseMode, - RT: ResponseType, - S: SubjectIdentifierType, -{ - issuer: IssuerUrl, - authorization_endpoint: AuthUrl, - token_endpoint: Option, - userinfo_endpoint: Option, - jwks_uri: JsonWebKeySetUrl, - #[serde(default = "JsonWebKeySet::default", skip)] - jwks: JsonWebKeySet, - registration_endpoint: Option, - scopes_supported: Option>, - #[serde(bound(deserialize = "RT: ResponseType"))] - response_types_supported: Vec>, - #[serde(bound(deserialize = "RM: ResponseMode"))] - response_modes_supported: Option>, - #[serde(bound(deserialize = "G: GrantType"))] - grant_types_supported: Option>, - acr_values_supported: Option>, - #[serde(bound(deserialize = "S: SubjectIdentifierType"))] - subject_types_supported: Vec, - #[serde(bound(deserialize = "JS: JwsSigningAlgorithm"))] - #[serde_as(as = "VecSkipError<_>")] - id_token_signing_alg_values_supported: Vec, - #[serde( - bound(deserialize = "JK: JweKeyManagementAlgorithm"), - default = "Option::default" - )] - #[serde_as(as = "Option>")] - id_token_encryption_alg_values_supported: Option>, - #[serde( - bound(deserialize = "JE: JweContentEncryptionAlgorithm"), - default = "Option::default" - )] - #[serde_as(as = "Option>")] - id_token_encryption_enc_values_supported: Option>, - #[serde( - bound(deserialize = "JS: JwsSigningAlgorithm"), - default = "Option::default" - )] - #[serde_as(as = "Option>")] - userinfo_signing_alg_values_supported: Option>, - #[serde( - bound(deserialize = "JK: JweKeyManagementAlgorithm"), - default = "Option::default" - )] - #[serde_as(as = "Option>")] - userinfo_encryption_alg_values_supported: Option>, - #[serde( - bound(deserialize = "JE: JweContentEncryptionAlgorithm"), - default = "Option::default" - )] - #[serde_as(as = "Option>")] - userinfo_encryption_enc_values_supported: Option>, - #[serde( - bound(deserialize = "JS: JwsSigningAlgorithm"), - default = "Option::default" - )] - #[serde_as(as = "Option>")] - request_object_signing_alg_values_supported: Option>, - #[serde( - bound(deserialize = "JK: JweKeyManagementAlgorithm"), - default = "Option::default" - )] - #[serde_as(as = "Option>")] - request_object_encryption_alg_values_supported: Option>, - #[serde( - bound(deserialize = "JE: JweContentEncryptionAlgorithm"), - default = "Option::default" - )] - #[serde_as(as = "Option>")] - request_object_encryption_enc_values_supported: Option>, - #[serde(bound(deserialize = "CA: ClientAuthMethod"))] - token_endpoint_auth_methods_supported: Option>, - #[serde( - bound(deserialize = "JS: JwsSigningAlgorithm"), - default = "Option::default" - )] - #[serde_as(as = "Option>")] - token_endpoint_auth_signing_alg_values_supported: Option>, - #[serde(bound(deserialize = "AD: AuthDisplay"))] - display_values_supported: Option>, - #[serde(bound(deserialize = "CT: ClaimType"))] - claim_types_supported: Option>, - #[serde(bound(deserialize = "CN: ClaimName"))] - claims_supported: Option>, - service_documentation: Option, - claims_locales_supported: Option>, - ui_locales_supported: Option>, - claims_parameter_supported: Option, - request_parameter_supported: Option, - request_uri_parameter_supported: Option, - require_request_uri_registration: Option, - op_policy_uri: Option, - op_tos_uri: Option, - - #[serde(bound(deserialize = "A: AdditionalProviderMetadata"), flatten)] - additional_metadata: A, - - #[serde(skip)] - _phantom_jt: PhantomData, -} -impl - ProviderMetadata -where - A: AdditionalProviderMetadata, - AD: AuthDisplay, - CA: ClientAuthMethod, - CN: ClaimName, - CT: ClaimType, - G: GrantType, - JE: JweContentEncryptionAlgorithm, - JK: JweKeyManagementAlgorithm, - JS: JwsSigningAlgorithm, - JT: JsonWebKeyType, - JU: JsonWebKeyUse, - K: JsonWebKey, - RM: ResponseMode, - RT: ResponseType, - S: SubjectIdentifierType, -{ - /// Instantiates new provider metadata. - pub fn new( - issuer: IssuerUrl, - authorization_endpoint: AuthUrl, - jwks_uri: JsonWebKeySetUrl, - response_types_supported: Vec>, - subject_types_supported: Vec, - id_token_signing_alg_values_supported: Vec, - additional_metadata: A, - ) -> Self { - Self { - issuer, - authorization_endpoint, - token_endpoint: None, - userinfo_endpoint: None, - jwks_uri, - jwks: JsonWebKeySet::new(Vec::new()), - registration_endpoint: None, - scopes_supported: None, - response_types_supported, - response_modes_supported: None, - grant_types_supported: None, - acr_values_supported: None, - subject_types_supported, - id_token_signing_alg_values_supported, - id_token_encryption_alg_values_supported: None, - id_token_encryption_enc_values_supported: None, - userinfo_signing_alg_values_supported: None, - userinfo_encryption_alg_values_supported: None, - userinfo_encryption_enc_values_supported: None, - request_object_signing_alg_values_supported: None, - request_object_encryption_alg_values_supported: None, - request_object_encryption_enc_values_supported: None, - token_endpoint_auth_methods_supported: None, - token_endpoint_auth_signing_alg_values_supported: None, - display_values_supported: None, - claim_types_supported: None, - claims_supported: None, - service_documentation: None, - claims_locales_supported: None, - ui_locales_supported: None, - claims_parameter_supported: None, - request_parameter_supported: None, - request_uri_parameter_supported: None, - require_request_uri_registration: None, - op_policy_uri: None, - op_tos_uri: None, - additional_metadata, - _phantom_jt: PhantomData, - } - } - - field_getters_setters![ - pub self [self] ["provider metadata value"] { - set_issuer -> issuer[IssuerUrl], - set_authorization_endpoint -> authorization_endpoint[AuthUrl], - set_token_endpoint -> token_endpoint[Option], - set_userinfo_endpoint -> userinfo_endpoint[Option], - set_jwks_uri -> jwks_uri[JsonWebKeySetUrl], - set_jwks -> jwks[JsonWebKeySet], - set_registration_endpoint -> registration_endpoint[Option], - set_scopes_supported -> scopes_supported[Option>], - set_response_types_supported -> response_types_supported[Vec>], - set_response_modes_supported -> response_modes_supported[Option>], - set_grant_types_supported -> grant_types_supported[Option>], - set_acr_values_supported - -> acr_values_supported[Option>], - set_subject_types_supported -> subject_types_supported[Vec], - set_id_token_signing_alg_values_supported - -> id_token_signing_alg_values_supported[Vec], - set_id_token_encryption_alg_values_supported - -> id_token_encryption_alg_values_supported[Option>], - set_id_token_encryption_enc_values_supported - -> id_token_encryption_enc_values_supported[Option>], - set_userinfo_signing_alg_values_supported - -> userinfo_signing_alg_values_supported[Option>], - set_userinfo_encryption_alg_values_supported - -> userinfo_encryption_alg_values_supported[Option>], - set_userinfo_encryption_enc_values_supported - -> userinfo_encryption_enc_values_supported[Option>], - set_request_object_signing_alg_values_supported - -> request_object_signing_alg_values_supported[Option>], - set_request_object_encryption_alg_values_supported - -> request_object_encryption_alg_values_supported[Option>], - set_request_object_encryption_enc_values_supported - -> request_object_encryption_enc_values_supported[Option>], - set_token_endpoint_auth_methods_supported - -> token_endpoint_auth_methods_supported[Option>], - set_token_endpoint_auth_signing_alg_values_supported - -> token_endpoint_auth_signing_alg_values_supported[Option>], - set_display_values_supported -> display_values_supported[Option>], - set_claim_types_supported -> claim_types_supported[Option>], - set_claims_supported -> claims_supported[Option>], - set_service_documentation -> service_documentation[Option], - set_claims_locales_supported -> claims_locales_supported[Option>], - set_ui_locales_supported -> ui_locales_supported[Option>], - set_claims_parameter_supported -> claims_parameter_supported[Option], - set_request_parameter_supported -> request_parameter_supported[Option], - set_request_uri_parameter_supported -> request_uri_parameter_supported[Option], - set_require_request_uri_registration -> require_request_uri_registration[Option], - set_op_policy_uri -> op_policy_uri[Option], - set_op_tos_uri -> op_tos_uri[Option], - } - ]; - - /// Fetches the OpenID Connect Discovery document and associated JSON Web Key Set from the - /// OpenID Connect Provider. - pub fn discover( - issuer_url: &IssuerUrl, - http_client: HC, - ) -> Result> - where - HC: Fn(HttpRequest) -> Result, - RE: std::error::Error + 'static, - { - let discovery_url = issuer_url - .join(CONFIG_URL_SUFFIX) - .map_err(DiscoveryError::UrlParse)?; - - http_client(Self::discovery_request(discovery_url.clone())) - .map_err(DiscoveryError::Request) - .and_then(|http_response| { - Self::discovery_response(issuer_url, &discovery_url, http_response) - }) - .and_then(|provider_metadata| { - JsonWebKeySet::fetch(provider_metadata.jwks_uri(), http_client).map(|jwks| Self { - jwks, - ..provider_metadata - }) - }) - } - - /// Asynchronously fetches the OpenID Connect Discovery document and associated JSON Web Key Set - /// from the OpenID Connect Provider. - pub async fn discover_async( - issuer_url: IssuerUrl, - http_client: HC, - ) -> Result> - where - F: Future>, - HC: Fn(HttpRequest) -> F, - RE: std::error::Error + 'static, - { - let discovery_url = issuer_url - .join(CONFIG_URL_SUFFIX) - .map_err(DiscoveryError::UrlParse)?; - - let provider_metadata = http_client(Self::discovery_request(discovery_url.clone())) - .await - .map_err(DiscoveryError::Request) - .and_then(|http_response| { - Self::discovery_response(&issuer_url, &discovery_url, http_response) - })?; - - JsonWebKeySet::fetch_async(provider_metadata.jwks_uri(), http_client) - .await - .map(|jwks| Self { - jwks, - ..provider_metadata - }) - } - - fn discovery_request(discovery_url: url::Url) -> HttpRequest { - HttpRequest { - url: discovery_url, - method: Method::GET, - headers: vec![(ACCEPT, HeaderValue::from_static(MIME_TYPE_JSON))] - .into_iter() - .collect(), - body: Vec::new(), - } - } - - fn discovery_response( - issuer_url: &IssuerUrl, - discovery_url: &url::Url, - discovery_response: HttpResponse, - ) -> Result> - where - RE: std::error::Error + 'static, - { - if discovery_response.status_code != StatusCode::OK { - return Err(DiscoveryError::Response( - discovery_response.status_code, - discovery_response.body, - format!( - "HTTP status code {} at {}", - discovery_response.status_code, discovery_url - ), - )); - } - - check_content_type(&discovery_response.headers, MIME_TYPE_JSON).map_err(|err_msg| { - DiscoveryError::Response( - discovery_response.status_code, - discovery_response.body.clone(), - err_msg, - ) - })?; - - let provider_metadata = serde_path_to_error::deserialize::<_, Self>( - &mut serde_json::Deserializer::from_slice(&discovery_response.body), - ) - .map_err(DiscoveryError::Parse)?; - - if provider_metadata.issuer() != issuer_url { - Err(DiscoveryError::Validation(format!( - "unexpected issuer URI `{}` (expected `{}`)", - provider_metadata.issuer().as_str(), - issuer_url.as_str() - ))) - } else { - Ok(provider_metadata) - } - } - - /// Returns additional provider metadata fields. - pub fn additional_metadata(&self) -> &A { - &self.additional_metadata - } - /// Returns mutable additional provider metadata fields. - pub fn additional_metadata_mut(&mut self) -> &mut A { - &mut self.additional_metadata - } -} - -/// Error retrieving provider metadata. -#[derive(Debug, Error)] -#[non_exhaustive] -pub enum DiscoveryError -where - RE: std::error::Error + 'static, -{ - /// An unexpected error occurred. - #[error("Other error: {0}")] - Other(String), - /// Failed to parse server response. - #[error("Failed to parse server response")] - Parse(#[source] serde_path_to_error::Error), - /// An error occurred while sending the request or receiving the response (e.g., network - /// connectivity failed). - #[error("Request failed")] - Request(#[source] RE), - /// Server returned an invalid response. - #[error("Server returned invalid response: {2}")] - Response(StatusCode, Vec, String), - /// Failed to parse discovery URL from issuer URL. - #[error("Failed to parse URL")] - UrlParse(#[source] url::ParseError), - /// Failed to validate provider metadata. - #[error("Validation error: {0}")] - Validation(String), -} - -#[cfg(test)] -mod tests { - use crate::core::{ - CoreAuthDisplay, CoreClaimName, CoreClaimType, CoreClientAuthMethod, CoreGrantType, - CoreJweContentEncryptionAlgorithm, CoreJweKeyManagementAlgorithm, CoreJwsSigningAlgorithm, - CoreProviderMetadata, CoreResponseMode, CoreResponseType, CoreSubjectIdentifierType, - }; - use crate::{ - AuthUrl, AuthenticationContextClass, IssuerUrl, JsonWebKeySetUrl, LanguageTag, OpPolicyUrl, - OpTosUrl, RegistrationUrl, ResponseTypes, Scope, ServiceDocUrl, TokenUrl, UserInfoUrl, - }; - - #[test] - fn test_discovery_deserialization() { - // Fetched from: https://rp.certification.openid.net:8080/openidconnect-rs/ - // rp-response_type-code/.well-known/openid-configuration - let json_response_standard = "\ - \"issuer\":\"https://rp.certification.openid.net:8080/openidconnect-rs/rp-response_type-code\",\ - \"authorization_endpoint\":\"https://rp.certification.openid.net:8080/openidconnect-rs/rp-response_type-code/authorization\",\ - \"token_endpoint\":\"https://rp.certification.openid.net:8080/openidconnect-rs/rp-response_type-code/token\",\ - \"userinfo_endpoint\":\"https://rp.certification.openid.net:8080/openidconnect-rs/rp-response_type-code/userinfo\",\ - \"jwks_uri\":\"https://rp.certification.openid.net:8080/static/jwks_3INbZl52IrrPCp2j.json\",\ - \"registration_endpoint\":\"https://rp.certification.openid.net:8080/openidconnect-rs/rp-response_type-code/registration\",\ - \"scopes_supported\":[\ - \"email\",\ - \"phone\",\ - \"profile\",\ - \"openid\",\ - \"address\",\ - \"offline_access\",\ - \"openid\"\ - ],\ - \"response_types_supported\":[\ - \"code\"\ - ],\ - \"response_modes_supported\":[\ - \"query\",\ - \"fragment\",\ - \"form_post\"\ - ],\ - \"grant_types_supported\":[\ - \"authorization_code\",\ - \"implicit\",\ - \"urn:ietf:params:oauth:grant-type:jwt-bearer\",\ - \"refresh_token\"\ - ],\ - \"acr_values_supported\":[\ - \"PASSWORD\"\ - ],\ - \"subject_types_supported\":[\ - \"public\",\ - \"pairwise\"\ - ],\ - \"id_token_signing_alg_values_supported\":[\ - \"RS256\",\ - \"RS384\",\ - \"RS512\",\ - \"ES256\",\ - \"ES384\",\ - \"ES512\",\ - \"HS256\",\ - \"HS384\",\ - \"HS512\",\ - \"PS256\",\ - \"PS384\",\ - \"PS512\",\ - \"none\"\ - ],\ - \"id_token_encryption_alg_values_supported\":[\ - \"RSA1_5\",\ - \"RSA-OAEP\",\ - \"RSA-OAEP-256\",\ - \"A128KW\",\ - \"A192KW\",\ - \"A256KW\",\ - \"ECDH-ES\",\ - \"ECDH-ES+A128KW\",\ - \"ECDH-ES+A192KW\",\ - \"ECDH-ES+A256KW\"\ - ],\ - \"id_token_encryption_enc_values_supported\":[\ - \"A128CBC-HS256\",\ - \"A192CBC-HS384\",\ - \"A256CBC-HS512\",\ - \"A128GCM\",\ - \"A192GCM\",\ - \"A256GCM\"\ - ],\ - \"userinfo_signing_alg_values_supported\":[\ - \"RS256\",\ - \"RS384\",\ - \"RS512\",\ - \"ES256\",\ - \"ES384\",\ - \"ES512\",\ - \"HS256\",\ - \"HS384\",\ - \"HS512\",\ - \"PS256\",\ - \"PS384\",\ - \"PS512\",\ - \"none\"\ - ],\ - \"userinfo_encryption_alg_values_supported\":[\ - \"RSA1_5\",\ - \"RSA-OAEP\",\ - \"RSA-OAEP-256\",\ - \"A128KW\",\ - \"A192KW\",\ - \"A256KW\",\ - \"ECDH-ES\",\ - \"ECDH-ES+A128KW\",\ - \"ECDH-ES+A192KW\",\ - \"ECDH-ES+A256KW\"\ - ],\ - \"userinfo_encryption_enc_values_supported\":[\ - \"A128CBC-HS256\",\ - \"A192CBC-HS384\",\ - \"A256CBC-HS512\",\ - \"A128GCM\",\ - \"A192GCM\",\ - \"A256GCM\"\ - ],\ - \"request_object_signing_alg_values_supported\":[\ - \"RS256\",\ - \"RS384\",\ - \"RS512\",\ - \"ES256\",\ - \"ES384\",\ - \"ES512\",\ - \"HS256\",\ - \"HS384\",\ - \"HS512\",\ - \"PS256\",\ - \"PS384\",\ - \"PS512\",\ - \"none\"\ - ],\ - \"request_object_encryption_alg_values_supported\":[\ - \"RSA1_5\",\ - \"RSA-OAEP\",\ - \"RSA-OAEP-256\",\ - \"A128KW\",\ - \"A192KW\",\ - \"A256KW\",\ - \"ECDH-ES\",\ - \"ECDH-ES+A128KW\",\ - \"ECDH-ES+A192KW\",\ - \"ECDH-ES+A256KW\"\ - ],\ - \"request_object_encryption_enc_values_supported\":[\ - \"A128CBC-HS256\",\ - \"A192CBC-HS384\",\ - \"A256CBC-HS512\",\ - \"A128GCM\",\ - \"A192GCM\",\ - \"A256GCM\"\ - ],\ - \"token_endpoint_auth_methods_supported\":[\ - \"client_secret_post\",\ - \"client_secret_basic\",\ - \"client_secret_jwt\",\ - \"private_key_jwt\"\ - ],\ - \"token_endpoint_auth_signing_alg_values_supported\":[\ - \"RS256\",\ - \"RS384\",\ - \"RS512\",\ - \"ES256\",\ - \"ES384\",\ - \"ES512\",\ - \"HS256\",\ - \"HS384\",\ - \"HS512\",\ - \"PS256\",\ - \"PS384\",\ - \"PS512\"\ - ],\ - \"claim_types_supported\":[\ - \"normal\",\ - \"aggregated\",\ - \"distributed\"\ - ],\ - \"claims_supported\":[\ - \"name\",\ - \"given_name\",\ - \"middle_name\",\ - \"picture\",\ - \"email_verified\",\ - \"birthdate\",\ - \"sub\",\ - \"address\",\ - \"zoneinfo\",\ - \"email\",\ - \"gender\",\ - \"preferred_username\",\ - \"family_name\",\ - \"website\",\ - \"profile\",\ - \"phone_number_verified\",\ - \"nickname\",\ - \"updated_at\",\ - \"phone_number\",\ - \"locale\"\ - ],\ - \"claims_parameter_supported\":true,\ - \"request_parameter_supported\":true,\ - \"request_uri_parameter_supported\":true,\ - \"require_request_uri_registration\":true"; - - let json_response = format!( - "{{{},{}}}", - json_response_standard, - "\"end_session_endpoint\":\"https://rp.certification.openid.net:8080/openidconnect-rs/rp-response_type-code/end_session\",\ - \"version\":\"3.0\"" - ); - - let all_signing_algs = vec![ - CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256, - CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha384, - CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha512, - CoreJwsSigningAlgorithm::EcdsaP256Sha256, - CoreJwsSigningAlgorithm::EcdsaP384Sha384, - CoreJwsSigningAlgorithm::EcdsaP521Sha512, - CoreJwsSigningAlgorithm::HmacSha256, - CoreJwsSigningAlgorithm::HmacSha384, - CoreJwsSigningAlgorithm::HmacSha512, - CoreJwsSigningAlgorithm::RsaSsaPssSha256, - CoreJwsSigningAlgorithm::RsaSsaPssSha384, - CoreJwsSigningAlgorithm::RsaSsaPssSha512, - CoreJwsSigningAlgorithm::None, - ]; - let all_encryption_algs = vec![ - CoreJweKeyManagementAlgorithm::RsaPkcs1V15, - CoreJweKeyManagementAlgorithm::RsaOaep, - CoreJweKeyManagementAlgorithm::RsaOaepSha256, - CoreJweKeyManagementAlgorithm::AesKeyWrap128, - CoreJweKeyManagementAlgorithm::AesKeyWrap192, - CoreJweKeyManagementAlgorithm::AesKeyWrap256, - CoreJweKeyManagementAlgorithm::EcdhEs, - CoreJweKeyManagementAlgorithm::EcdhEsAesKeyWrap128, - CoreJweKeyManagementAlgorithm::EcdhEsAesKeyWrap192, - CoreJweKeyManagementAlgorithm::EcdhEsAesKeyWrap256, - ]; - let new_provider_metadata = CoreProviderMetadata::new( - IssuerUrl::new( - "https://rp.certification.openid.net:8080/openidconnect-rs/rp-response_type-code" - .to_string(), - ) - .unwrap(), - AuthUrl::new( - "https://rp.certification.openid.net:8080/openidconnect-rs/\ - rp-response_type-code/authorization" - .to_string(), - ) - .unwrap(), - JsonWebKeySetUrl::new( - "https://rp.certification.openid.net:8080/static/jwks_3INbZl52IrrPCp2j.json" - .to_string(), - ) - .unwrap(), - vec![ResponseTypes::new(vec![CoreResponseType::Code])], - vec![ - CoreSubjectIdentifierType::Public, - CoreSubjectIdentifierType::Pairwise, - ], - all_signing_algs.clone(), - Default::default(), - ) - .set_request_object_signing_alg_values_supported(Some(all_signing_algs.clone())) - .set_token_endpoint_auth_signing_alg_values_supported(Some(vec![ - CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256, - CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha384, - CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha512, - CoreJwsSigningAlgorithm::EcdsaP256Sha256, - CoreJwsSigningAlgorithm::EcdsaP384Sha384, - CoreJwsSigningAlgorithm::EcdsaP521Sha512, - CoreJwsSigningAlgorithm::HmacSha256, - CoreJwsSigningAlgorithm::HmacSha384, - CoreJwsSigningAlgorithm::HmacSha512, - CoreJwsSigningAlgorithm::RsaSsaPssSha256, - CoreJwsSigningAlgorithm::RsaSsaPssSha384, - CoreJwsSigningAlgorithm::RsaSsaPssSha512, - ])) - .set_scopes_supported(Some(vec![ - Scope::new("email".to_string()), - Scope::new("phone".to_string()), - Scope::new("profile".to_string()), - Scope::new("openid".to_string()), - Scope::new("address".to_string()), - Scope::new("offline_access".to_string()), - Scope::new("openid".to_string()), - ])) - .set_userinfo_signing_alg_values_supported(Some(all_signing_algs)) - .set_id_token_encryption_enc_values_supported(Some(vec![ - CoreJweContentEncryptionAlgorithm::Aes128CbcHmacSha256, - CoreJweContentEncryptionAlgorithm::Aes192CbcHmacSha384, - CoreJweContentEncryptionAlgorithm::Aes256CbcHmacSha512, - CoreJweContentEncryptionAlgorithm::Aes128Gcm, - CoreJweContentEncryptionAlgorithm::Aes192Gcm, - CoreJweContentEncryptionAlgorithm::Aes256Gcm, - ])) - .set_grant_types_supported(Some(vec![ - CoreGrantType::AuthorizationCode, - CoreGrantType::Implicit, - CoreGrantType::JwtBearer, - CoreGrantType::RefreshToken, - ])) - .set_response_modes_supported(Some(vec![ - CoreResponseMode::Query, - CoreResponseMode::Fragment, - CoreResponseMode::FormPost, - ])) - .set_require_request_uri_registration(Some(true)) - .set_registration_endpoint(Some( - RegistrationUrl::new( - "https://rp.certification.openid.net:8080/openidconnect-rs/\ - rp-response_type-code/registration" - .to_string(), - ) - .unwrap(), - )) - .set_claims_parameter_supported(Some(true)) - .set_request_object_encryption_enc_values_supported(Some(vec![ - CoreJweContentEncryptionAlgorithm::Aes128CbcHmacSha256, - CoreJweContentEncryptionAlgorithm::Aes192CbcHmacSha384, - CoreJweContentEncryptionAlgorithm::Aes256CbcHmacSha512, - CoreJweContentEncryptionAlgorithm::Aes128Gcm, - CoreJweContentEncryptionAlgorithm::Aes192Gcm, - CoreJweContentEncryptionAlgorithm::Aes256Gcm, - ])) - .set_userinfo_endpoint(Some( - UserInfoUrl::new( - "https://rp.certification.openid.net:8080/openidconnect-rs/\ - rp-response_type-code/userinfo" - .to_string(), - ) - .unwrap(), - )) - .set_token_endpoint_auth_methods_supported(Some(vec![ - CoreClientAuthMethod::ClientSecretPost, - CoreClientAuthMethod::ClientSecretBasic, - CoreClientAuthMethod::ClientSecretJwt, - CoreClientAuthMethod::PrivateKeyJwt, - ])) - .set_claims_supported(Some( - vec![ - "name", - "given_name", - "middle_name", - "picture", - "email_verified", - "birthdate", - "sub", - "address", - "zoneinfo", - "email", - "gender", - "preferred_username", - "family_name", - "website", - "profile", - "phone_number_verified", - "nickname", - "updated_at", - "phone_number", - "locale", - ] - .iter() - .map(|claim| CoreClaimName::new((*claim).to_string())) - .collect(), - )) - .set_request_object_encryption_alg_values_supported(Some(all_encryption_algs.clone())) - .set_claim_types_supported(Some(vec![ - CoreClaimType::Normal, - CoreClaimType::Aggregated, - CoreClaimType::Distributed, - ])) - .set_request_uri_parameter_supported(Some(true)) - .set_request_parameter_supported(Some(true)) - .set_token_endpoint(Some( - TokenUrl::new( - "https://rp.certification.openid.net:8080/openidconnect-rs/\ - rp-response_type-code/token" - .to_string(), - ) - .unwrap(), - )) - .set_id_token_encryption_alg_values_supported(Some(all_encryption_algs.clone())) - .set_userinfo_encryption_alg_values_supported(Some(all_encryption_algs)) - .set_userinfo_encryption_enc_values_supported(Some(vec![ - CoreJweContentEncryptionAlgorithm::Aes128CbcHmacSha256, - CoreJweContentEncryptionAlgorithm::Aes192CbcHmacSha384, - CoreJweContentEncryptionAlgorithm::Aes256CbcHmacSha512, - CoreJweContentEncryptionAlgorithm::Aes128Gcm, - CoreJweContentEncryptionAlgorithm::Aes192Gcm, - CoreJweContentEncryptionAlgorithm::Aes256Gcm, - ])) - .set_acr_values_supported(Some(vec![AuthenticationContextClass::new( - "PASSWORD".to_string(), - )])); - - let provider_metadata: CoreProviderMetadata = serde_json::from_str(&json_response).unwrap(); - assert_eq!(provider_metadata, new_provider_metadata); - - let serialized = serde_json::to_string(&provider_metadata).unwrap(); - assert_eq!(serialized, format!("{{{}}}", json_response_standard)); - - assert_eq!( - IssuerUrl::new( - "https://rp.certification.openid.net:8080/openidconnect-rs/rp-response_type-code" - .to_string() - ) - .unwrap(), - *provider_metadata.issuer() - ); - assert_eq!( - AuthUrl::new( - "https://rp.certification.openid.net:8080/openidconnect-rs/rp-response_type-code\ - /authorization" - .to_string() - ) - .unwrap(), - *provider_metadata.authorization_endpoint() - ); - assert_eq!( - Some( - &TokenUrl::new( - "https://rp.certification.openid.net:8080/openidconnect-rs\ - /rp-response_type-code/token" - .to_string() - ) - .unwrap() - ), - provider_metadata.token_endpoint() - ); - assert_eq!( - Some( - &UserInfoUrl::new( - "https://rp.certification.openid.net:8080/openidconnect-rs\ - /rp-response_type-code/userinfo" - .to_string() - ) - .unwrap() - ), - provider_metadata.userinfo_endpoint() - ); - assert_eq!( - &JsonWebKeySetUrl::new( - "https://rp.certification.openid.net:8080/static/jwks_3INbZl52IrrPCp2j.json" - .to_string() - ) - .unwrap(), - provider_metadata.jwks_uri() - ); - assert_eq!( - Some( - &RegistrationUrl::new( - "https://rp.certification.openid.net:8080/openidconnect-rs\ - /rp-response_type-code/registration" - .to_string() - ) - .unwrap() - ), - provider_metadata.registration_endpoint() - ); - assert_eq!( - Some( - &[ - "email", - "phone", - "profile", - "openid", - "address", - "offline_access", - "openid", - ] - .iter() - .map(|s| (*s).to_string()) - .map(Scope::new) - .collect::>() - ), - provider_metadata.scopes_supported() - ); - assert_eq!( - vec![ResponseTypes::new(vec![CoreResponseType::Code])], - *provider_metadata.response_types_supported() - ); - assert_eq!( - Some(&vec![ - CoreResponseMode::Query, - CoreResponseMode::Fragment, - CoreResponseMode::FormPost, - ]), - provider_metadata.response_modes_supported() - ); - assert_eq!( - Some( - &vec![ - CoreGrantType::AuthorizationCode, - CoreGrantType::Implicit, - CoreGrantType::JwtBearer, - CoreGrantType::RefreshToken, - ] - .into_iter() - .collect::>() - ), - provider_metadata.grant_types_supported() - ); - assert_eq!( - Some(&vec![AuthenticationContextClass::new( - "PASSWORD".to_string(), - )]), - provider_metadata.acr_values_supported() - ); - assert_eq!( - vec![ - CoreSubjectIdentifierType::Public, - CoreSubjectIdentifierType::Pairwise, - ], - *provider_metadata.subject_types_supported() - ); - assert_eq!( - vec![ - CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256, - CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha384, - CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha512, - CoreJwsSigningAlgorithm::EcdsaP256Sha256, - CoreJwsSigningAlgorithm::EcdsaP384Sha384, - CoreJwsSigningAlgorithm::EcdsaP521Sha512, - CoreJwsSigningAlgorithm::HmacSha256, - CoreJwsSigningAlgorithm::HmacSha384, - CoreJwsSigningAlgorithm::HmacSha512, - CoreJwsSigningAlgorithm::RsaSsaPssSha256, - CoreJwsSigningAlgorithm::RsaSsaPssSha384, - CoreJwsSigningAlgorithm::RsaSsaPssSha512, - CoreJwsSigningAlgorithm::None, - ], - *provider_metadata.id_token_signing_alg_values_supported() - ); - assert_eq!( - Some(&vec![ - CoreJweKeyManagementAlgorithm::RsaPkcs1V15, - CoreJweKeyManagementAlgorithm::RsaOaep, - CoreJweKeyManagementAlgorithm::RsaOaepSha256, - CoreJweKeyManagementAlgorithm::AesKeyWrap128, - CoreJweKeyManagementAlgorithm::AesKeyWrap192, - CoreJweKeyManagementAlgorithm::AesKeyWrap256, - CoreJweKeyManagementAlgorithm::EcdhEs, - CoreJweKeyManagementAlgorithm::EcdhEsAesKeyWrap128, - CoreJweKeyManagementAlgorithm::EcdhEsAesKeyWrap192, - CoreJweKeyManagementAlgorithm::EcdhEsAesKeyWrap256, - ]), - provider_metadata.id_token_encryption_alg_values_supported() - ); - assert_eq!( - Some(&vec![ - CoreJweContentEncryptionAlgorithm::Aes128CbcHmacSha256, - CoreJweContentEncryptionAlgorithm::Aes192CbcHmacSha384, - CoreJweContentEncryptionAlgorithm::Aes256CbcHmacSha512, - CoreJweContentEncryptionAlgorithm::Aes128Gcm, - CoreJweContentEncryptionAlgorithm::Aes192Gcm, - CoreJweContentEncryptionAlgorithm::Aes256Gcm, - ]), - provider_metadata.id_token_encryption_enc_values_supported() - ); - assert_eq!( - Some(&vec![ - CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256, - CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha384, - CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha512, - CoreJwsSigningAlgorithm::EcdsaP256Sha256, - CoreJwsSigningAlgorithm::EcdsaP384Sha384, - CoreJwsSigningAlgorithm::EcdsaP521Sha512, - CoreJwsSigningAlgorithm::HmacSha256, - CoreJwsSigningAlgorithm::HmacSha384, - CoreJwsSigningAlgorithm::HmacSha512, - CoreJwsSigningAlgorithm::RsaSsaPssSha256, - CoreJwsSigningAlgorithm::RsaSsaPssSha384, - CoreJwsSigningAlgorithm::RsaSsaPssSha512, - CoreJwsSigningAlgorithm::None, - ]), - provider_metadata.userinfo_signing_alg_values_supported() - ); - assert_eq!( - Some(&vec![ - CoreJweKeyManagementAlgorithm::RsaPkcs1V15, - CoreJweKeyManagementAlgorithm::RsaOaep, - CoreJweKeyManagementAlgorithm::RsaOaepSha256, - CoreJweKeyManagementAlgorithm::AesKeyWrap128, - CoreJweKeyManagementAlgorithm::AesKeyWrap192, - CoreJweKeyManagementAlgorithm::AesKeyWrap256, - CoreJweKeyManagementAlgorithm::EcdhEs, - CoreJweKeyManagementAlgorithm::EcdhEsAesKeyWrap128, - CoreJweKeyManagementAlgorithm::EcdhEsAesKeyWrap192, - CoreJweKeyManagementAlgorithm::EcdhEsAesKeyWrap256, - ]), - provider_metadata.userinfo_encryption_alg_values_supported() - ); - assert_eq!( - Some(&vec![ - CoreJweContentEncryptionAlgorithm::Aes128CbcHmacSha256, - CoreJweContentEncryptionAlgorithm::Aes192CbcHmacSha384, - CoreJweContentEncryptionAlgorithm::Aes256CbcHmacSha512, - CoreJweContentEncryptionAlgorithm::Aes128Gcm, - CoreJweContentEncryptionAlgorithm::Aes192Gcm, - CoreJweContentEncryptionAlgorithm::Aes256Gcm, - ]), - provider_metadata.userinfo_encryption_enc_values_supported() - ); - assert_eq!( - Some(&vec![ - CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256, - CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha384, - CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha512, - CoreJwsSigningAlgorithm::EcdsaP256Sha256, - CoreJwsSigningAlgorithm::EcdsaP384Sha384, - CoreJwsSigningAlgorithm::EcdsaP521Sha512, - CoreJwsSigningAlgorithm::HmacSha256, - CoreJwsSigningAlgorithm::HmacSha384, - CoreJwsSigningAlgorithm::HmacSha512, - CoreJwsSigningAlgorithm::RsaSsaPssSha256, - CoreJwsSigningAlgorithm::RsaSsaPssSha384, - CoreJwsSigningAlgorithm::RsaSsaPssSha512, - CoreJwsSigningAlgorithm::None, - ]), - provider_metadata.request_object_signing_alg_values_supported() - ); - assert_eq!( - Some(&vec![ - CoreJweKeyManagementAlgorithm::RsaPkcs1V15, - CoreJweKeyManagementAlgorithm::RsaOaep, - CoreJweKeyManagementAlgorithm::RsaOaepSha256, - CoreJweKeyManagementAlgorithm::AesKeyWrap128, - CoreJweKeyManagementAlgorithm::AesKeyWrap192, - CoreJweKeyManagementAlgorithm::AesKeyWrap256, - CoreJweKeyManagementAlgorithm::EcdhEs, - CoreJweKeyManagementAlgorithm::EcdhEsAesKeyWrap128, - CoreJweKeyManagementAlgorithm::EcdhEsAesKeyWrap192, - CoreJweKeyManagementAlgorithm::EcdhEsAesKeyWrap256, - ]), - provider_metadata.request_object_encryption_alg_values_supported() - ); - assert_eq!( - Some(&vec![ - CoreJweContentEncryptionAlgorithm::Aes128CbcHmacSha256, - CoreJweContentEncryptionAlgorithm::Aes192CbcHmacSha384, - CoreJweContentEncryptionAlgorithm::Aes256CbcHmacSha512, - CoreJweContentEncryptionAlgorithm::Aes128Gcm, - CoreJweContentEncryptionAlgorithm::Aes192Gcm, - CoreJweContentEncryptionAlgorithm::Aes256Gcm, - ]), - provider_metadata.request_object_encryption_enc_values_supported() - ); - assert_eq!( - Some(&vec![ - CoreClientAuthMethod::ClientSecretPost, - CoreClientAuthMethod::ClientSecretBasic, - CoreClientAuthMethod::ClientSecretJwt, - CoreClientAuthMethod::PrivateKeyJwt, - ]), - provider_metadata.token_endpoint_auth_methods_supported() - ); - assert_eq!( - Some(&vec![ - CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256, - CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha384, - CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha512, - CoreJwsSigningAlgorithm::EcdsaP256Sha256, - CoreJwsSigningAlgorithm::EcdsaP384Sha384, - CoreJwsSigningAlgorithm::EcdsaP521Sha512, - CoreJwsSigningAlgorithm::HmacSha256, - CoreJwsSigningAlgorithm::HmacSha384, - CoreJwsSigningAlgorithm::HmacSha512, - CoreJwsSigningAlgorithm::RsaSsaPssSha256, - CoreJwsSigningAlgorithm::RsaSsaPssSha384, - CoreJwsSigningAlgorithm::RsaSsaPssSha512, - ]), - provider_metadata.token_endpoint_auth_signing_alg_values_supported() - ); - assert_eq!(None, provider_metadata.display_values_supported()); - assert_eq!( - Some(&vec![ - CoreClaimType::Normal, - CoreClaimType::Aggregated, - CoreClaimType::Distributed, - ]), - provider_metadata.claim_types_supported() - ); - assert_eq!( - Some(&vec![ - CoreClaimName::new("name".to_string()), - CoreClaimName::new("given_name".to_string()), - CoreClaimName::new("middle_name".to_string()), - CoreClaimName::new("picture".to_string()), - CoreClaimName::new("email_verified".to_string()), - CoreClaimName::new("birthdate".to_string()), - CoreClaimName::new("sub".to_string()), - CoreClaimName::new("address".to_string()), - CoreClaimName::new("zoneinfo".to_string()), - CoreClaimName::new("email".to_string()), - CoreClaimName::new("gender".to_string()), - CoreClaimName::new("preferred_username".to_string()), - CoreClaimName::new("family_name".to_string()), - CoreClaimName::new("website".to_string()), - CoreClaimName::new("profile".to_string()), - CoreClaimName::new("phone_number_verified".to_string()), - CoreClaimName::new("nickname".to_string()), - CoreClaimName::new("updated_at".to_string()), - CoreClaimName::new("phone_number".to_string()), - CoreClaimName::new("locale".to_string()), - ]), - provider_metadata.claims_supported() - ); - assert_eq!(None, provider_metadata.service_documentation()); - assert_eq!(None, provider_metadata.claims_locales_supported()); - assert_eq!(None, provider_metadata.ui_locales_supported()); - assert_eq!(Some(true), provider_metadata.claims_parameter_supported()); - assert_eq!(Some(true), provider_metadata.request_parameter_supported()); - assert_eq!( - Some(true), - provider_metadata.request_uri_parameter_supported() - ); - assert_eq!( - Some(true), - provider_metadata.require_request_uri_registration() - ); - assert_eq!(None, provider_metadata.op_policy_uri()); - assert_eq!(None, provider_metadata.op_tos_uri()); - - // Note: the following fields provided by the response above are not part of the OpenID - // Connect Discovery 1.0 spec: - // - end_session_endpoint - // - version - - let serialized_json = serde_json::to_string(&provider_metadata).unwrap(); - - let redeserialized_metadata: CoreProviderMetadata = - serde_json::from_str(&serialized_json).unwrap(); - assert_eq!(provider_metadata, redeserialized_metadata); - } - - // Tests the fields missing from the example response in test_discovery_deserialization(). - #[test] - fn test_discovery_deserialization_other_fields() { - let json_response = "{ - \"issuer\" : \"https://rp.certification.openid.net:8080/openidconnect-rs/rp-response_type-code\", - \"authorization_endpoint\" : \"https://rp.certification.openid.net:8080/openidconnect-rs/rp-response_type-code/authorization\", - \"jwks_uri\" : \"https://rp.certification.openid.net:8080/static/jwks_oMXD5waO08Q1GEnv.json\", - \"response_types_supported\" : [ - \"code\", - \"code token\", - \"code id_token\", - \"id_token token\", - \"code id_token token\", - \"token id_token\", - \"token id_token code\", - \"id_token\", - \"token\" - ], - \"subject_types_supported\" : [ - \"public\", - \"pairwise\" - ], - \"id_token_signing_alg_values_supported\" : [ - \"HS256\", - \"HS384\", - \"HS512\" - ], - \"display_values_supported\" : [ - \"page\", - \"popup\", - \"touch\", - \"wap\" - ], - \"service_documentation\" : \"https://rp.certification.openid.net:8080/openidconnect-rs/rp-response_type-code/documentation\", - \"claims_locales_supported\" : [ - \"de\", - \"fr\", - \"de-CH-1901\" - ], - \"ui_locales_supported\" : [ - \"ja\", - \"sr-Latn\", - \"yue-HK\" - ], - \"op_policy_uri\" : \"https://rp.certification.openid.net:8080/openidconnect-rs/rp-response_type-code/op_policy\", - \"op_tos_uri\" : \"https://rp.certification.openid.net:8080/openidconnect-rs/rp-response_type-code/op_tos\" - }"; - - let provider_metadata: CoreProviderMetadata = serde_json::from_str(json_response).unwrap(); - - assert_eq!( - IssuerUrl::new( - "https://rp.certification.openid.net:8080/openidconnect-rs/rp-response_type-code" - .to_string() - ) - .unwrap(), - *provider_metadata.issuer() - ); - assert_eq!( - AuthUrl::new( - "https://rp.certification.openid.net:8080/openidconnect-rs/rp-response_type-code\ - /authorization" - .to_string() - ) - .unwrap(), - *provider_metadata.authorization_endpoint() - ); - assert_eq!(None, provider_metadata.token_endpoint()); - assert_eq!(None, provider_metadata.userinfo_endpoint()); - assert_eq!( - JsonWebKeySetUrl::new( - "https://rp.certification.openid.net:8080/static/jwks_oMXD5waO08Q1GEnv.json" - .to_string() - ) - .unwrap(), - *provider_metadata.jwks_uri() - ); - assert_eq!(None, provider_metadata.registration_endpoint()); - assert_eq!(None, provider_metadata.scopes_supported()); - assert_eq!( - vec![ - ResponseTypes::new(vec![CoreResponseType::Code]), - ResponseTypes::new(vec![CoreResponseType::Code, CoreResponseType::Token]), - ResponseTypes::new(vec![CoreResponseType::Code, CoreResponseType::IdToken]), - ResponseTypes::new(vec![CoreResponseType::IdToken, CoreResponseType::Token]), - ResponseTypes::new(vec![ - CoreResponseType::Code, - CoreResponseType::IdToken, - CoreResponseType::Token, - ]), - ResponseTypes::new(vec![CoreResponseType::Token, CoreResponseType::IdToken]), - ResponseTypes::new(vec![ - CoreResponseType::Token, - CoreResponseType::IdToken, - CoreResponseType::Code, - ]), - ResponseTypes::new(vec![CoreResponseType::IdToken]), - ResponseTypes::new(vec![CoreResponseType::Token]), - ], - *provider_metadata.response_types_supported() - ); - assert_eq!(None, provider_metadata.response_modes_supported()); - assert_eq!(None, provider_metadata.grant_types_supported()); - assert_eq!(None, provider_metadata.acr_values_supported()); - assert_eq!( - vec![ - CoreSubjectIdentifierType::Public, - CoreSubjectIdentifierType::Pairwise, - ], - *provider_metadata.subject_types_supported() - ); - assert_eq!( - vec![ - CoreJwsSigningAlgorithm::HmacSha256, - CoreJwsSigningAlgorithm::HmacSha384, - CoreJwsSigningAlgorithm::HmacSha512, - ], - *provider_metadata.id_token_signing_alg_values_supported() - ); - assert_eq!( - None, - provider_metadata.id_token_encryption_alg_values_supported() - ); - assert_eq!( - None, - provider_metadata.id_token_encryption_enc_values_supported() - ); - assert_eq!( - None, - provider_metadata.userinfo_signing_alg_values_supported() - ); - assert_eq!( - None, - provider_metadata.userinfo_encryption_alg_values_supported() - ); - assert_eq!( - None, - provider_metadata.userinfo_encryption_enc_values_supported() - ); - assert_eq!( - None, - provider_metadata.request_object_signing_alg_values_supported() - ); - assert_eq!( - None, - provider_metadata.request_object_encryption_alg_values_supported() - ); - assert_eq!( - None, - provider_metadata.request_object_encryption_enc_values_supported() - ); - assert_eq!( - None, - provider_metadata.token_endpoint_auth_methods_supported() - ); - assert_eq!( - None, - provider_metadata.token_endpoint_auth_signing_alg_values_supported() - ); - assert_eq!( - Some(&vec![ - CoreAuthDisplay::Page, - CoreAuthDisplay::Popup, - CoreAuthDisplay::Touch, - CoreAuthDisplay::Wap, - ]), - provider_metadata.display_values_supported() - ); - assert_eq!(None, provider_metadata.claim_types_supported()); - assert_eq!(None, provider_metadata.claims_supported()); - - assert_eq!( - Some( - &ServiceDocUrl::new( - "https://rp.certification.openid.net:8080/openidconnect-rs/rp-response_type-code\ - /documentation" - .to_string() - ) - .unwrap() - ), - provider_metadata.service_documentation() - ); - assert_eq!( - Some(&vec![ - LanguageTag::new("de".to_string()), - LanguageTag::new("fr".to_string()), - LanguageTag::new("de-CH-1901".to_string()), - ]), - provider_metadata.claims_locales_supported() - ); - assert_eq!( - Some(&vec![ - LanguageTag::new("ja".to_string()), - LanguageTag::new("sr-Latn".to_string()), - LanguageTag::new("yue-HK".to_string()), - ]), - provider_metadata.ui_locales_supported() - ); - assert_eq!(None, provider_metadata.claims_parameter_supported()); - assert_eq!(None, provider_metadata.request_parameter_supported()); - assert_eq!(None, provider_metadata.request_uri_parameter_supported()); - assert_eq!(None, provider_metadata.require_request_uri_registration()); - assert_eq!( - Some( - &OpPolicyUrl::new( - "https://rp.certification.openid.net:8080/openidconnect-rs/rp-response_type-code\ - /op_policy" - .to_string() - ) - .unwrap() - ), - provider_metadata.op_policy_uri() - ); - assert_eq!( - Some( - &OpTosUrl::new( - "https://rp.certification.openid.net:8080/openidconnect-rs/rp-response_type-code\ - /op_tos" - .to_string() - ) - .unwrap() - ), - provider_metadata.op_tos_uri() - ); - - let serialized_json = serde_json::to_string(&provider_metadata).unwrap(); - - let redeserialized_metadata: CoreProviderMetadata = - serde_json::from_str(&serialized_json).unwrap(); - assert_eq!(provider_metadata, redeserialized_metadata); - } - - // Tests that we ignore enum values that the OIDC provider supports but that the client does - // not (which trigger serde deserialization errors while parsing the provider metadata). - #[test] - fn test_unsupported_enum_values() { - let json_response = "{\ - \"issuer\":\"https://rp.certification.openid.net:8080/openidconnect-rs/rp-response_type-code\",\ - \"authorization_endpoint\":\"https://rp.certification.openid.net:8080/openidconnect-rs/rp-response_type-code/authorization\",\ - \"jwks_uri\":\"https://rp.certification.openid.net:8080/static/jwks_3INbZl52IrrPCp2j.json\",\ - \"response_types_supported\":[\ - \"code\"\ - ],\ - \"subject_types_supported\":[\ - \"public\",\ - \"pairwise\"\ - ],\ - \"id_token_signing_alg_values_supported\":[\ - \"RS256\",\ - \"MAGIC\",\ - \"none\"\ - ],\ - \"id_token_encryption_alg_values_supported\":[\ - \"RSA1_5\",\ - \"MAGIC\"\ - ],\ - \"id_token_encryption_enc_values_supported\":[\ - \"A128CBC-HS256\",\ - \"MAGIC\"\ - ],\ - \"userinfo_signing_alg_values_supported\":[\ - \"RS256\",\ - \"MAGIC\",\ - \"none\"\ - ],\ - \"userinfo_encryption_alg_values_supported\":[\ - \"RSA1_5\",\ - \"MAGIC\"\ - ],\ - \"userinfo_encryption_enc_values_supported\":[\ - \"A128CBC-HS256\",\ - \"MAGIC\"\ - ],\ - \"request_object_signing_alg_values_supported\":[\ - \"RS256\",\ - \"MAGIC\",\ - \"none\"\ - ],\ - \"request_object_encryption_alg_values_supported\":[\ - \"RSA1_5\",\ - \"MAGIC\"\ - ],\ - \"request_object_encryption_enc_values_supported\":[\ - \"A128CBC-HS256\",\ - \"MAGIC\"\ - ],\ - \"token_endpoint_auth_signing_alg_values_supported\":[\ - \"RS256\",\ - \"MAGIC\",\ - \"none\"\ - ]\ - }"; - - let provider_metadata: CoreProviderMetadata = serde_json::from_str(json_response).unwrap(); - - assert_eq!( - IssuerUrl::new( - "https://rp.certification.openid.net:8080/openidconnect-rs/rp-response_type-code" - .to_string() - ) - .unwrap(), - *provider_metadata.issuer() - ); - assert_eq!( - AuthUrl::new( - "https://rp.certification.openid.net:8080/openidconnect-rs/rp-response_type-code\ - /authorization" - .to_string() - ) - .unwrap(), - *provider_metadata.authorization_endpoint() - ); - assert_eq!(None, provider_metadata.token_endpoint()); - assert_eq!(None, provider_metadata.userinfo_endpoint()); - assert_eq!( - JsonWebKeySetUrl::new( - "https://rp.certification.openid.net:8080/static/jwks_3INbZl52IrrPCp2j.json" - .to_string() - ) - .unwrap(), - *provider_metadata.jwks_uri() - ); - assert_eq!(None, provider_metadata.registration_endpoint()); - assert_eq!(None, provider_metadata.scopes_supported()); - assert_eq!( - vec![ResponseTypes::new(vec![CoreResponseType::Code])], - *provider_metadata.response_types_supported() - ); - assert_eq!(None, provider_metadata.response_modes_supported()); - assert_eq!(None, provider_metadata.grant_types_supported()); - assert_eq!(None, provider_metadata.acr_values_supported()); - assert_eq!( - vec![ - CoreSubjectIdentifierType::Public, - CoreSubjectIdentifierType::Pairwise, - ], - *provider_metadata.subject_types_supported() - ); - assert_eq!( - vec![ - CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256, - CoreJwsSigningAlgorithm::None, - ], - *provider_metadata.id_token_signing_alg_values_supported() - ); - assert_eq!( - Some(&vec![CoreJweKeyManagementAlgorithm::RsaPkcs1V15]), - provider_metadata.id_token_encryption_alg_values_supported() - ); - assert_eq!( - Some(&vec![ - CoreJweContentEncryptionAlgorithm::Aes128CbcHmacSha256 - ]), - provider_metadata.id_token_encryption_enc_values_supported() - ); - assert_eq!( - Some(&vec![ - CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256, - CoreJwsSigningAlgorithm::None, - ]), - provider_metadata.userinfo_signing_alg_values_supported() - ); - assert_eq!( - Some(&vec![CoreJweKeyManagementAlgorithm::RsaPkcs1V15]), - provider_metadata.userinfo_encryption_alg_values_supported() - ); - assert_eq!( - Some(&vec![ - CoreJweContentEncryptionAlgorithm::Aes128CbcHmacSha256 - ]), - provider_metadata.userinfo_encryption_enc_values_supported() - ); - assert_eq!( - Some(&vec![ - CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256, - CoreJwsSigningAlgorithm::None, - ]), - provider_metadata.request_object_signing_alg_values_supported() - ); - assert_eq!( - Some(&vec![CoreJweKeyManagementAlgorithm::RsaPkcs1V15]), - provider_metadata.request_object_encryption_alg_values_supported() - ); - assert_eq!( - Some(&vec![ - CoreJweContentEncryptionAlgorithm::Aes128CbcHmacSha256 - ]), - provider_metadata.request_object_encryption_enc_values_supported() - ); - assert_eq!( - None, - provider_metadata.token_endpoint_auth_methods_supported() - ); - assert_eq!( - Some(&vec![ - CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256, - CoreJwsSigningAlgorithm::None, - ]), - provider_metadata.token_endpoint_auth_signing_alg_values_supported() - ); - assert_eq!(None, provider_metadata.display_values_supported()); - assert_eq!(None, provider_metadata.claim_types_supported()); - assert_eq!(None, provider_metadata.claims_supported()); - - assert_eq!(None, provider_metadata.service_documentation()); - assert_eq!(None, provider_metadata.claims_locales_supported()); - assert_eq!(None, provider_metadata.ui_locales_supported()); - assert_eq!(None, provider_metadata.claims_parameter_supported()); - assert_eq!(None, provider_metadata.request_parameter_supported()); - assert_eq!(None, provider_metadata.request_uri_parameter_supported()); - assert_eq!(None, provider_metadata.require_request_uri_registration()); - assert_eq!(None, provider_metadata.op_policy_uri()); - assert_eq!(None, provider_metadata.op_tos_uri()); - - let serialized_json = serde_json::to_string(&provider_metadata).unwrap(); - - let redeserialized_metadata: CoreProviderMetadata = - serde_json::from_str(&serialized_json).unwrap(); - assert_eq!(provider_metadata, redeserialized_metadata); - } -} diff --git a/src/discovery/mod.rs b/src/discovery/mod.rs new file mode 100644 index 0000000..e13c75c --- /dev/null +++ b/src/discovery/mod.rs @@ -0,0 +1,432 @@ +use crate::http_utils::{check_content_type, MIME_TYPE_JSON}; +use crate::{ + AuthDisplay, AuthUrl, AuthenticationContextClass, ClaimName, ClaimType, ClientAuthMethod, + GrantType, HttpRequest, HttpResponse, IssuerUrl, JsonWebKey, JsonWebKeySet, JsonWebKeySetUrl, + JsonWebKeyType, JsonWebKeyUse, JweContentEncryptionAlgorithm, JweKeyManagementAlgorithm, + JwsSigningAlgorithm, LanguageTag, OpPolicyUrl, OpTosUrl, RegistrationUrl, ResponseMode, + ResponseType, ResponseTypes, Scope, ServiceDocUrl, SubjectIdentifierType, TokenUrl, + UserInfoUrl, +}; + +use http::header::{HeaderValue, ACCEPT}; +use http::method::Method; +use http::status::StatusCode; +use serde::de::DeserializeOwned; +use serde::{Deserialize, Serialize}; +use serde_with::{serde_as, skip_serializing_none, VecSkipError}; +use thiserror::Error; + +use std::fmt::Debug; +use std::future::Future; +use std::marker::PhantomData; + +#[cfg(test)] +mod tests; + +const CONFIG_URL_SUFFIX: &str = ".well-known/openid-configuration"; + +/// Trait for adding extra fields to [`ProviderMetadata`]. +pub trait AdditionalProviderMetadata: Clone + Debug + DeserializeOwned + Serialize {} + +// In order to support serde flatten, this must be an empty struct rather than an empty +// tuple struct. +/// Empty (default) extra [`ProviderMetadata`] fields. +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Eq, Serialize)] +pub struct EmptyAdditionalProviderMetadata {} +impl AdditionalProviderMetadata for EmptyAdditionalProviderMetadata {} + +/// Provider metadata returned by [OpenID Connect Discovery]( +/// https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata). +#[serde_as] +#[skip_serializing_none] +#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)] +#[allow(clippy::type_complexity)] +pub struct ProviderMetadata +where + A: AdditionalProviderMetadata, + AD: AuthDisplay, + CA: ClientAuthMethod, + CN: ClaimName, + CT: ClaimType, + G: GrantType, + JE: JweContentEncryptionAlgorithm, + JK: JweKeyManagementAlgorithm, + JS: JwsSigningAlgorithm, + JT: JsonWebKeyType, + JU: JsonWebKeyUse, + K: JsonWebKey, + RM: ResponseMode, + RT: ResponseType, + S: SubjectIdentifierType, +{ + issuer: IssuerUrl, + authorization_endpoint: AuthUrl, + token_endpoint: Option, + userinfo_endpoint: Option, + jwks_uri: JsonWebKeySetUrl, + #[serde(default = "JsonWebKeySet::default", skip)] + jwks: JsonWebKeySet, + registration_endpoint: Option, + scopes_supported: Option>, + #[serde(bound(deserialize = "RT: ResponseType"))] + response_types_supported: Vec>, + #[serde(bound(deserialize = "RM: ResponseMode"))] + response_modes_supported: Option>, + #[serde(bound(deserialize = "G: GrantType"))] + grant_types_supported: Option>, + acr_values_supported: Option>, + #[serde(bound(deserialize = "S: SubjectIdentifierType"))] + subject_types_supported: Vec, + #[serde(bound(deserialize = "JS: JwsSigningAlgorithm"))] + #[serde_as(as = "VecSkipError<_>")] + id_token_signing_alg_values_supported: Vec, + #[serde( + bound(deserialize = "JK: JweKeyManagementAlgorithm"), + default = "Option::default" + )] + #[serde_as(as = "Option>")] + id_token_encryption_alg_values_supported: Option>, + #[serde( + bound(deserialize = "JE: JweContentEncryptionAlgorithm"), + default = "Option::default" + )] + #[serde_as(as = "Option>")] + id_token_encryption_enc_values_supported: Option>, + #[serde( + bound(deserialize = "JS: JwsSigningAlgorithm"), + default = "Option::default" + )] + #[serde_as(as = "Option>")] + userinfo_signing_alg_values_supported: Option>, + #[serde( + bound(deserialize = "JK: JweKeyManagementAlgorithm"), + default = "Option::default" + )] + #[serde_as(as = "Option>")] + userinfo_encryption_alg_values_supported: Option>, + #[serde( + bound(deserialize = "JE: JweContentEncryptionAlgorithm"), + default = "Option::default" + )] + #[serde_as(as = "Option>")] + userinfo_encryption_enc_values_supported: Option>, + #[serde( + bound(deserialize = "JS: JwsSigningAlgorithm"), + default = "Option::default" + )] + #[serde_as(as = "Option>")] + request_object_signing_alg_values_supported: Option>, + #[serde( + bound(deserialize = "JK: JweKeyManagementAlgorithm"), + default = "Option::default" + )] + #[serde_as(as = "Option>")] + request_object_encryption_alg_values_supported: Option>, + #[serde( + bound(deserialize = "JE: JweContentEncryptionAlgorithm"), + default = "Option::default" + )] + #[serde_as(as = "Option>")] + request_object_encryption_enc_values_supported: Option>, + #[serde(bound(deserialize = "CA: ClientAuthMethod"))] + token_endpoint_auth_methods_supported: Option>, + #[serde( + bound(deserialize = "JS: JwsSigningAlgorithm"), + default = "Option::default" + )] + #[serde_as(as = "Option>")] + token_endpoint_auth_signing_alg_values_supported: Option>, + #[serde(bound(deserialize = "AD: AuthDisplay"))] + display_values_supported: Option>, + #[serde(bound(deserialize = "CT: ClaimType"))] + claim_types_supported: Option>, + #[serde(bound(deserialize = "CN: ClaimName"))] + claims_supported: Option>, + service_documentation: Option, + claims_locales_supported: Option>, + ui_locales_supported: Option>, + claims_parameter_supported: Option, + request_parameter_supported: Option, + request_uri_parameter_supported: Option, + require_request_uri_registration: Option, + op_policy_uri: Option, + op_tos_uri: Option, + + #[serde(bound(deserialize = "A: AdditionalProviderMetadata"), flatten)] + additional_metadata: A, + + #[serde(skip)] + _phantom_jt: PhantomData, +} +impl + ProviderMetadata +where + A: AdditionalProviderMetadata, + AD: AuthDisplay, + CA: ClientAuthMethod, + CN: ClaimName, + CT: ClaimType, + G: GrantType, + JE: JweContentEncryptionAlgorithm, + JK: JweKeyManagementAlgorithm, + JS: JwsSigningAlgorithm, + JT: JsonWebKeyType, + JU: JsonWebKeyUse, + K: JsonWebKey, + RM: ResponseMode, + RT: ResponseType, + S: SubjectIdentifierType, +{ + /// Instantiates new provider metadata. + pub fn new( + issuer: IssuerUrl, + authorization_endpoint: AuthUrl, + jwks_uri: JsonWebKeySetUrl, + response_types_supported: Vec>, + subject_types_supported: Vec, + id_token_signing_alg_values_supported: Vec, + additional_metadata: A, + ) -> Self { + Self { + issuer, + authorization_endpoint, + token_endpoint: None, + userinfo_endpoint: None, + jwks_uri, + jwks: JsonWebKeySet::new(Vec::new()), + registration_endpoint: None, + scopes_supported: None, + response_types_supported, + response_modes_supported: None, + grant_types_supported: None, + acr_values_supported: None, + subject_types_supported, + id_token_signing_alg_values_supported, + id_token_encryption_alg_values_supported: None, + id_token_encryption_enc_values_supported: None, + userinfo_signing_alg_values_supported: None, + userinfo_encryption_alg_values_supported: None, + userinfo_encryption_enc_values_supported: None, + request_object_signing_alg_values_supported: None, + request_object_encryption_alg_values_supported: None, + request_object_encryption_enc_values_supported: None, + token_endpoint_auth_methods_supported: None, + token_endpoint_auth_signing_alg_values_supported: None, + display_values_supported: None, + claim_types_supported: None, + claims_supported: None, + service_documentation: None, + claims_locales_supported: None, + ui_locales_supported: None, + claims_parameter_supported: None, + request_parameter_supported: None, + request_uri_parameter_supported: None, + require_request_uri_registration: None, + op_policy_uri: None, + op_tos_uri: None, + additional_metadata, + _phantom_jt: PhantomData, + } + } + + field_getters_setters![ + pub self [self] ["provider metadata value"] { + set_issuer -> issuer[IssuerUrl], + set_authorization_endpoint -> authorization_endpoint[AuthUrl], + set_token_endpoint -> token_endpoint[Option], + set_userinfo_endpoint -> userinfo_endpoint[Option], + set_jwks_uri -> jwks_uri[JsonWebKeySetUrl], + set_jwks -> jwks[JsonWebKeySet], + set_registration_endpoint -> registration_endpoint[Option], + set_scopes_supported -> scopes_supported[Option>], + set_response_types_supported -> response_types_supported[Vec>], + set_response_modes_supported -> response_modes_supported[Option>], + set_grant_types_supported -> grant_types_supported[Option>], + set_acr_values_supported + -> acr_values_supported[Option>], + set_subject_types_supported -> subject_types_supported[Vec], + set_id_token_signing_alg_values_supported + -> id_token_signing_alg_values_supported[Vec], + set_id_token_encryption_alg_values_supported + -> id_token_encryption_alg_values_supported[Option>], + set_id_token_encryption_enc_values_supported + -> id_token_encryption_enc_values_supported[Option>], + set_userinfo_signing_alg_values_supported + -> userinfo_signing_alg_values_supported[Option>], + set_userinfo_encryption_alg_values_supported + -> userinfo_encryption_alg_values_supported[Option>], + set_userinfo_encryption_enc_values_supported + -> userinfo_encryption_enc_values_supported[Option>], + set_request_object_signing_alg_values_supported + -> request_object_signing_alg_values_supported[Option>], + set_request_object_encryption_alg_values_supported + -> request_object_encryption_alg_values_supported[Option>], + set_request_object_encryption_enc_values_supported + -> request_object_encryption_enc_values_supported[Option>], + set_token_endpoint_auth_methods_supported + -> token_endpoint_auth_methods_supported[Option>], + set_token_endpoint_auth_signing_alg_values_supported + -> token_endpoint_auth_signing_alg_values_supported[Option>], + set_display_values_supported -> display_values_supported[Option>], + set_claim_types_supported -> claim_types_supported[Option>], + set_claims_supported -> claims_supported[Option>], + set_service_documentation -> service_documentation[Option], + set_claims_locales_supported -> claims_locales_supported[Option>], + set_ui_locales_supported -> ui_locales_supported[Option>], + set_claims_parameter_supported -> claims_parameter_supported[Option], + set_request_parameter_supported -> request_parameter_supported[Option], + set_request_uri_parameter_supported -> request_uri_parameter_supported[Option], + set_require_request_uri_registration -> require_request_uri_registration[Option], + set_op_policy_uri -> op_policy_uri[Option], + set_op_tos_uri -> op_tos_uri[Option], + } + ]; + + /// Fetches the OpenID Connect Discovery document and associated JSON Web Key Set from the + /// OpenID Connect Provider. + pub fn discover( + issuer_url: &IssuerUrl, + http_client: HC, + ) -> Result> + where + HC: Fn(HttpRequest) -> Result, + RE: std::error::Error + 'static, + { + let discovery_url = issuer_url + .join(CONFIG_URL_SUFFIX) + .map_err(DiscoveryError::UrlParse)?; + + http_client(Self::discovery_request(discovery_url.clone())) + .map_err(DiscoveryError::Request) + .and_then(|http_response| { + Self::discovery_response(issuer_url, &discovery_url, http_response) + }) + .and_then(|provider_metadata| { + JsonWebKeySet::fetch(provider_metadata.jwks_uri(), http_client).map(|jwks| Self { + jwks, + ..provider_metadata + }) + }) + } + + /// Asynchronously fetches the OpenID Connect Discovery document and associated JSON Web Key Set + /// from the OpenID Connect Provider. + pub async fn discover_async( + issuer_url: IssuerUrl, + http_client: HC, + ) -> Result> + where + F: Future>, + HC: Fn(HttpRequest) -> F, + RE: std::error::Error + 'static, + { + let discovery_url = issuer_url + .join(CONFIG_URL_SUFFIX) + .map_err(DiscoveryError::UrlParse)?; + + let provider_metadata = http_client(Self::discovery_request(discovery_url.clone())) + .await + .map_err(DiscoveryError::Request) + .and_then(|http_response| { + Self::discovery_response(&issuer_url, &discovery_url, http_response) + })?; + + JsonWebKeySet::fetch_async(provider_metadata.jwks_uri(), http_client) + .await + .map(|jwks| Self { + jwks, + ..provider_metadata + }) + } + + fn discovery_request(discovery_url: url::Url) -> HttpRequest { + HttpRequest { + url: discovery_url, + method: Method::GET, + headers: vec![(ACCEPT, HeaderValue::from_static(MIME_TYPE_JSON))] + .into_iter() + .collect(), + body: Vec::new(), + } + } + + fn discovery_response( + issuer_url: &IssuerUrl, + discovery_url: &url::Url, + discovery_response: HttpResponse, + ) -> Result> + where + RE: std::error::Error + 'static, + { + if discovery_response.status_code != StatusCode::OK { + return Err(DiscoveryError::Response( + discovery_response.status_code, + discovery_response.body, + format!( + "HTTP status code {} at {}", + discovery_response.status_code, discovery_url + ), + )); + } + + check_content_type(&discovery_response.headers, MIME_TYPE_JSON).map_err(|err_msg| { + DiscoveryError::Response( + discovery_response.status_code, + discovery_response.body.clone(), + err_msg, + ) + })?; + + let provider_metadata = serde_path_to_error::deserialize::<_, Self>( + &mut serde_json::Deserializer::from_slice(&discovery_response.body), + ) + .map_err(DiscoveryError::Parse)?; + + if provider_metadata.issuer() != issuer_url { + Err(DiscoveryError::Validation(format!( + "unexpected issuer URI `{}` (expected `{}`)", + provider_metadata.issuer().as_str(), + issuer_url.as_str() + ))) + } else { + Ok(provider_metadata) + } + } + + /// Returns additional provider metadata fields. + pub fn additional_metadata(&self) -> &A { + &self.additional_metadata + } + /// Returns mutable additional provider metadata fields. + pub fn additional_metadata_mut(&mut self) -> &mut A { + &mut self.additional_metadata + } +} + +/// Error retrieving provider metadata. +#[derive(Debug, Error)] +#[non_exhaustive] +pub enum DiscoveryError +where + RE: std::error::Error + 'static, +{ + /// An unexpected error occurred. + #[error("Other error: {0}")] + Other(String), + /// Failed to parse server response. + #[error("Failed to parse server response")] + Parse(#[source] serde_path_to_error::Error), + /// An error occurred while sending the request or receiving the response (e.g., network + /// connectivity failed). + #[error("Request failed")] + Request(#[source] RE), + /// Server returned an invalid response. + #[error("Server returned invalid response: {2}")] + Response(StatusCode, Vec, String), + /// Failed to parse discovery URL from issuer URL. + #[error("Failed to parse URL")] + UrlParse(#[source] url::ParseError), + /// Failed to validate provider metadata. + #[error("Validation error: {0}")] + Validation(String), +} diff --git a/src/discovery/tests.rs b/src/discovery/tests.rs new file mode 100644 index 0000000..7b6436b --- /dev/null +++ b/src/discovery/tests.rs @@ -0,0 +1,1160 @@ +use crate::core::{ + CoreAuthDisplay, CoreClaimName, CoreClaimType, CoreClientAuthMethod, CoreGrantType, + CoreJweContentEncryptionAlgorithm, CoreJweKeyManagementAlgorithm, CoreJwsSigningAlgorithm, + CoreProviderMetadata, CoreResponseMode, CoreResponseType, CoreSubjectIdentifierType, +}; +use crate::{ + AuthUrl, AuthenticationContextClass, IssuerUrl, JsonWebKeySetUrl, LanguageTag, OpPolicyUrl, + OpTosUrl, RegistrationUrl, ResponseTypes, Scope, ServiceDocUrl, TokenUrl, UserInfoUrl, +}; + +#[test] +fn test_discovery_deserialization() { + // Fetched from: https://rp.certification.openid.net:8080/openidconnect-rs/ + // rp-response_type-code/.well-known/openid-configuration + let json_response_standard = "\ + \"issuer\":\"https://rp.certification.openid.net:8080/openidconnect-rs/rp-response_type-code\",\ + \"authorization_endpoint\":\"https://rp.certification.openid.net:8080/openidconnect-rs/rp-response_type-code/authorization\",\ + \"token_endpoint\":\"https://rp.certification.openid.net:8080/openidconnect-rs/rp-response_type-code/token\",\ + \"userinfo_endpoint\":\"https://rp.certification.openid.net:8080/openidconnect-rs/rp-response_type-code/userinfo\",\ + \"jwks_uri\":\"https://rp.certification.openid.net:8080/static/jwks_3INbZl52IrrPCp2j.json\",\ + \"registration_endpoint\":\"https://rp.certification.openid.net:8080/openidconnect-rs/rp-response_type-code/registration\",\ + \"scopes_supported\":[\ + \"email\",\ + \"phone\",\ + \"profile\",\ + \"openid\",\ + \"address\",\ + \"offline_access\",\ + \"openid\"\ + ],\ + \"response_types_supported\":[\ + \"code\"\ + ],\ + \"response_modes_supported\":[\ + \"query\",\ + \"fragment\",\ + \"form_post\"\ + ],\ + \"grant_types_supported\":[\ + \"authorization_code\",\ + \"implicit\",\ + \"urn:ietf:params:oauth:grant-type:jwt-bearer\",\ + \"refresh_token\"\ + ],\ + \"acr_values_supported\":[\ + \"PASSWORD\"\ + ],\ + \"subject_types_supported\":[\ + \"public\",\ + \"pairwise\"\ + ],\ + \"id_token_signing_alg_values_supported\":[\ + \"RS256\",\ + \"RS384\",\ + \"RS512\",\ + \"ES256\",\ + \"ES384\",\ + \"ES512\",\ + \"HS256\",\ + \"HS384\",\ + \"HS512\",\ + \"PS256\",\ + \"PS384\",\ + \"PS512\",\ + \"none\"\ + ],\ + \"id_token_encryption_alg_values_supported\":[\ + \"RSA1_5\",\ + \"RSA-OAEP\",\ + \"RSA-OAEP-256\",\ + \"A128KW\",\ + \"A192KW\",\ + \"A256KW\",\ + \"ECDH-ES\",\ + \"ECDH-ES+A128KW\",\ + \"ECDH-ES+A192KW\",\ + \"ECDH-ES+A256KW\"\ + ],\ + \"id_token_encryption_enc_values_supported\":[\ + \"A128CBC-HS256\",\ + \"A192CBC-HS384\",\ + \"A256CBC-HS512\",\ + \"A128GCM\",\ + \"A192GCM\",\ + \"A256GCM\"\ + ],\ + \"userinfo_signing_alg_values_supported\":[\ + \"RS256\",\ + \"RS384\",\ + \"RS512\",\ + \"ES256\",\ + \"ES384\",\ + \"ES512\",\ + \"HS256\",\ + \"HS384\",\ + \"HS512\",\ + \"PS256\",\ + \"PS384\",\ + \"PS512\",\ + \"none\"\ + ],\ + \"userinfo_encryption_alg_values_supported\":[\ + \"RSA1_5\",\ + \"RSA-OAEP\",\ + \"RSA-OAEP-256\",\ + \"A128KW\",\ + \"A192KW\",\ + \"A256KW\",\ + \"ECDH-ES\",\ + \"ECDH-ES+A128KW\",\ + \"ECDH-ES+A192KW\",\ + \"ECDH-ES+A256KW\"\ + ],\ + \"userinfo_encryption_enc_values_supported\":[\ + \"A128CBC-HS256\",\ + \"A192CBC-HS384\",\ + \"A256CBC-HS512\",\ + \"A128GCM\",\ + \"A192GCM\",\ + \"A256GCM\"\ + ],\ + \"request_object_signing_alg_values_supported\":[\ + \"RS256\",\ + \"RS384\",\ + \"RS512\",\ + \"ES256\",\ + \"ES384\",\ + \"ES512\",\ + \"HS256\",\ + \"HS384\",\ + \"HS512\",\ + \"PS256\",\ + \"PS384\",\ + \"PS512\",\ + \"none\"\ + ],\ + \"request_object_encryption_alg_values_supported\":[\ + \"RSA1_5\",\ + \"RSA-OAEP\",\ + \"RSA-OAEP-256\",\ + \"A128KW\",\ + \"A192KW\",\ + \"A256KW\",\ + \"ECDH-ES\",\ + \"ECDH-ES+A128KW\",\ + \"ECDH-ES+A192KW\",\ + \"ECDH-ES+A256KW\"\ + ],\ + \"request_object_encryption_enc_values_supported\":[\ + \"A128CBC-HS256\",\ + \"A192CBC-HS384\",\ + \"A256CBC-HS512\",\ + \"A128GCM\",\ + \"A192GCM\",\ + \"A256GCM\"\ + ],\ + \"token_endpoint_auth_methods_supported\":[\ + \"client_secret_post\",\ + \"client_secret_basic\",\ + \"client_secret_jwt\",\ + \"private_key_jwt\"\ + ],\ + \"token_endpoint_auth_signing_alg_values_supported\":[\ + \"RS256\",\ + \"RS384\",\ + \"RS512\",\ + \"ES256\",\ + \"ES384\",\ + \"ES512\",\ + \"HS256\",\ + \"HS384\",\ + \"HS512\",\ + \"PS256\",\ + \"PS384\",\ + \"PS512\"\ + ],\ + \"claim_types_supported\":[\ + \"normal\",\ + \"aggregated\",\ + \"distributed\"\ + ],\ + \"claims_supported\":[\ + \"name\",\ + \"given_name\",\ + \"middle_name\",\ + \"picture\",\ + \"email_verified\",\ + \"birthdate\",\ + \"sub\",\ + \"address\",\ + \"zoneinfo\",\ + \"email\",\ + \"gender\",\ + \"preferred_username\",\ + \"family_name\",\ + \"website\",\ + \"profile\",\ + \"phone_number_verified\",\ + \"nickname\",\ + \"updated_at\",\ + \"phone_number\",\ + \"locale\"\ + ],\ + \"claims_parameter_supported\":true,\ + \"request_parameter_supported\":true,\ + \"request_uri_parameter_supported\":true,\ + \"require_request_uri_registration\":true"; + + let json_response = format!( + "{{{},{}}}", + json_response_standard, + "\"end_session_endpoint\":\"https://rp.certification.openid.net:8080/openidconnect-rs/rp-response_type-code/end_session\",\ + \"version\":\"3.0\"" + ); + + let all_signing_algs = vec![ + CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256, + CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha384, + CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha512, + CoreJwsSigningAlgorithm::EcdsaP256Sha256, + CoreJwsSigningAlgorithm::EcdsaP384Sha384, + CoreJwsSigningAlgorithm::EcdsaP521Sha512, + CoreJwsSigningAlgorithm::HmacSha256, + CoreJwsSigningAlgorithm::HmacSha384, + CoreJwsSigningAlgorithm::HmacSha512, + CoreJwsSigningAlgorithm::RsaSsaPssSha256, + CoreJwsSigningAlgorithm::RsaSsaPssSha384, + CoreJwsSigningAlgorithm::RsaSsaPssSha512, + CoreJwsSigningAlgorithm::None, + ]; + let all_encryption_algs = vec![ + CoreJweKeyManagementAlgorithm::RsaPkcs1V15, + CoreJweKeyManagementAlgorithm::RsaOaep, + CoreJweKeyManagementAlgorithm::RsaOaepSha256, + CoreJweKeyManagementAlgorithm::AesKeyWrap128, + CoreJweKeyManagementAlgorithm::AesKeyWrap192, + CoreJweKeyManagementAlgorithm::AesKeyWrap256, + CoreJweKeyManagementAlgorithm::EcdhEs, + CoreJweKeyManagementAlgorithm::EcdhEsAesKeyWrap128, + CoreJweKeyManagementAlgorithm::EcdhEsAesKeyWrap192, + CoreJweKeyManagementAlgorithm::EcdhEsAesKeyWrap256, + ]; + let new_provider_metadata = CoreProviderMetadata::new( + IssuerUrl::new( + "https://rp.certification.openid.net:8080/openidconnect-rs/rp-response_type-code" + .to_string(), + ) + .unwrap(), + AuthUrl::new( + "https://rp.certification.openid.net:8080/openidconnect-rs/\ + rp-response_type-code/authorization" + .to_string(), + ) + .unwrap(), + JsonWebKeySetUrl::new( + "https://rp.certification.openid.net:8080/static/jwks_3INbZl52IrrPCp2j.json" + .to_string(), + ) + .unwrap(), + vec![ResponseTypes::new(vec![CoreResponseType::Code])], + vec![ + CoreSubjectIdentifierType::Public, + CoreSubjectIdentifierType::Pairwise, + ], + all_signing_algs.clone(), + Default::default(), + ) + .set_request_object_signing_alg_values_supported(Some(all_signing_algs.clone())) + .set_token_endpoint_auth_signing_alg_values_supported(Some(vec![ + CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256, + CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha384, + CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha512, + CoreJwsSigningAlgorithm::EcdsaP256Sha256, + CoreJwsSigningAlgorithm::EcdsaP384Sha384, + CoreJwsSigningAlgorithm::EcdsaP521Sha512, + CoreJwsSigningAlgorithm::HmacSha256, + CoreJwsSigningAlgorithm::HmacSha384, + CoreJwsSigningAlgorithm::HmacSha512, + CoreJwsSigningAlgorithm::RsaSsaPssSha256, + CoreJwsSigningAlgorithm::RsaSsaPssSha384, + CoreJwsSigningAlgorithm::RsaSsaPssSha512, + ])) + .set_scopes_supported(Some(vec![ + Scope::new("email".to_string()), + Scope::new("phone".to_string()), + Scope::new("profile".to_string()), + Scope::new("openid".to_string()), + Scope::new("address".to_string()), + Scope::new("offline_access".to_string()), + Scope::new("openid".to_string()), + ])) + .set_userinfo_signing_alg_values_supported(Some(all_signing_algs)) + .set_id_token_encryption_enc_values_supported(Some(vec![ + CoreJweContentEncryptionAlgorithm::Aes128CbcHmacSha256, + CoreJweContentEncryptionAlgorithm::Aes192CbcHmacSha384, + CoreJweContentEncryptionAlgorithm::Aes256CbcHmacSha512, + CoreJweContentEncryptionAlgorithm::Aes128Gcm, + CoreJweContentEncryptionAlgorithm::Aes192Gcm, + CoreJweContentEncryptionAlgorithm::Aes256Gcm, + ])) + .set_grant_types_supported(Some(vec![ + CoreGrantType::AuthorizationCode, + CoreGrantType::Implicit, + CoreGrantType::JwtBearer, + CoreGrantType::RefreshToken, + ])) + .set_response_modes_supported(Some(vec![ + CoreResponseMode::Query, + CoreResponseMode::Fragment, + CoreResponseMode::FormPost, + ])) + .set_require_request_uri_registration(Some(true)) + .set_registration_endpoint(Some( + RegistrationUrl::new( + "https://rp.certification.openid.net:8080/openidconnect-rs/\ + rp-response_type-code/registration" + .to_string(), + ) + .unwrap(), + )) + .set_claims_parameter_supported(Some(true)) + .set_request_object_encryption_enc_values_supported(Some(vec![ + CoreJweContentEncryptionAlgorithm::Aes128CbcHmacSha256, + CoreJweContentEncryptionAlgorithm::Aes192CbcHmacSha384, + CoreJweContentEncryptionAlgorithm::Aes256CbcHmacSha512, + CoreJweContentEncryptionAlgorithm::Aes128Gcm, + CoreJweContentEncryptionAlgorithm::Aes192Gcm, + CoreJweContentEncryptionAlgorithm::Aes256Gcm, + ])) + .set_userinfo_endpoint(Some( + UserInfoUrl::new( + "https://rp.certification.openid.net:8080/openidconnect-rs/\ + rp-response_type-code/userinfo" + .to_string(), + ) + .unwrap(), + )) + .set_token_endpoint_auth_methods_supported(Some(vec![ + CoreClientAuthMethod::ClientSecretPost, + CoreClientAuthMethod::ClientSecretBasic, + CoreClientAuthMethod::ClientSecretJwt, + CoreClientAuthMethod::PrivateKeyJwt, + ])) + .set_claims_supported(Some( + vec![ + "name", + "given_name", + "middle_name", + "picture", + "email_verified", + "birthdate", + "sub", + "address", + "zoneinfo", + "email", + "gender", + "preferred_username", + "family_name", + "website", + "profile", + "phone_number_verified", + "nickname", + "updated_at", + "phone_number", + "locale", + ] + .iter() + .map(|claim| CoreClaimName::new((*claim).to_string())) + .collect(), + )) + .set_request_object_encryption_alg_values_supported(Some(all_encryption_algs.clone())) + .set_claim_types_supported(Some(vec![ + CoreClaimType::Normal, + CoreClaimType::Aggregated, + CoreClaimType::Distributed, + ])) + .set_request_uri_parameter_supported(Some(true)) + .set_request_parameter_supported(Some(true)) + .set_token_endpoint(Some( + TokenUrl::new( + "https://rp.certification.openid.net:8080/openidconnect-rs/\ + rp-response_type-code/token" + .to_string(), + ) + .unwrap(), + )) + .set_id_token_encryption_alg_values_supported(Some(all_encryption_algs.clone())) + .set_userinfo_encryption_alg_values_supported(Some(all_encryption_algs)) + .set_userinfo_encryption_enc_values_supported(Some(vec![ + CoreJweContentEncryptionAlgorithm::Aes128CbcHmacSha256, + CoreJweContentEncryptionAlgorithm::Aes192CbcHmacSha384, + CoreJweContentEncryptionAlgorithm::Aes256CbcHmacSha512, + CoreJweContentEncryptionAlgorithm::Aes128Gcm, + CoreJweContentEncryptionAlgorithm::Aes192Gcm, + CoreJweContentEncryptionAlgorithm::Aes256Gcm, + ])) + .set_acr_values_supported(Some(vec![AuthenticationContextClass::new( + "PASSWORD".to_string(), + )])); + + let provider_metadata: CoreProviderMetadata = serde_json::from_str(&json_response).unwrap(); + assert_eq!(provider_metadata, new_provider_metadata); + + let serialized = serde_json::to_string(&provider_metadata).unwrap(); + assert_eq!(serialized, format!("{{{}}}", json_response_standard)); + + assert_eq!( + IssuerUrl::new( + "https://rp.certification.openid.net:8080/openidconnect-rs/rp-response_type-code" + .to_string() + ) + .unwrap(), + *provider_metadata.issuer() + ); + assert_eq!( + AuthUrl::new( + "https://rp.certification.openid.net:8080/openidconnect-rs/rp-response_type-code\ + /authorization" + .to_string() + ) + .unwrap(), + *provider_metadata.authorization_endpoint() + ); + assert_eq!( + Some( + &TokenUrl::new( + "https://rp.certification.openid.net:8080/openidconnect-rs\ + /rp-response_type-code/token" + .to_string() + ) + .unwrap() + ), + provider_metadata.token_endpoint() + ); + assert_eq!( + Some( + &UserInfoUrl::new( + "https://rp.certification.openid.net:8080/openidconnect-rs\ + /rp-response_type-code/userinfo" + .to_string() + ) + .unwrap() + ), + provider_metadata.userinfo_endpoint() + ); + assert_eq!( + &JsonWebKeySetUrl::new( + "https://rp.certification.openid.net:8080/static/jwks_3INbZl52IrrPCp2j.json" + .to_string() + ) + .unwrap(), + provider_metadata.jwks_uri() + ); + assert_eq!( + Some( + &RegistrationUrl::new( + "https://rp.certification.openid.net:8080/openidconnect-rs\ + /rp-response_type-code/registration" + .to_string() + ) + .unwrap() + ), + provider_metadata.registration_endpoint() + ); + assert_eq!( + Some( + &[ + "email", + "phone", + "profile", + "openid", + "address", + "offline_access", + "openid", + ] + .iter() + .map(|s| (*s).to_string()) + .map(Scope::new) + .collect::>() + ), + provider_metadata.scopes_supported() + ); + assert_eq!( + vec![ResponseTypes::new(vec![CoreResponseType::Code])], + *provider_metadata.response_types_supported() + ); + assert_eq!( + Some(&vec![ + CoreResponseMode::Query, + CoreResponseMode::Fragment, + CoreResponseMode::FormPost, + ]), + provider_metadata.response_modes_supported() + ); + assert_eq!( + Some( + &vec![ + CoreGrantType::AuthorizationCode, + CoreGrantType::Implicit, + CoreGrantType::JwtBearer, + CoreGrantType::RefreshToken, + ] + .into_iter() + .collect::>() + ), + provider_metadata.grant_types_supported() + ); + assert_eq!( + Some(&vec![AuthenticationContextClass::new( + "PASSWORD".to_string(), + )]), + provider_metadata.acr_values_supported() + ); + assert_eq!( + vec![ + CoreSubjectIdentifierType::Public, + CoreSubjectIdentifierType::Pairwise, + ], + *provider_metadata.subject_types_supported() + ); + assert_eq!( + vec![ + CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256, + CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha384, + CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha512, + CoreJwsSigningAlgorithm::EcdsaP256Sha256, + CoreJwsSigningAlgorithm::EcdsaP384Sha384, + CoreJwsSigningAlgorithm::EcdsaP521Sha512, + CoreJwsSigningAlgorithm::HmacSha256, + CoreJwsSigningAlgorithm::HmacSha384, + CoreJwsSigningAlgorithm::HmacSha512, + CoreJwsSigningAlgorithm::RsaSsaPssSha256, + CoreJwsSigningAlgorithm::RsaSsaPssSha384, + CoreJwsSigningAlgorithm::RsaSsaPssSha512, + CoreJwsSigningAlgorithm::None, + ], + *provider_metadata.id_token_signing_alg_values_supported() + ); + assert_eq!( + Some(&vec![ + CoreJweKeyManagementAlgorithm::RsaPkcs1V15, + CoreJweKeyManagementAlgorithm::RsaOaep, + CoreJweKeyManagementAlgorithm::RsaOaepSha256, + CoreJweKeyManagementAlgorithm::AesKeyWrap128, + CoreJweKeyManagementAlgorithm::AesKeyWrap192, + CoreJweKeyManagementAlgorithm::AesKeyWrap256, + CoreJweKeyManagementAlgorithm::EcdhEs, + CoreJweKeyManagementAlgorithm::EcdhEsAesKeyWrap128, + CoreJweKeyManagementAlgorithm::EcdhEsAesKeyWrap192, + CoreJweKeyManagementAlgorithm::EcdhEsAesKeyWrap256, + ]), + provider_metadata.id_token_encryption_alg_values_supported() + ); + assert_eq!( + Some(&vec![ + CoreJweContentEncryptionAlgorithm::Aes128CbcHmacSha256, + CoreJweContentEncryptionAlgorithm::Aes192CbcHmacSha384, + CoreJweContentEncryptionAlgorithm::Aes256CbcHmacSha512, + CoreJweContentEncryptionAlgorithm::Aes128Gcm, + CoreJweContentEncryptionAlgorithm::Aes192Gcm, + CoreJweContentEncryptionAlgorithm::Aes256Gcm, + ]), + provider_metadata.id_token_encryption_enc_values_supported() + ); + assert_eq!( + Some(&vec![ + CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256, + CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha384, + CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha512, + CoreJwsSigningAlgorithm::EcdsaP256Sha256, + CoreJwsSigningAlgorithm::EcdsaP384Sha384, + CoreJwsSigningAlgorithm::EcdsaP521Sha512, + CoreJwsSigningAlgorithm::HmacSha256, + CoreJwsSigningAlgorithm::HmacSha384, + CoreJwsSigningAlgorithm::HmacSha512, + CoreJwsSigningAlgorithm::RsaSsaPssSha256, + CoreJwsSigningAlgorithm::RsaSsaPssSha384, + CoreJwsSigningAlgorithm::RsaSsaPssSha512, + CoreJwsSigningAlgorithm::None, + ]), + provider_metadata.userinfo_signing_alg_values_supported() + ); + assert_eq!( + Some(&vec![ + CoreJweKeyManagementAlgorithm::RsaPkcs1V15, + CoreJweKeyManagementAlgorithm::RsaOaep, + CoreJweKeyManagementAlgorithm::RsaOaepSha256, + CoreJweKeyManagementAlgorithm::AesKeyWrap128, + CoreJweKeyManagementAlgorithm::AesKeyWrap192, + CoreJweKeyManagementAlgorithm::AesKeyWrap256, + CoreJweKeyManagementAlgorithm::EcdhEs, + CoreJweKeyManagementAlgorithm::EcdhEsAesKeyWrap128, + CoreJweKeyManagementAlgorithm::EcdhEsAesKeyWrap192, + CoreJweKeyManagementAlgorithm::EcdhEsAesKeyWrap256, + ]), + provider_metadata.userinfo_encryption_alg_values_supported() + ); + assert_eq!( + Some(&vec![ + CoreJweContentEncryptionAlgorithm::Aes128CbcHmacSha256, + CoreJweContentEncryptionAlgorithm::Aes192CbcHmacSha384, + CoreJweContentEncryptionAlgorithm::Aes256CbcHmacSha512, + CoreJweContentEncryptionAlgorithm::Aes128Gcm, + CoreJweContentEncryptionAlgorithm::Aes192Gcm, + CoreJweContentEncryptionAlgorithm::Aes256Gcm, + ]), + provider_metadata.userinfo_encryption_enc_values_supported() + ); + assert_eq!( + Some(&vec![ + CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256, + CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha384, + CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha512, + CoreJwsSigningAlgorithm::EcdsaP256Sha256, + CoreJwsSigningAlgorithm::EcdsaP384Sha384, + CoreJwsSigningAlgorithm::EcdsaP521Sha512, + CoreJwsSigningAlgorithm::HmacSha256, + CoreJwsSigningAlgorithm::HmacSha384, + CoreJwsSigningAlgorithm::HmacSha512, + CoreJwsSigningAlgorithm::RsaSsaPssSha256, + CoreJwsSigningAlgorithm::RsaSsaPssSha384, + CoreJwsSigningAlgorithm::RsaSsaPssSha512, + CoreJwsSigningAlgorithm::None, + ]), + provider_metadata.request_object_signing_alg_values_supported() + ); + assert_eq!( + Some(&vec![ + CoreJweKeyManagementAlgorithm::RsaPkcs1V15, + CoreJweKeyManagementAlgorithm::RsaOaep, + CoreJweKeyManagementAlgorithm::RsaOaepSha256, + CoreJweKeyManagementAlgorithm::AesKeyWrap128, + CoreJweKeyManagementAlgorithm::AesKeyWrap192, + CoreJweKeyManagementAlgorithm::AesKeyWrap256, + CoreJweKeyManagementAlgorithm::EcdhEs, + CoreJweKeyManagementAlgorithm::EcdhEsAesKeyWrap128, + CoreJweKeyManagementAlgorithm::EcdhEsAesKeyWrap192, + CoreJweKeyManagementAlgorithm::EcdhEsAesKeyWrap256, + ]), + provider_metadata.request_object_encryption_alg_values_supported() + ); + assert_eq!( + Some(&vec![ + CoreJweContentEncryptionAlgorithm::Aes128CbcHmacSha256, + CoreJweContentEncryptionAlgorithm::Aes192CbcHmacSha384, + CoreJweContentEncryptionAlgorithm::Aes256CbcHmacSha512, + CoreJweContentEncryptionAlgorithm::Aes128Gcm, + CoreJweContentEncryptionAlgorithm::Aes192Gcm, + CoreJweContentEncryptionAlgorithm::Aes256Gcm, + ]), + provider_metadata.request_object_encryption_enc_values_supported() + ); + assert_eq!( + Some(&vec![ + CoreClientAuthMethod::ClientSecretPost, + CoreClientAuthMethod::ClientSecretBasic, + CoreClientAuthMethod::ClientSecretJwt, + CoreClientAuthMethod::PrivateKeyJwt, + ]), + provider_metadata.token_endpoint_auth_methods_supported() + ); + assert_eq!( + Some(&vec![ + CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256, + CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha384, + CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha512, + CoreJwsSigningAlgorithm::EcdsaP256Sha256, + CoreJwsSigningAlgorithm::EcdsaP384Sha384, + CoreJwsSigningAlgorithm::EcdsaP521Sha512, + CoreJwsSigningAlgorithm::HmacSha256, + CoreJwsSigningAlgorithm::HmacSha384, + CoreJwsSigningAlgorithm::HmacSha512, + CoreJwsSigningAlgorithm::RsaSsaPssSha256, + CoreJwsSigningAlgorithm::RsaSsaPssSha384, + CoreJwsSigningAlgorithm::RsaSsaPssSha512, + ]), + provider_metadata.token_endpoint_auth_signing_alg_values_supported() + ); + assert_eq!(None, provider_metadata.display_values_supported()); + assert_eq!( + Some(&vec![ + CoreClaimType::Normal, + CoreClaimType::Aggregated, + CoreClaimType::Distributed, + ]), + provider_metadata.claim_types_supported() + ); + assert_eq!( + Some(&vec![ + CoreClaimName::new("name".to_string()), + CoreClaimName::new("given_name".to_string()), + CoreClaimName::new("middle_name".to_string()), + CoreClaimName::new("picture".to_string()), + CoreClaimName::new("email_verified".to_string()), + CoreClaimName::new("birthdate".to_string()), + CoreClaimName::new("sub".to_string()), + CoreClaimName::new("address".to_string()), + CoreClaimName::new("zoneinfo".to_string()), + CoreClaimName::new("email".to_string()), + CoreClaimName::new("gender".to_string()), + CoreClaimName::new("preferred_username".to_string()), + CoreClaimName::new("family_name".to_string()), + CoreClaimName::new("website".to_string()), + CoreClaimName::new("profile".to_string()), + CoreClaimName::new("phone_number_verified".to_string()), + CoreClaimName::new("nickname".to_string()), + CoreClaimName::new("updated_at".to_string()), + CoreClaimName::new("phone_number".to_string()), + CoreClaimName::new("locale".to_string()), + ]), + provider_metadata.claims_supported() + ); + assert_eq!(None, provider_metadata.service_documentation()); + assert_eq!(None, provider_metadata.claims_locales_supported()); + assert_eq!(None, provider_metadata.ui_locales_supported()); + assert_eq!(Some(true), provider_metadata.claims_parameter_supported()); + assert_eq!(Some(true), provider_metadata.request_parameter_supported()); + assert_eq!( + Some(true), + provider_metadata.request_uri_parameter_supported() + ); + assert_eq!( + Some(true), + provider_metadata.require_request_uri_registration() + ); + assert_eq!(None, provider_metadata.op_policy_uri()); + assert_eq!(None, provider_metadata.op_tos_uri()); + + // Note: the following fields provided by the response above are not part of the OpenID + // Connect Discovery 1.0 spec: + // - end_session_endpoint + // - version + + let serialized_json = serde_json::to_string(&provider_metadata).unwrap(); + + let redeserialized_metadata: CoreProviderMetadata = + serde_json::from_str(&serialized_json).unwrap(); + assert_eq!(provider_metadata, redeserialized_metadata); +} + +// Tests the fields missing from the example response in test_discovery_deserialization(). +#[test] +fn test_discovery_deserialization_other_fields() { + let json_response = "{ + \"issuer\" : \"https://rp.certification.openid.net:8080/openidconnect-rs/rp-response_type-code\", + \"authorization_endpoint\" : \"https://rp.certification.openid.net:8080/openidconnect-rs/rp-response_type-code/authorization\", + \"jwks_uri\" : \"https://rp.certification.openid.net:8080/static/jwks_oMXD5waO08Q1GEnv.json\", + \"response_types_supported\" : [ + \"code\", + \"code token\", + \"code id_token\", + \"id_token token\", + \"code id_token token\", + \"token id_token\", + \"token id_token code\", + \"id_token\", + \"token\" + ], + \"subject_types_supported\" : [ + \"public\", + \"pairwise\" + ], + \"id_token_signing_alg_values_supported\" : [ + \"HS256\", + \"HS384\", + \"HS512\" + ], + \"display_values_supported\" : [ + \"page\", + \"popup\", + \"touch\", + \"wap\" + ], + \"service_documentation\" : \"https://rp.certification.openid.net:8080/openidconnect-rs/rp-response_type-code/documentation\", + \"claims_locales_supported\" : [ + \"de\", + \"fr\", + \"de-CH-1901\" + ], + \"ui_locales_supported\" : [ + \"ja\", + \"sr-Latn\", + \"yue-HK\" + ], + \"op_policy_uri\" : \"https://rp.certification.openid.net:8080/openidconnect-rs/rp-response_type-code/op_policy\", + \"op_tos_uri\" : \"https://rp.certification.openid.net:8080/openidconnect-rs/rp-response_type-code/op_tos\" + }"; + + let provider_metadata: CoreProviderMetadata = serde_json::from_str(json_response).unwrap(); + + assert_eq!( + IssuerUrl::new( + "https://rp.certification.openid.net:8080/openidconnect-rs/rp-response_type-code" + .to_string() + ) + .unwrap(), + *provider_metadata.issuer() + ); + assert_eq!( + AuthUrl::new( + "https://rp.certification.openid.net:8080/openidconnect-rs/rp-response_type-code\ + /authorization" + .to_string() + ) + .unwrap(), + *provider_metadata.authorization_endpoint() + ); + assert_eq!(None, provider_metadata.token_endpoint()); + assert_eq!(None, provider_metadata.userinfo_endpoint()); + assert_eq!( + JsonWebKeySetUrl::new( + "https://rp.certification.openid.net:8080/static/jwks_oMXD5waO08Q1GEnv.json" + .to_string() + ) + .unwrap(), + *provider_metadata.jwks_uri() + ); + assert_eq!(None, provider_metadata.registration_endpoint()); + assert_eq!(None, provider_metadata.scopes_supported()); + assert_eq!( + vec![ + ResponseTypes::new(vec![CoreResponseType::Code]), + ResponseTypes::new(vec![CoreResponseType::Code, CoreResponseType::Token]), + ResponseTypes::new(vec![CoreResponseType::Code, CoreResponseType::IdToken]), + ResponseTypes::new(vec![CoreResponseType::IdToken, CoreResponseType::Token]), + ResponseTypes::new(vec![ + CoreResponseType::Code, + CoreResponseType::IdToken, + CoreResponseType::Token, + ]), + ResponseTypes::new(vec![CoreResponseType::Token, CoreResponseType::IdToken]), + ResponseTypes::new(vec![ + CoreResponseType::Token, + CoreResponseType::IdToken, + CoreResponseType::Code, + ]), + ResponseTypes::new(vec![CoreResponseType::IdToken]), + ResponseTypes::new(vec![CoreResponseType::Token]), + ], + *provider_metadata.response_types_supported() + ); + assert_eq!(None, provider_metadata.response_modes_supported()); + assert_eq!(None, provider_metadata.grant_types_supported()); + assert_eq!(None, provider_metadata.acr_values_supported()); + assert_eq!( + vec![ + CoreSubjectIdentifierType::Public, + CoreSubjectIdentifierType::Pairwise, + ], + *provider_metadata.subject_types_supported() + ); + assert_eq!( + vec![ + CoreJwsSigningAlgorithm::HmacSha256, + CoreJwsSigningAlgorithm::HmacSha384, + CoreJwsSigningAlgorithm::HmacSha512, + ], + *provider_metadata.id_token_signing_alg_values_supported() + ); + assert_eq!( + None, + provider_metadata.id_token_encryption_alg_values_supported() + ); + assert_eq!( + None, + provider_metadata.id_token_encryption_enc_values_supported() + ); + assert_eq!( + None, + provider_metadata.userinfo_signing_alg_values_supported() + ); + assert_eq!( + None, + provider_metadata.userinfo_encryption_alg_values_supported() + ); + assert_eq!( + None, + provider_metadata.userinfo_encryption_enc_values_supported() + ); + assert_eq!( + None, + provider_metadata.request_object_signing_alg_values_supported() + ); + assert_eq!( + None, + provider_metadata.request_object_encryption_alg_values_supported() + ); + assert_eq!( + None, + provider_metadata.request_object_encryption_enc_values_supported() + ); + assert_eq!( + None, + provider_metadata.token_endpoint_auth_methods_supported() + ); + assert_eq!( + None, + provider_metadata.token_endpoint_auth_signing_alg_values_supported() + ); + assert_eq!( + Some(&vec![ + CoreAuthDisplay::Page, + CoreAuthDisplay::Popup, + CoreAuthDisplay::Touch, + CoreAuthDisplay::Wap, + ]), + provider_metadata.display_values_supported() + ); + assert_eq!(None, provider_metadata.claim_types_supported()); + assert_eq!(None, provider_metadata.claims_supported()); + + assert_eq!( + Some( + &ServiceDocUrl::new( + "https://rp.certification.openid.net:8080/openidconnect-rs/rp-response_type-code\ + /documentation" + .to_string() + ) + .unwrap() + ), + provider_metadata.service_documentation() + ); + assert_eq!( + Some(&vec![ + LanguageTag::new("de".to_string()), + LanguageTag::new("fr".to_string()), + LanguageTag::new("de-CH-1901".to_string()), + ]), + provider_metadata.claims_locales_supported() + ); + assert_eq!( + Some(&vec![ + LanguageTag::new("ja".to_string()), + LanguageTag::new("sr-Latn".to_string()), + LanguageTag::new("yue-HK".to_string()), + ]), + provider_metadata.ui_locales_supported() + ); + assert_eq!(None, provider_metadata.claims_parameter_supported()); + assert_eq!(None, provider_metadata.request_parameter_supported()); + assert_eq!(None, provider_metadata.request_uri_parameter_supported()); + assert_eq!(None, provider_metadata.require_request_uri_registration()); + assert_eq!( + Some( + &OpPolicyUrl::new( + "https://rp.certification.openid.net:8080/openidconnect-rs/rp-response_type-code\ + /op_policy" + .to_string() + ) + .unwrap() + ), + provider_metadata.op_policy_uri() + ); + assert_eq!( + Some( + &OpTosUrl::new( + "https://rp.certification.openid.net:8080/openidconnect-rs/rp-response_type-code\ + /op_tos" + .to_string() + ) + .unwrap() + ), + provider_metadata.op_tos_uri() + ); + + let serialized_json = serde_json::to_string(&provider_metadata).unwrap(); + + let redeserialized_metadata: CoreProviderMetadata = + serde_json::from_str(&serialized_json).unwrap(); + assert_eq!(provider_metadata, redeserialized_metadata); +} + +// Tests that we ignore enum values that the OIDC provider supports but that the client does +// not (which trigger serde deserialization errors while parsing the provider metadata). +#[test] +fn test_unsupported_enum_values() { + let json_response = "{\ + \"issuer\":\"https://rp.certification.openid.net:8080/openidconnect-rs/rp-response_type-code\",\ + \"authorization_endpoint\":\"https://rp.certification.openid.net:8080/openidconnect-rs/rp-response_type-code/authorization\",\ + \"jwks_uri\":\"https://rp.certification.openid.net:8080/static/jwks_3INbZl52IrrPCp2j.json\",\ + \"response_types_supported\":[\ + \"code\"\ + ],\ + \"subject_types_supported\":[\ + \"public\",\ + \"pairwise\"\ + ],\ + \"id_token_signing_alg_values_supported\":[\ + \"RS256\",\ + \"MAGIC\",\ + \"none\"\ + ],\ + \"id_token_encryption_alg_values_supported\":[\ + \"RSA1_5\",\ + \"MAGIC\"\ + ],\ + \"id_token_encryption_enc_values_supported\":[\ + \"A128CBC-HS256\",\ + \"MAGIC\"\ + ],\ + \"userinfo_signing_alg_values_supported\":[\ + \"RS256\",\ + \"MAGIC\",\ + \"none\"\ + ],\ + \"userinfo_encryption_alg_values_supported\":[\ + \"RSA1_5\",\ + \"MAGIC\"\ + ],\ + \"userinfo_encryption_enc_values_supported\":[\ + \"A128CBC-HS256\",\ + \"MAGIC\"\ + ],\ + \"request_object_signing_alg_values_supported\":[\ + \"RS256\",\ + \"MAGIC\",\ + \"none\"\ + ],\ + \"request_object_encryption_alg_values_supported\":[\ + \"RSA1_5\",\ + \"MAGIC\"\ + ],\ + \"request_object_encryption_enc_values_supported\":[\ + \"A128CBC-HS256\",\ + \"MAGIC\"\ + ],\ + \"token_endpoint_auth_signing_alg_values_supported\":[\ + \"RS256\",\ + \"MAGIC\",\ + \"none\"\ + ]\ + }"; + + let provider_metadata: CoreProviderMetadata = serde_json::from_str(json_response).unwrap(); + + assert_eq!( + IssuerUrl::new( + "https://rp.certification.openid.net:8080/openidconnect-rs/rp-response_type-code" + .to_string() + ) + .unwrap(), + *provider_metadata.issuer() + ); + assert_eq!( + AuthUrl::new( + "https://rp.certification.openid.net:8080/openidconnect-rs/rp-response_type-code\ + /authorization" + .to_string() + ) + .unwrap(), + *provider_metadata.authorization_endpoint() + ); + assert_eq!(None, provider_metadata.token_endpoint()); + assert_eq!(None, provider_metadata.userinfo_endpoint()); + assert_eq!( + JsonWebKeySetUrl::new( + "https://rp.certification.openid.net:8080/static/jwks_3INbZl52IrrPCp2j.json" + .to_string() + ) + .unwrap(), + *provider_metadata.jwks_uri() + ); + assert_eq!(None, provider_metadata.registration_endpoint()); + assert_eq!(None, provider_metadata.scopes_supported()); + assert_eq!( + vec![ResponseTypes::new(vec![CoreResponseType::Code])], + *provider_metadata.response_types_supported() + ); + assert_eq!(None, provider_metadata.response_modes_supported()); + assert_eq!(None, provider_metadata.grant_types_supported()); + assert_eq!(None, provider_metadata.acr_values_supported()); + assert_eq!( + vec![ + CoreSubjectIdentifierType::Public, + CoreSubjectIdentifierType::Pairwise, + ], + *provider_metadata.subject_types_supported() + ); + assert_eq!( + vec![ + CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256, + CoreJwsSigningAlgorithm::None, + ], + *provider_metadata.id_token_signing_alg_values_supported() + ); + assert_eq!( + Some(&vec![CoreJweKeyManagementAlgorithm::RsaPkcs1V15]), + provider_metadata.id_token_encryption_alg_values_supported() + ); + assert_eq!( + Some(&vec![ + CoreJweContentEncryptionAlgorithm::Aes128CbcHmacSha256 + ]), + provider_metadata.id_token_encryption_enc_values_supported() + ); + assert_eq!( + Some(&vec![ + CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256, + CoreJwsSigningAlgorithm::None, + ]), + provider_metadata.userinfo_signing_alg_values_supported() + ); + assert_eq!( + Some(&vec![CoreJweKeyManagementAlgorithm::RsaPkcs1V15]), + provider_metadata.userinfo_encryption_alg_values_supported() + ); + assert_eq!( + Some(&vec![ + CoreJweContentEncryptionAlgorithm::Aes128CbcHmacSha256 + ]), + provider_metadata.userinfo_encryption_enc_values_supported() + ); + assert_eq!( + Some(&vec![ + CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256, + CoreJwsSigningAlgorithm::None, + ]), + provider_metadata.request_object_signing_alg_values_supported() + ); + assert_eq!( + Some(&vec![CoreJweKeyManagementAlgorithm::RsaPkcs1V15]), + provider_metadata.request_object_encryption_alg_values_supported() + ); + assert_eq!( + Some(&vec![ + CoreJweContentEncryptionAlgorithm::Aes128CbcHmacSha256 + ]), + provider_metadata.request_object_encryption_enc_values_supported() + ); + assert_eq!( + None, + provider_metadata.token_endpoint_auth_methods_supported() + ); + assert_eq!( + Some(&vec![ + CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256, + CoreJwsSigningAlgorithm::None, + ]), + provider_metadata.token_endpoint_auth_signing_alg_values_supported() + ); + assert_eq!(None, provider_metadata.display_values_supported()); + assert_eq!(None, provider_metadata.claim_types_supported()); + assert_eq!(None, provider_metadata.claims_supported()); + + assert_eq!(None, provider_metadata.service_documentation()); + assert_eq!(None, provider_metadata.claims_locales_supported()); + assert_eq!(None, provider_metadata.ui_locales_supported()); + assert_eq!(None, provider_metadata.claims_parameter_supported()); + assert_eq!(None, provider_metadata.request_parameter_supported()); + assert_eq!(None, provider_metadata.request_uri_parameter_supported()); + assert_eq!(None, provider_metadata.require_request_uri_registration()); + assert_eq!(None, provider_metadata.op_policy_uri()); + assert_eq!(None, provider_metadata.op_tos_uri()); + + let serialized_json = serde_json::to_string(&provider_metadata).unwrap(); + + let redeserialized_metadata: CoreProviderMetadata = + serde_json::from_str(&serialized_json).unwrap(); + assert_eq!(provider_metadata, redeserialized_metadata); +} diff --git a/src/helpers.rs b/src/helpers.rs index 99b2770..f219653 100644 --- a/src/helpers.rs +++ b/src/helpers.rs @@ -1,12 +1,128 @@ +use chrono::{DateTime, TimeZone, Utc}; use serde::de::value::MapDeserializer; use serde::de::{DeserializeOwned, Deserializer, MapAccess, Visitor}; -use serde::{Deserialize, Serialize}; -use serde_value::{Value, ValueDeserializer}; +use serde::{Deserialize, Serialize, Serializer}; +use serde_json::from_value; +use serde_value::ValueDeserializer; use std::cmp::PartialEq; -use std::fmt::{Debug, Formatter, Result as FormatterResult}; +use std::fmt::{Debug, Display, Formatter, Result as FormatterResult}; use std::marker::PhantomData; +pub(crate) fn deserialize_string_or_vec<'de, T, D>(deserializer: D) -> Result, D::Error> +where + T: DeserializeOwned, + D: Deserializer<'de>, +{ + use serde::de::Error; + + let value: serde_json::Value = Deserialize::deserialize(deserializer)?; + match from_value::>(value.clone()) { + Ok(val) => Ok(val), + Err(_) => { + let single_val: T = from_value(value).map_err(Error::custom)?; + Ok(vec![single_val]) + } + } +} + +pub(crate) fn deserialize_string_or_vec_opt<'de, T, D>( + deserializer: D, +) -> Result>, D::Error> +where + T: DeserializeOwned, + D: Deserializer<'de>, +{ + use serde::de::Error; + + let value: serde_json::Value = Deserialize::deserialize(deserializer)?; + match from_value::>>(value.clone()) { + Ok(val) => Ok(val), + Err(_) => { + let single_val: T = from_value(value).map_err(Error::custom)?; + Ok(Some(vec![single_val])) + } + } +} + +// Attempt to deserialize the value; if the value is null or an error occurs, return None. +// This is useful when deserializing fields that may mean different things in different +// contexts, and where we would rather ignore the result than fail to deserialize. For example, +// the fields in JWKs are not well defined; extensions could theoretically define their own +// field names that overload field names used by other JWK types. +pub(crate) fn deserialize_option_or_none<'de, T, D>(deserializer: D) -> Result, D::Error> +where + T: DeserializeOwned, + D: Deserializer<'de>, +{ + let value: serde_json::Value = Deserialize::deserialize(deserializer)?; + match from_value::>(value) { + Ok(val) => Ok(val), + Err(_) => Ok(None), + } +} + +// Some providers return boolean values as strings. Provide support for +// parsing using stdlib. +#[cfg(feature = "accept-string-booleans")] +pub(crate) mod serde_string_bool { + use serde::{de, Deserializer}; + + use std::fmt; + + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct BooleanLikeVisitor; + + impl<'de> de::Visitor<'de> for BooleanLikeVisitor { + type Value = bool; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("A boolean-like value") + } + + fn visit_bool(self, v: bool) -> Result + where + E: de::Error, + { + Ok(v) + } + + fn visit_str(self, v: &str) -> Result + where + E: de::Error, + { + v.parse().map_err(E::custom) + } + } + deserializer.deserialize_any(BooleanLikeVisitor) + } +} + +/// Serde space-delimited string serializer for an `Option>`. +/// +/// This function serializes a string vector into a single space-delimited string. +/// If `string_vec_opt` is `None`, the function serializes it as `None` (e.g., `null` +/// in the case of JSON serialization). +pub(crate) fn serialize_space_delimited_vec( + vec: &[T], + serializer: S, +) -> Result +where + T: AsRef, + S: Serializer, +{ + let space_delimited = vec + .iter() + .map(AsRef::::as_ref) + .collect::>() + .join(" "); + + serializer.serialize_str(&space_delimited) +} + pub(crate) trait FlattenFilter { fn should_include(field_name: &str) -> bool; } @@ -120,9 +236,9 @@ where where V: MapAccess<'de>, { - let mut entries = Vec::<(Value, Value)>::new(); + let mut entries = Vec::<(serde_value::Value, serde_value::Value)>::new(); // JSON only supports String keys, and we really only need to support JSON input. - while let Some(key) = map.next_key::()? { + while let Some(key) = map.next_key::()? { let key_str = String::deserialize(ValueDeserializer::new(key.clone()))?; if F::should_include(&key_str) { entries.push((key, map.next_value()?)); @@ -182,3 +298,177 @@ where Debug::fmt(&self.inner, f) } } + +pub(crate) fn join_vec(entries: &[T]) -> String +where + T: AsRef, +{ + entries + .iter() + .map(AsRef::as_ref) + .collect::>() + .join(" ") +} + +/// Newtype around a bool, optionally supporting string values. +#[derive(Debug, Deserialize, Serialize)] +#[serde(transparent)] +pub(crate) struct Boolean( + #[cfg_attr( + feature = "accept-string-booleans", + serde(deserialize_with = "crate::helpers::serde_string_bool::deserialize") + )] + pub bool, +); + +impl Display for Boolean { + fn fmt(&self, f: &mut Formatter) -> Result<(), std::fmt::Error> { + Display::fmt(&self.0, f) + } +} + +/// Timestamp as seconds since the unix epoch, or optionally an ISO 8601 string. +#[derive(Debug, Deserialize, Serialize)] +#[serde(untagged)] +pub(crate) enum Timestamp { + Seconds(serde_json::Number), + #[cfg(feature = "accept-rfc3339-timestamps")] + Rfc3339(String), +} + +impl Display for Timestamp { + fn fmt(&self, f: &mut Formatter) -> Result<(), std::fmt::Error> { + match self { + Timestamp::Seconds(seconds) => Display::fmt(seconds, f), + #[cfg(feature = "accept-rfc3339-timestamps")] + Timestamp::Rfc3339(iso) => Display::fmt(iso, f), + } + } +} + +pub(crate) fn timestamp_to_utc(timestamp: &Timestamp) -> Result, ()> { + match timestamp { + Timestamp::Seconds(seconds) => { + let (secs, nsecs) = if seconds.is_i64() { + (seconds.as_i64().ok_or(())?, 0u32) + } else { + let secs_f64 = seconds.as_f64().ok_or(())?; + let secs = secs_f64.floor(); + ( + secs as i64, + ((secs_f64 - secs) * 1_000_000_000.).floor() as u32, + ) + }; + Utc.timestamp_opt(secs, nsecs).single().ok_or(()) + } + #[cfg(feature = "accept-rfc3339-timestamps")] + Timestamp::Rfc3339(iso) => { + let datetime = DateTime::parse_from_rfc3339(iso).map_err(|_| ())?; + Ok(datetime.into()) + } + } +} + +pub mod serde_utc_seconds { + use crate::helpers::{timestamp_to_utc, utc_to_seconds, Timestamp}; + + use chrono::{DateTime, Utc}; + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + + pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + let seconds: Timestamp = Deserialize::deserialize(deserializer)?; + timestamp_to_utc(&seconds).map_err(|_| { + serde::de::Error::custom(format!( + "failed to parse `{}` as UTC datetime (in seconds)", + seconds + )) + }) + } + + pub fn serialize(v: &DateTime, serializer: S) -> Result + where + S: Serializer, + { + utc_to_seconds(v).serialize(serializer) + } +} + +pub mod serde_utc_seconds_opt { + use crate::helpers::{timestamp_to_utc, utc_to_seconds, Timestamp}; + + use chrono::{DateTime, Utc}; + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + + pub fn deserialize<'de, D>(deserializer: D) -> Result>, D::Error> + where + D: Deserializer<'de>, + { + let seconds: Option = Deserialize::deserialize(deserializer)?; + seconds + .map(|sec| { + timestamp_to_utc(&sec).map_err(|_| { + serde::de::Error::custom(format!( + "failed to parse `{}` as UTC datetime (in seconds)", + sec + )) + }) + }) + .transpose() + } + + pub fn serialize(v: &Option>, serializer: S) -> Result + where + S: Serializer, + { + v.map(|sec| utc_to_seconds(&sec)).serialize(serializer) + } +} + +// The spec is ambiguous about whether seconds should be expressed as integers, or +// whether floating-point values are allowed. For compatibility with a wide range of +// clients, we round down to the nearest second. +pub(crate) fn utc_to_seconds(utc: &DateTime) -> Timestamp { + Timestamp::Seconds(utc.timestamp().into()) +} + +new_type![ + #[derive(Deserialize, Hash, Serialize)] + pub(crate) Base64UrlEncodedBytes( + #[serde(with = "serde_base64url_byte_array")] + Vec + ) +]; + +mod serde_base64url_byte_array { + use crate::core::base64_url_safe_no_pad; + + use serde::de::Error; + use serde::{Deserialize, Deserializer, Serializer}; + use serde_json::{from_value, Value}; + + pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + let value: Value = Deserialize::deserialize(deserializer)?; + let base64_encoded: String = from_value(value).map_err(D::Error::custom)?; + + base64::decode_config(&base64_encoded, base64_url_safe_no_pad()).map_err(|err| { + D::Error::custom(format!( + "invalid base64url encoding `{}`: {:?}", + base64_encoded, err + )) + }) + } + + pub fn serialize(v: &[u8], serializer: S) -> Result + where + S: Serializer, + { + let base64_encoded = base64::encode_config(v, base64::URL_SAFE_NO_PAD); + serializer.serialize_str(&base64_encoded) + } +} diff --git a/src/id_token.rs b/src/id_token.rs deleted file mode 100644 index 34829d1..0000000 --- a/src/id_token.rs +++ /dev/null @@ -1,1133 +0,0 @@ -use crate::helpers::FilteredFlatten; -use crate::jwt::JsonWebTokenAccess; -use crate::jwt::{JsonWebTokenError, JsonWebTokenJsonPayloadSerde}; -use crate::types::helpers::{deserialize_string_or_vec, serde_utc_seconds, serde_utc_seconds_opt}; -use crate::types::LocalizedClaim; -use crate::{ - AccessToken, AccessTokenHash, AdditionalClaims, AddressClaim, Audience, AudiencesClaim, - AuthenticationContextClass, AuthenticationMethodReference, AuthorizationCode, - AuthorizationCodeHash, ClaimsVerificationError, ClientId, EndUserBirthday, EndUserEmail, - EndUserFamilyName, EndUserGivenName, EndUserMiddleName, EndUserName, EndUserNickname, - EndUserPhoneNumber, EndUserPictureUrl, EndUserProfileUrl, EndUserTimezone, EndUserUsername, - EndUserWebsiteUrl, ExtraTokenFields, GenderClaim, IdTokenVerifier, IssuerClaim, IssuerUrl, - JsonWebKey, JsonWebKeyType, JsonWebKeyUse, JsonWebToken, JsonWebTokenAlgorithm, - JweContentEncryptionAlgorithm, JwsSigningAlgorithm, LanguageTag, Nonce, NonceVerifier, - PrivateSigningKey, SigningError, StandardClaims, SubjectIdentifier, -}; - -use chrono::{DateTime, Utc}; -use serde::{Deserialize, Serialize}; -use serde_json::Value; - -use std::fmt::Debug; -use std::marker::PhantomData; -use std::str::FromStr; - -// This wrapper layer exists instead of directly verifying the JWT and returning the claims so that -// we can pass it around and easily access a serialized JWT representation of it (e.g., for passing -// to the authorization endpoint as an id_token_hint). -/// OpenID Connect ID token. -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] -pub struct IdToken< - AC: AdditionalClaims, - GC: GenderClaim, - JE: JweContentEncryptionAlgorithm, - JS: JwsSigningAlgorithm, - JT: JsonWebKeyType, ->( - #[serde(bound = "AC: AdditionalClaims")] - JsonWebToken, JsonWebTokenJsonPayloadSerde>, -); - -impl FromStr for IdToken -where - AC: AdditionalClaims, - GC: GenderClaim, - JE: JweContentEncryptionAlgorithm, - JS: JwsSigningAlgorithm, - JT: JsonWebKeyType, -{ - type Err = serde_json::Error; - fn from_str(s: &str) -> Result { - serde_json::from_value(Value::String(s.to_string())) - } -} - -impl IdToken -where - AC: AdditionalClaims, - GC: GenderClaim, - JE: JweContentEncryptionAlgorithm, - JS: JwsSigningAlgorithm, - JT: JsonWebKeyType, -{ - /// Initializes an ID token with the specified claims, signed using the given signing key and - /// algorithm. - /// - /// If an `access_token` and/or `code` are provided, this method sets the `at_hash` and/or - /// `c_hash` claims using the given signing algorithm, respectively. Otherwise, those claims are - /// unchanged from the values specified in `claims`. - pub fn new( - claims: IdTokenClaims, - signing_key: &S, - alg: JS, - access_token: Option<&AccessToken>, - code: Option<&AuthorizationCode>, - ) -> Result - where - JU: JsonWebKeyUse, - K: JsonWebKey, - S: PrivateSigningKey, - { - let at_hash = access_token - .map(|at| { - AccessTokenHash::from_token(at, &alg).map_err(JsonWebTokenError::SigningError) - }) - .transpose()? - .or_else(|| claims.access_token_hash.clone()); - let c_hash = code - .map(|c| { - AuthorizationCodeHash::from_code(c, &alg).map_err(JsonWebTokenError::SigningError) - }) - .transpose()? - .or_else(|| claims.code_hash.clone()); - - JsonWebToken::new( - IdTokenClaims { - access_token_hash: at_hash, - code_hash: c_hash, - ..claims - }, - signing_key, - &alg, - ) - .map(Self) - } - - /// Verifies and returns a reference to the ID token claims. - pub fn claims<'a, JU, K, N>( - &'a self, - verifier: &IdTokenVerifier, - nonce_verifier: N, - ) -> Result<&'a IdTokenClaims, ClaimsVerificationError> - where - JU: JsonWebKeyUse, - K: JsonWebKey, - N: NonceVerifier, - { - verifier.verified_claims(&self.0, nonce_verifier) - } - - /// Verifies and returns the ID token claims. - pub fn into_claims( - self, - verifier: &IdTokenVerifier, - nonce_verifier: N, - ) -> Result, ClaimsVerificationError> - where - JU: JsonWebKeyUse, - K: JsonWebKey, - N: NonceVerifier, - { - verifier.verified_claims_owned(self.0, nonce_verifier) - } - - /// Returns the [`JwsSigningAlgorithm`] used to sign this ID token. - /// - /// This function returns an error if the token is unsigned or utilizes JSON Web Encryption - /// (JWE). - pub fn signing_alg(&self) -> Result { - match self.0.unverified_header().alg { - JsonWebTokenAlgorithm::Signature(ref signing_alg, _) => Ok(signing_alg.clone()), - JsonWebTokenAlgorithm::Encryption(ref other) => Err(SigningError::UnsupportedAlg( - serde_plain::to_string(other).unwrap_or_else(|err| { - panic!( - "encryption alg {:?} failed to serialize to a string: {}", - other, err - ) - }), - )), - JsonWebTokenAlgorithm::None => Err(SigningError::UnsupportedAlg("none".to_string())), - } - } -} -impl ToString for IdToken -where - AC: AdditionalClaims, - GC: GenderClaim, - 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() - } -} - -/// OpenID Connect ID token claims. -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] -pub struct IdTokenClaims -where - AC: AdditionalClaims, - GC: GenderClaim, -{ - #[serde(rename = "iss")] - issuer: IssuerUrl, - // We always serialize as an array, which is valid according to the spec. This sets the - // 'default' attribute to be compatible with non-spec compliant OIDC providers that omit this - // field. - #[serde( - default, - rename = "aud", - deserialize_with = "deserialize_string_or_vec" - )] - audiences: Vec, - #[serde(rename = "exp", with = "serde_utc_seconds")] - expiration: DateTime, - #[serde(rename = "iat", with = "serde_utc_seconds")] - issue_time: DateTime, - #[serde( - default, - skip_serializing_if = "Option::is_none", - with = "serde_utc_seconds_opt" - )] - auth_time: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - nonce: Option, - #[serde(rename = "acr", skip_serializing_if = "Option::is_none")] - auth_context_ref: Option, - #[serde(rename = "amr", skip_serializing_if = "Option::is_none")] - auth_method_refs: Option>, - #[serde(rename = "azp", skip_serializing_if = "Option::is_none")] - authorized_party: Option, - #[serde(rename = "at_hash", skip_serializing_if = "Option::is_none")] - access_token_hash: Option, - #[serde(rename = "c_hash", skip_serializing_if = "Option::is_none")] - code_hash: Option, - - #[serde(bound = "GC: GenderClaim")] - #[serde(flatten)] - standard_claims: StandardClaims, - - #[serde(bound = "AC: AdditionalClaims")] - #[serde(flatten)] - additional_claims: FilteredFlatten, AC>, -} -impl IdTokenClaims -where - AC: AdditionalClaims, - GC: GenderClaim, -{ - /// Initializes new ID token claims. - pub fn new( - issuer: IssuerUrl, - audiences: Vec, - expiration: DateTime, - issue_time: DateTime, - standard_claims: StandardClaims, - additional_claims: AC, - ) -> Self { - Self { - issuer, - audiences, - expiration, - issue_time, - auth_time: None, - nonce: None, - auth_context_ref: None, - auth_method_refs: None, - authorized_party: None, - access_token_hash: None, - code_hash: None, - standard_claims, - additional_claims: additional_claims.into(), - } - } - - field_getters_setters![ - pub self [self] ["claim"] { - set_issuer -> issuer[IssuerUrl] ["iss"], - set_audiences -> audiences[Vec] ["aud"], - set_expiration -> expiration[DateTime] ["exp"], - set_issue_time -> issue_time[DateTime] ["iat"], - set_auth_time -> auth_time[Option>], - set_nonce -> nonce[Option], - set_auth_context_ref -> auth_context_ref[Option] ["acr"], - set_auth_method_refs -> auth_method_refs[Option>] ["amr"], - set_authorized_party -> authorized_party[Option] ["azp"], - set_access_token_hash -> access_token_hash[Option] ["at_hash"], - set_code_hash -> code_hash[Option] ["c_hash"], - } - ]; - - /// Returns the `sub` claim. - pub fn subject(&self) -> &SubjectIdentifier { - &self.standard_claims.sub - } - /// Sets the `sub` claim. - pub fn set_subject(mut self, subject: SubjectIdentifier) -> Self { - self.standard_claims.sub = subject; - self - } - - field_getters_setters![ - pub self [self.standard_claims] ["claim"] { - set_name -> name[Option>], - set_given_name -> given_name[Option>], - set_family_name -> - family_name[Option>], - set_middle_name -> - middle_name[Option>], - set_nickname -> nickname[Option>], - set_preferred_username -> preferred_username[Option], - set_profile -> profile[Option>], - set_picture -> picture[Option>], - set_website -> website[Option>], - set_email -> email[Option], - set_email_verified -> email_verified[Option], - set_gender -> gender[Option], - set_birthday -> birthday[Option], - set_birthdate -> birthdate[Option], - set_zoneinfo -> zoneinfo[Option], - set_locale -> locale[Option], - set_phone_number -> phone_number[Option], - set_phone_number_verified -> phone_number_verified[Option], - set_address -> address[Option], - set_updated_at -> updated_at[Option>], - } - ]; - - /// Returns additional ID token claims. - pub fn additional_claims(&self) -> &AC { - self.additional_claims.as_ref() - } - /// Returns mutable additional ID token claims. - pub fn additional_claims_mut(&mut self) -> &mut AC { - self.additional_claims.as_mut() - } -} -impl AudiencesClaim for IdTokenClaims -where - AC: AdditionalClaims, - GC: GenderClaim, -{ - fn audiences(&self) -> Option<&Vec> { - Some(IdTokenClaims::audiences(self)) - } -} -impl<'a, AC, GC> AudiencesClaim for &'a IdTokenClaims -where - AC: AdditionalClaims, - GC: GenderClaim, -{ - fn audiences(&self) -> Option<&Vec> { - Some(IdTokenClaims::audiences(self)) - } -} -impl IssuerClaim for IdTokenClaims -where - AC: AdditionalClaims, - GC: GenderClaim, -{ - fn issuer(&self) -> Option<&IssuerUrl> { - Some(IdTokenClaims::issuer(self)) - } -} -impl<'a, AC, GC> IssuerClaim for &'a IdTokenClaims -where - AC: AdditionalClaims, - GC: GenderClaim, -{ - fn issuer(&self) -> Option<&IssuerUrl> { - Some(IdTokenClaims::issuer(self)) - } -} - -/// Extends the base OAuth2 token response with an ID token. -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] -pub struct IdTokenFields -where - AC: AdditionalClaims, - EF: ExtraTokenFields, - GC: GenderClaim, - JE: JweContentEncryptionAlgorithm, - JS: JwsSigningAlgorithm, - JT: JsonWebKeyType, -{ - #[serde(bound = "AC: AdditionalClaims")] - id_token: Option>, - #[serde(bound = "EF: ExtraTokenFields", flatten)] - extra_fields: EF, - #[serde(skip)] - _phantom: PhantomData, -} -impl IdTokenFields -where - AC: AdditionalClaims, - EF: ExtraTokenFields, - GC: GenderClaim, - JE: JweContentEncryptionAlgorithm, - JS: JwsSigningAlgorithm, - JT: JsonWebKeyType, -{ - /// Initializes new ID token fields containing the specified [`IdToken`] and extra fields. - pub fn new(id_token: Option>, extra_fields: EF) -> Self { - Self { - id_token, - extra_fields, - _phantom: PhantomData, - } - } - - /// Returns the [`IdToken`] contained in the OAuth2 token response. - pub fn id_token(&self) -> Option<&IdToken> { - self.id_token.as_ref() - } - /// Returns the extra fields contained in the OAuth2 token response. - pub fn extra_fields(&self) -> &EF { - &self.extra_fields - } -} -impl ExtraTokenFields for IdTokenFields -where - AC: AdditionalClaims, - EF: ExtraTokenFields, - GC: GenderClaim, - JE: JweContentEncryptionAlgorithm, - JS: JwsSigningAlgorithm, - JT: JsonWebKeyType, -{ -} - -#[cfg(test)] -mod tests { - use crate::claims::{AdditionalClaims, EmptyAdditionalClaims, StandardClaims}; - use crate::core::{ - CoreGenderClaim, CoreIdToken, CoreIdTokenClaims, CoreTokenResponse, CoreTokenType, - }; - use crate::jwt::JsonWebTokenAccess; - use crate::{ - AccessTokenHash, AddressClaim, AddressCountry, AddressLocality, AddressPostalCode, - AddressRegion, Audience, AudiencesClaim, AuthenticationContextClass, - AuthenticationMethodReference, AuthorizationCodeHash, ClientId, EndUserBirthday, - EndUserEmail, EndUserFamilyName, EndUserGivenName, EndUserMiddleName, EndUserName, - EndUserNickname, EndUserPhoneNumber, EndUserPictureUrl, EndUserProfileUrl, EndUserTimezone, - EndUserUsername, EndUserWebsiteUrl, FormattedAddress, IdTokenClaims, IssuerClaim, - IssuerUrl, LanguageTag, Nonce, StreetAddress, SubjectIdentifier, - }; - - use chrono::{TimeZone, Utc}; - use oauth2::TokenResponse; - use serde::{Deserialize, Serialize}; - use url::Url; - - use std::collections::HashMap; - use std::str::FromStr; - - #[test] - fn test_id_token() { - static ID_TOKEN: &str = concat!( - "eyJhbGciOiJSUzI1NiJ9.", - "eyJpc3MiOiJodHRwczovL3NlcnZlci5leGFtcGxlLmNvbSIsImF1ZCI6WyJzNkJoZ", - "FJrcXQzIl0sImV4cCI6MTMxMTI4MTk3MCwiaWF0IjoxMzExMjgwOTcwLCJzdWIiOi", - "IyNDQwMDMyMCIsInRmYV9tZXRob2QiOiJ1MmYifQ.", - "aW52YWxpZF9zaWduYXR1cmU" - ); - - // `serde::Deserialize` implementation is tested within the `FromStr` implementation - let id_token = CoreIdToken::from_str(ID_TOKEN).expect("failed to parse id_token"); - - let claims = id_token.0.unverified_payload_ref(); - - assert_eq!( - *claims.issuer().url(), - Url::parse("https://server.example.com").unwrap() - ); - assert_eq!( - *claims.audiences(), - vec![Audience::new("s6BhdRkqt3".to_string())] - ); - assert_eq!( - claims.expiration(), - Utc.timestamp_opt(1311281970, 0) - .single() - .expect("valid timestamp") - ); - assert_eq!( - claims.issue_time(), - Utc.timestamp_opt(1311280970, 0) - .single() - .expect("valid timestamp") - ); - assert_eq!( - *claims.subject(), - SubjectIdentifier::new("24400320".to_string()) - ); - - // test `ToString` implementation - assert_eq!(&id_token.to_string(), ID_TOKEN); - - // test `serde::Serialize` implementation too - let de = serde_json::to_string(&id_token).expect("failed to deserializee id token"); - assert_eq!(de, format!("\"{}\"", ID_TOKEN)); - } - - #[test] - fn test_oauth2_response() { - let response_str = "{\ - \"access_token\":\"foobar\",\ - \"token_type\":\"bearer\",\ - \"id_token\":\"eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJodHRwczovL3NlcnZlci5leGFtcGxlLmNvbSIsImF\ - 1ZCI6WyJzNkJoZFJrcXQzIl0sImV4cCI6MTMxMTI4MTk3MCwiaWF0IjoxMzExMjgwOTcwLCJzdWIiOiIyNDQwMD\ - MyMCIsInRmYV9tZXRob2QiOiJ1MmYifQ.aW52YWxpZF9zaWduYXR1cmU\"\ - }"; - let response = - serde_json::from_str::(response_str).expect("failed to deserialize"); - - assert_eq!(*response.access_token().secret(), "foobar"); - assert_eq!(*response.token_type(), CoreTokenType::Bearer); - - let id_token = response.extra_fields().id_token(); - let claims = id_token.unwrap().0.unverified_payload_ref(); - - assert_eq!( - *claims.issuer().url(), - Url::parse("https://server.example.com").unwrap() - ); - assert_eq!( - *claims.audiences(), - vec![Audience::new("s6BhdRkqt3".to_string())] - ); - assert_eq!( - claims.expiration(), - Utc.timestamp_opt(1311281970, 0) - .single() - .expect("valid timestamp") - ); - assert_eq!( - claims.issue_time(), - Utc.timestamp_opt(1311280970, 0) - .single() - .expect("valid timestamp") - ); - assert_eq!( - *claims.subject(), - SubjectIdentifier::new("24400320".to_string()) - ); - - assert_eq!( - serde_json::to_string(&response).expect("failed to serialize"), - response_str - ); - } - - #[test] - fn test_minimal_claims_serde() { - let new_claims = CoreIdTokenClaims::new( - IssuerUrl::new("https://server.example.com".to_string()).unwrap(), - vec![Audience::new("s6BhdRkqt3".to_string())], - Utc.timestamp_opt(1311281970, 0) - .single() - .expect("valid timestamp"), - Utc.timestamp_opt(1311280970, 0) - .single() - .expect("valid timestamp"), - StandardClaims::new(SubjectIdentifier::new("24400320".to_string())), - EmptyAdditionalClaims {}, - ); - let expected_serialized_claims = "\ - {\ - \"iss\":\"https://server.example.com\",\ - \"aud\":[\"s6BhdRkqt3\"],\ - \"exp\":1311281970,\ - \"iat\":1311280970,\ - \"sub\":\"24400320\"\ - }"; - - let new_serialized_claims = - serde_json::to_string(&new_claims).expect("failed to serialize"); - assert_eq!(new_serialized_claims, expected_serialized_claims); - - let claims: CoreIdTokenClaims = serde_json::from_str( - "{ - \"iss\": \"https://server.example.com\", - \"sub\": \"24400320\", - \"aud\": \"s6BhdRkqt3\", - \"exp\": 1311281970, - \"iat\": 1311280970 - }", - ) - .expect("failed to deserialize"); - assert_eq!(claims, new_claims); - assert_eq!(claims.issuer().url(), new_claims.issuer().url()); - assert_eq!(claims.audiences(), new_claims.audiences()); - assert_eq!(claims.expiration(), new_claims.expiration()); - assert_eq!(claims.issue_time(), new_claims.issue_time()); - assert_eq!(claims.auth_time(), None); - assert!(claims.nonce().is_none()); - assert_eq!(claims.auth_context_ref(), None); - assert_eq!(claims.auth_method_refs(), None); - assert_eq!(claims.authorized_party(), None); - assert_eq!(claims.access_token_hash(), None); - assert_eq!(claims.code_hash(), None); - assert_eq!(*claims.additional_claims(), EmptyAdditionalClaims {}); - assert_eq!(claims.subject(), new_claims.subject()); - assert_eq!(claims.name(), None); - assert_eq!(claims.given_name(), None); - assert_eq!(claims.family_name(), None); - assert_eq!(claims.middle_name(), None); - assert_eq!(claims.nickname(), None); - assert_eq!(claims.preferred_username(), None); - assert_eq!(claims.profile(), None); - assert_eq!(claims.picture(), None); - assert_eq!(claims.website(), None); - assert_eq!(claims.email(), None); - assert_eq!(claims.email_verified(), None); - assert_eq!(claims.gender(), None); - assert_eq!(claims.birthday(), None); - assert_eq!(claims.birthdate(), None); - assert_eq!(claims.zoneinfo(), None); - assert_eq!(claims.locale(), None); - assert_eq!(claims.phone_number(), None); - assert_eq!(claims.phone_number_verified(), None); - assert_eq!(claims.address(), None); - assert_eq!(claims.updated_at(), None); - - let serialized_claims = serde_json::to_string(&claims).expect("failed to serialize"); - assert_eq!(serialized_claims, expected_serialized_claims); - - let claims_round_trip: CoreIdTokenClaims = - serde_json::from_str(&serialized_claims).expect("failed to deserialize"); - assert_eq!(claims, claims_round_trip); - } - - #[test] - fn test_complete_claims_serde() { - let claims_json = "{\ - \"iss\":\"https://server.example.com\",\ - \"aud\":[\"s6BhdRkqt3\"],\ - \"exp\":1311281970,\ - \"iat\":1311280970,\ - \"auth_time\":1311282970,\ - \"nonce\":\"Zm9vYmFy\",\ - \"acr\":\"urn:mace:incommon:iap:silver\",\ - \"amr\":[\"password\",\"totp\"],\ - \"azp\":\"dGhpc19jbGllbnQ\",\ - \"at_hash\":\"_JPLB-GtkomFJxAOWKHPHQ\",\ - \"c_hash\":\"VpTQii5T_8rgwxA-Wtb2Bw\",\ - \"sub\":\"24400320\",\ - \"name\":\"Homer Simpson\",\ - \"name#es\":\"Jomer Simpson\",\ - \"given_name\":\"Homer\",\ - \"given_name#es\":\"Jomer\",\ - \"family_name\":\"Simpson\",\ - \"family_name#es\":\"Simpson\",\ - \"middle_name\":\"Jay\",\ - \"middle_name#es\":\"Jay\",\ - \"nickname\":\"Homer\",\ - \"nickname#es\":\"Jomer\",\ - \"preferred_username\":\"homersimpson\",\ - \"profile\":\"https://example.com/profile?id=12345\",\ - \"profile#es\":\"https://example.com/profile?id=12345&lang=es\",\ - \"picture\":\"https://example.com/avatar?id=12345\",\ - \"picture#es\":\"https://example.com/avatar?id=12345&lang=es\",\ - \"website\":\"https://homersimpson.me\",\ - \"website#es\":\"https://homersimpson.me/?lang=es\",\ - \"email\":\"homer@homersimpson.me\",\ - \"email_verified\":true,\ - \"gender\":\"male\",\ - \"birthday\":\"1956-05-12\",\ - \"birthdate\":\"1956-07-12\",\ - \"zoneinfo\":\"America/Los_Angeles\",\ - \"locale\":\"en-US\",\ - \"phone_number\":\"+1 (555) 555-5555\",\ - \"phone_number_verified\":false,\ - \"address\":{\ - \"formatted\":\"1234 Hollywood Blvd., Los Angeles, CA 90210\",\ - \"street_address\":\"1234 Hollywood Blvd.\",\ - \"locality\":\"Los Angeles\",\ - \"region\":\"CA\",\ - \"postal_code\":\"90210\",\ - \"country\":\"US\"\ - },\ - \"updated_at\":1311283970\ - }"; - - let new_claims = CoreIdTokenClaims::new( - IssuerUrl::new("https://server.example.com".to_string()).unwrap(), - vec![Audience::new("s6BhdRkqt3".to_string())], - Utc.timestamp_opt(1311281970, 0) - .single() - .expect("valid timestamp"), - Utc.timestamp_opt(1311280970, 0) - .single() - .expect("valid timestamp"), - StandardClaims { - sub: SubjectIdentifier::new("24400320".to_string()), - name: Some( - vec![ - (None, EndUserName::new("Homer Simpson".to_string())), - ( - Some(LanguageTag::new("es".to_string())), - EndUserName::new("Jomer Simpson".to_string()), - ), - ] - .into_iter() - .collect(), - ), - given_name: Some( - vec![ - (None, EndUserGivenName::new("Homer".to_string())), - ( - Some(LanguageTag::new("es".to_string())), - EndUserGivenName::new("Jomer".to_string()), - ), - ] - .into_iter() - .collect(), - ), - family_name: Some( - vec![ - (None, EndUserFamilyName::new("Simpson".to_string())), - ( - Some(LanguageTag::new("es".to_string())), - EndUserFamilyName::new("Simpson".to_string()), - ), - ] - .into_iter() - .collect(), - ), - middle_name: Some( - vec![ - (None, EndUserMiddleName::new("Jay".to_string())), - ( - Some(LanguageTag::new("es".to_string())), - EndUserMiddleName::new("Jay".to_string()), - ), - ] - .into_iter() - .collect(), - ), - nickname: Some( - vec![ - (None, EndUserNickname::new("Homer".to_string())), - ( - Some(LanguageTag::new("es".to_string())), - EndUserNickname::new("Jomer".to_string()), - ), - ] - .into_iter() - .collect(), - ), - preferred_username: Some(EndUserUsername::new("homersimpson".to_string())), - profile: Some( - vec![ - ( - None, - EndUserProfileUrl::new( - "https://example.com/profile?id=12345".to_string(), - ), - ), - ( - Some(LanguageTag::new("es".to_string())), - EndUserProfileUrl::new( - "https://example.com/profile?id=12345&lang=es".to_string(), - ), - ), - ] - .into_iter() - .collect(), - ), - picture: Some( - vec![ - ( - None, - EndUserPictureUrl::new( - "https://example.com/avatar?id=12345".to_string(), - ), - ), - ( - Some(LanguageTag::new("es".to_string())), - EndUserPictureUrl::new( - "https://example.com/avatar?id=12345&lang=es".to_string(), - ), - ), - ] - .into_iter() - .collect(), - ), - website: Some( - vec![ - ( - None, - EndUserWebsiteUrl::new("https://homersimpson.me".to_string()), - ), - ( - Some(LanguageTag::new("es".to_string())), - EndUserWebsiteUrl::new("https://homersimpson.me/?lang=es".to_string()), - ), - ] - .into_iter() - .collect(), - ), - email: Some(EndUserEmail::new("homer@homersimpson.me".to_string())), - email_verified: Some(true), - gender: Some(CoreGenderClaim::new("male".to_string())), - birthday: Some(EndUserBirthday::new("1956-05-12".to_string())), - birthdate: Some(EndUserBirthday::new("1956-07-12".to_string())), - zoneinfo: Some(EndUserTimezone::new("America/Los_Angeles".to_string())), - locale: Some(LanguageTag::new("en-US".to_string())), - phone_number: Some(EndUserPhoneNumber::new("+1 (555) 555-5555".to_string())), - phone_number_verified: Some(false), - address: Some(AddressClaim { - formatted: Some(FormattedAddress::new( - "1234 Hollywood Blvd., Los Angeles, CA 90210".to_string(), - )), - street_address: Some(StreetAddress::new("1234 Hollywood Blvd.".to_string())), - locality: Some(AddressLocality::new("Los Angeles".to_string())), - region: Some(AddressRegion::new("CA".to_string())), - postal_code: Some(AddressPostalCode::new("90210".to_string())), - country: Some(AddressCountry::new("US".to_string())), - }), - updated_at: Some( - Utc.timestamp_opt(1311283970, 0) - .single() - .expect("valid timestamp"), - ), - }, - EmptyAdditionalClaims {}, - ) - .set_auth_time(Some( - Utc.timestamp_opt(1311282970, 0) - .single() - .expect("valid timestamp"), - )) - .set_nonce(Some(Nonce::new("Zm9vYmFy".to_string()))) - .set_auth_context_ref(Some(AuthenticationContextClass::new( - "urn:mace:incommon:iap:silver".to_string(), - ))) - .set_auth_method_refs(Some(vec![ - AuthenticationMethodReference::new("password".to_string()), - AuthenticationMethodReference::new("totp".to_string()), - ])) - .set_authorized_party(Some(ClientId::new("dGhpc19jbGllbnQ".to_string()))) - .set_access_token_hash(Some(AccessTokenHash::new( - "_JPLB-GtkomFJxAOWKHPHQ".to_string(), - ))) - .set_code_hash(Some(AuthorizationCodeHash::new( - "VpTQii5T_8rgwxA-Wtb2Bw".to_string(), - ))); - - let claims: CoreIdTokenClaims = - serde_json::from_str(claims_json).expect("failed to deserialize"); - assert_eq!(claims, new_claims); - assert_eq!(claims.issuer(), new_claims.issuer()); - assert_eq!(claims.issuer().url(), new_claims.issuer().url()); - assert_eq!(claims.audiences(), new_claims.audiences()); - assert_eq!(claims.expiration(), new_claims.expiration()); - assert_eq!(claims.issue_time(), new_claims.issue_time()); - assert_eq!(claims.auth_time(), new_claims.auth_time()); - assert_eq!( - claims.nonce().unwrap().secret(), - new_claims.nonce().unwrap().secret() - ); - assert_eq!(claims.auth_context_ref(), new_claims.auth_context_ref()); - assert_eq!(claims.auth_method_refs(), new_claims.auth_method_refs()); - assert_eq!(claims.authorized_party(), new_claims.authorized_party()); - assert_eq!(claims.access_token_hash(), new_claims.access_token_hash()); - assert_eq!(claims.code_hash(), new_claims.code_hash()); - assert_eq!(*claims.additional_claims(), EmptyAdditionalClaims {}); - assert_eq!(claims.subject(), new_claims.subject()); - assert_eq!(claims.name(), new_claims.name()); - assert_eq!(claims.given_name(), new_claims.given_name()); - assert_eq!(claims.family_name(), new_claims.family_name()); - assert_eq!(claims.middle_name(), new_claims.middle_name()); - assert_eq!(claims.nickname(), new_claims.nickname()); - assert_eq!(claims.preferred_username(), new_claims.preferred_username()); - assert_eq!(claims.preferred_username(), new_claims.preferred_username()); - assert_eq!(claims.profile(), new_claims.profile()); - assert_eq!(claims.picture(), new_claims.picture()); - assert_eq!(claims.website(), new_claims.website()); - assert_eq!(claims.email(), new_claims.email()); - assert_eq!(claims.email_verified(), new_claims.email_verified()); - assert_eq!(claims.gender(), new_claims.gender()); - assert_eq!(claims.birthday(), new_claims.birthday()); - assert_eq!(claims.birthdate(), new_claims.birthdate()); - assert_eq!(claims.zoneinfo(), new_claims.zoneinfo()); - assert_eq!(claims.locale(), new_claims.locale()); - assert_eq!(claims.phone_number(), new_claims.phone_number(),); - assert_eq!( - claims.phone_number_verified(), - new_claims.phone_number_verified() - ); - assert_eq!(claims.address(), new_claims.address()); - assert_eq!(claims.updated_at(), new_claims.updated_at()); - - let serialized_claims = serde_json::to_string(&claims).expect("failed to serialize"); - let claims_round_trip: CoreIdTokenClaims = - serde_json::from_str(&serialized_claims).expect("failed to deserialize"); - assert_eq!(claims, claims_round_trip); - - let serialized_new_claims = - serde_json::to_string(&new_claims).expect("failed to serialize"); - assert_eq!(serialized_new_claims, claims_json); - } - - // See https://github.com/ramosbugs/openidconnect-rs/issues/23 - #[test] - #[cfg(feature = "accept-rfc3339-timestamps")] - fn test_accept_rfc3339_timestamp() { - let claims: CoreIdTokenClaims = serde_json::from_str( - "{ - \"iss\": \"https://server.example.com\", - \"sub\": \"24400320\", - \"aud\": \"s6BhdRkqt3\", - \"exp\": 1311281970, - \"iat\": 1311280970, - \"updated_at\": \"2021-12-22T02:10:37.000Z\" - }", - ) - .expect("failed to deserialize"); - assert_eq!( - claims.updated_at(), - Some( - Utc.timestamp_opt(1640139037, 0) - .single() - .expect("valid timestamp") - ) - ); - } - - #[test] - fn test_unknown_claims_serde() { - let expected_serialized_claims = "{\ - \"iss\":\"https://server.example.com\",\ - \"aud\":[\"s6BhdRkqt3\"],\ - \"exp\":1311281970,\ - \"iat\":1311280970,\ - \"sub\":\"24400320\"\ - }"; - - let claims: CoreIdTokenClaims = serde_json::from_str( - "{ - \"iss\": \"https://server.example.com\", - \"sub\": \"24400320\", - \"aud\": \"s6BhdRkqt3\", - \"exp\": 1311281970, - \"iat\": 1311280970, - \"some_other_field\":\"some_other_value\"\ - }", - ) - .expect("failed to deserialize"); - - let serialized_claims = serde_json::to_string(&claims).expect("failed to serialize"); - assert_eq!(serialized_claims, expected_serialized_claims); - - let claims_round_trip: CoreIdTokenClaims = - serde_json::from_str(&serialized_claims).expect("failed to deserialize"); - assert_eq!(claims, claims_round_trip); - } - - #[test] - fn test_audience() { - let single_aud_str_claims = serde_json::from_str::( - "{ - \"iss\": \"https://server.example.com\", - \"sub\": \"24400320\", - \"aud\": \"s6BhdRkqt3\", - \"exp\": 1311281970, - \"iat\": 1311280970 - }", - ) - .expect("failed to deserialize"); - assert_eq!( - *single_aud_str_claims.audiences(), - vec![Audience::new("s6BhdRkqt3".to_string())], - ); - - // We always serialize aud as an array, which is valid according to the spec. - assert_eq!( - serde_json::to_string(&single_aud_str_claims).expect("failed to serialize"), - "{\ - \"iss\":\"https://server.example.com\",\ - \"aud\":[\"s6BhdRkqt3\"],\ - \"exp\":1311281970,\ - \"iat\":1311280970,\ - \"sub\":\"24400320\"\ - }", - ); - - let single_aud_vec_claims = serde_json::from_str::( - "{ - \"iss\": \"https://server.example.com\", - \"sub\": \"24400320\", - \"aud\": [\"s6BhdRkqt3\"], - \"exp\": 1311281970, - \"iat\": 1311280970 - }", - ) - .expect("failed to deserialize"); - assert_eq!( - *single_aud_vec_claims.audiences(), - vec![Audience::new("s6BhdRkqt3".to_string())], - ); - assert_eq!( - serde_json::to_string(&single_aud_vec_claims).expect("failed to serialize"), - "{\ - \"iss\":\"https://server.example.com\",\ - \"aud\":[\"s6BhdRkqt3\"],\ - \"exp\":1311281970,\ - \"iat\":1311280970,\ - \"sub\":\"24400320\"\ - }", - ); - - let multi_aud_claims = serde_json::from_str::( - "{\ - \"iss\": \"https://server.example.com\", - \"sub\": \"24400320\", - \"aud\": [\"s6BhdRkqt3\", \"aud2\"], - \"exp\": 1311281970, - \"iat\": 1311280970 - }", - ) - .expect("failed to deserialize"); - assert_eq!( - *multi_aud_claims.audiences(), - vec![ - Audience::new("s6BhdRkqt3".to_string()), - Audience::new("aud2".to_string()) - ], - ); - assert_eq!( - serde_json::to_string(&multi_aud_claims).expect("failed to serialize"), - "{\ - \"iss\":\"https://server.example.com\",\ - \"aud\":[\"s6BhdRkqt3\",\"aud2\"],\ - \"exp\":1311281970,\ - \"iat\":1311280970,\ - \"sub\":\"24400320\"\ - }", - ); - } - - #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] - struct TestClaims { - pub tfa_method: String, - } - impl AdditionalClaims for TestClaims {} - - #[test] - fn test_additional_claims() { - let claims = serde_json::from_str::>( - "{ - \"iss\": \"https://server.example.com\", - \"sub\": \"24400320\", - \"aud\": [\"s6BhdRkqt3\"], - \"exp\": 1311281970, - \"iat\": 1311280970, - \"tfa_method\": \"u2f\" - }", - ) - .expect("failed to deserialize"); - assert_eq!(claims.additional_claims().tfa_method, "u2f"); - assert_eq!( - serde_json::to_string(&claims).expect("failed to serialize"), - "{\ - \"iss\":\"https://server.example.com\",\ - \"aud\":[\"s6BhdRkqt3\"],\ - \"exp\":1311281970,\ - \"iat\":1311280970,\ - \"sub\":\"24400320\",\ - \"tfa_method\":\"u2f\"\ - }", - ); - - serde_json::from_str::>( - "{ - \"iss\": \"https://server.example.com\", - \"sub\": \"24400320\", - \"aud\": [\"s6BhdRkqt3\"], - \"exp\": 1311281970, - \"iat\": 1311280970 - }", - ) - .expect_err("missing claim should fail to deserialize"); - } - - #[derive(Debug, Deserialize, Serialize)] - struct AllOtherClaims(HashMap); - impl AdditionalClaims for AllOtherClaims {} - - #[test] - fn test_catch_all_additional_claims() { - let claims = serde_json::from_str::>( - "{ - \"iss\": \"https://server.example.com\", - \"sub\": \"24400320\", - \"aud\": [\"s6BhdRkqt3\"], - \"exp\": 1311281970, - \"iat\": 1311280970, - \"tfa_method\": \"u2f\", - \"updated_at\": 1000 - }", - ) - .expect("failed to deserialize"); - - assert_eq!(claims.additional_claims().0.len(), 1); - assert_eq!(claims.additional_claims().0["tfa_method"], "u2f"); - } - - #[test] - fn test_audiences_claim() { - let claims = serde_json::from_str::( - "{ - \"iss\": \"https://server.example.com\", - \"sub\": \"24400320\", - \"aud\": \"s6BhdRkqt3\", - \"exp\": 1311281970, - \"iat\": 1311280970 - }", - ) - .expect("failed to deserialize"); - - fn verify_audiences(audiences_claim: &A) { - assert_eq!( - (*audiences_claim).audiences(), - Some(&vec![Audience::new("s6BhdRkqt3".to_string())]), - ) - } - verify_audiences(&claims); - verify_audiences(&&claims); - } - - #[test] - fn test_issuer_claim() { - let claims = serde_json::from_str::( - "{ - \"iss\": \"https://server.example.com\", - \"sub\": \"24400320\", - \"aud\": \"s6BhdRkqt3\", - \"exp\": 1311281970, - \"iat\": 1311280970 - }", - ) - .expect("failed to deserialize"); - - fn verify_issuer(issuer_claim: &I) { - assert_eq!( - (*issuer_claim).issuer(), - Some(&IssuerUrl::new("https://server.example.com".to_string()).unwrap()), - ) - } - verify_issuer(&claims); - verify_issuer(&&claims); - } -} diff --git a/src/id_token/mod.rs b/src/id_token/mod.rs new file mode 100644 index 0000000..902e82d --- /dev/null +++ b/src/id_token/mod.rs @@ -0,0 +1,411 @@ +use crate::helpers::{ + deserialize_string_or_vec, serde_utc_seconds, serde_utc_seconds_opt, FilteredFlatten, +}; +use crate::jwt::JsonWebTokenAccess; +use crate::jwt::{JsonWebTokenError, JsonWebTokenJsonPayloadSerde}; +use crate::types::jwk::{JsonWebKeyType, JwsSigningAlgorithm}; +use crate::{ + AccessToken, AccessTokenHash, AdditionalClaims, AddressClaim, Audience, AudiencesClaim, + AuthenticationContextClass, AuthenticationMethodReference, AuthorizationCode, + AuthorizationCodeHash, ClaimsVerificationError, ClientId, EndUserBirthday, EndUserEmail, + EndUserFamilyName, EndUserGivenName, EndUserMiddleName, EndUserName, EndUserNickname, + EndUserPhoneNumber, EndUserPictureUrl, EndUserProfileUrl, EndUserTimezone, EndUserUsername, + EndUserWebsiteUrl, ExtraTokenFields, GenderClaim, IdTokenVerifier, IssuerClaim, IssuerUrl, + JsonWebKey, JsonWebKeyUse, JsonWebToken, JsonWebTokenAlgorithm, JweContentEncryptionAlgorithm, + LanguageTag, LocalizedClaim, Nonce, NonceVerifier, PrivateSigningKey, SigningError, + StandardClaims, SubjectIdentifier, +}; + +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +use std::fmt::Debug; +use std::marker::PhantomData; +use std::str::FromStr; + +#[cfg(test)] +mod tests; + +// This wrapper layer exists instead of directly verifying the JWT and returning the claims so that +// we can pass it around and easily access a serialized JWT representation of it (e.g., for passing +// to the authorization endpoint as an id_token_hint). +/// OpenID Connect ID token. +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub struct IdToken< + AC: AdditionalClaims, + GC: GenderClaim, + JE: JweContentEncryptionAlgorithm, + JS: JwsSigningAlgorithm, + JT: JsonWebKeyType, +>( + #[serde(bound = "AC: AdditionalClaims")] + JsonWebToken, JsonWebTokenJsonPayloadSerde>, +); + +impl FromStr for IdToken +where + AC: AdditionalClaims, + GC: GenderClaim, + JE: JweContentEncryptionAlgorithm, + JS: JwsSigningAlgorithm, + JT: JsonWebKeyType, +{ + type Err = serde_json::Error; + fn from_str(s: &str) -> Result { + serde_json::from_value(Value::String(s.to_string())) + } +} + +impl IdToken +where + AC: AdditionalClaims, + GC: GenderClaim, + JE: JweContentEncryptionAlgorithm, + JS: JwsSigningAlgorithm, + JT: JsonWebKeyType, +{ + /// Initializes an ID token with the specified claims, signed using the given signing key and + /// algorithm. + /// + /// If an `access_token` and/or `code` are provided, this method sets the `at_hash` and/or + /// `c_hash` claims using the given signing algorithm, respectively. Otherwise, those claims are + /// unchanged from the values specified in `claims`. + pub fn new( + claims: IdTokenClaims, + signing_key: &S, + alg: JS, + access_token: Option<&AccessToken>, + code: Option<&AuthorizationCode>, + ) -> Result + where + JU: JsonWebKeyUse, + K: JsonWebKey, + S: PrivateSigningKey, + { + let at_hash = access_token + .map(|at| { + AccessTokenHash::from_token(at, &alg).map_err(JsonWebTokenError::SigningError) + }) + .transpose()? + .or_else(|| claims.access_token_hash.clone()); + let c_hash = code + .map(|c| { + AuthorizationCodeHash::from_code(c, &alg).map_err(JsonWebTokenError::SigningError) + }) + .transpose()? + .or_else(|| claims.code_hash.clone()); + + JsonWebToken::new( + IdTokenClaims { + access_token_hash: at_hash, + code_hash: c_hash, + ..claims + }, + signing_key, + &alg, + ) + .map(Self) + } + + /// Verifies and returns a reference to the ID token claims. + pub fn claims<'a, JU, K, N>( + &'a self, + verifier: &IdTokenVerifier, + nonce_verifier: N, + ) -> Result<&'a IdTokenClaims, ClaimsVerificationError> + where + JU: JsonWebKeyUse, + K: JsonWebKey, + N: NonceVerifier, + { + verifier.verified_claims(&self.0, nonce_verifier) + } + + /// Verifies and returns the ID token claims. + pub fn into_claims( + self, + verifier: &IdTokenVerifier, + nonce_verifier: N, + ) -> Result, ClaimsVerificationError> + where + JU: JsonWebKeyUse, + K: JsonWebKey, + N: NonceVerifier, + { + verifier.verified_claims_owned(self.0, nonce_verifier) + } + + /// Returns the [`JwsSigningAlgorithm`] used to sign this ID token. + /// + /// This function returns an error if the token is unsigned or utilizes JSON Web Encryption + /// (JWE). + pub fn signing_alg(&self) -> Result { + match self.0.unverified_header().alg { + JsonWebTokenAlgorithm::Signature(ref signing_alg, _) => Ok(signing_alg.clone()), + JsonWebTokenAlgorithm::Encryption(ref other) => Err(SigningError::UnsupportedAlg( + serde_plain::to_string(other).unwrap_or_else(|err| { + panic!( + "encryption alg {:?} failed to serialize to a string: {}", + other, err + ) + }), + )), + JsonWebTokenAlgorithm::None => Err(SigningError::UnsupportedAlg("none".to_string())), + } + } +} +impl ToString for IdToken +where + AC: AdditionalClaims, + GC: GenderClaim, + 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() + } +} + +/// OpenID Connect ID token claims. +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub struct IdTokenClaims +where + AC: AdditionalClaims, + GC: GenderClaim, +{ + #[serde(rename = "iss")] + issuer: IssuerUrl, + // We always serialize as an array, which is valid according to the spec. This sets the + // 'default' attribute to be compatible with non-spec compliant OIDC providers that omit this + // field. + #[serde( + default, + rename = "aud", + deserialize_with = "deserialize_string_or_vec" + )] + audiences: Vec, + #[serde(rename = "exp", with = "serde_utc_seconds")] + expiration: DateTime, + #[serde(rename = "iat", with = "serde_utc_seconds")] + issue_time: DateTime, + #[serde( + default, + skip_serializing_if = "Option::is_none", + with = "serde_utc_seconds_opt" + )] + auth_time: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + nonce: Option, + #[serde(rename = "acr", skip_serializing_if = "Option::is_none")] + auth_context_ref: Option, + #[serde(rename = "amr", skip_serializing_if = "Option::is_none")] + auth_method_refs: Option>, + #[serde(rename = "azp", skip_serializing_if = "Option::is_none")] + authorized_party: Option, + #[serde(rename = "at_hash", skip_serializing_if = "Option::is_none")] + access_token_hash: Option, + #[serde(rename = "c_hash", skip_serializing_if = "Option::is_none")] + code_hash: Option, + + #[serde(bound = "GC: GenderClaim")] + #[serde(flatten)] + standard_claims: StandardClaims, + + #[serde(bound = "AC: AdditionalClaims")] + #[serde(flatten)] + additional_claims: FilteredFlatten, AC>, +} +impl IdTokenClaims +where + AC: AdditionalClaims, + GC: GenderClaim, +{ + /// Initializes new ID token claims. + pub fn new( + issuer: IssuerUrl, + audiences: Vec, + expiration: DateTime, + issue_time: DateTime, + standard_claims: StandardClaims, + additional_claims: AC, + ) -> Self { + Self { + issuer, + audiences, + expiration, + issue_time, + auth_time: None, + nonce: None, + auth_context_ref: None, + auth_method_refs: None, + authorized_party: None, + access_token_hash: None, + code_hash: None, + standard_claims, + additional_claims: additional_claims.into(), + } + } + + field_getters_setters![ + pub self [self] ["claim"] { + set_issuer -> issuer[IssuerUrl] ["iss"], + set_audiences -> audiences[Vec] ["aud"], + set_expiration -> expiration[DateTime] ["exp"], + set_issue_time -> issue_time[DateTime] ["iat"], + set_auth_time -> auth_time[Option>], + set_nonce -> nonce[Option], + set_auth_context_ref -> auth_context_ref[Option] ["acr"], + set_auth_method_refs -> auth_method_refs[Option>] ["amr"], + set_authorized_party -> authorized_party[Option] ["azp"], + set_access_token_hash -> access_token_hash[Option] ["at_hash"], + set_code_hash -> code_hash[Option] ["c_hash"], + } + ]; + + /// Returns the `sub` claim. + pub fn subject(&self) -> &SubjectIdentifier { + &self.standard_claims.sub + } + /// Sets the `sub` claim. + pub fn set_subject(mut self, subject: SubjectIdentifier) -> Self { + self.standard_claims.sub = subject; + self + } + + field_getters_setters![ + pub self [self.standard_claims] ["claim"] { + set_name -> name[Option>], + set_given_name -> given_name[Option>], + set_family_name -> + family_name[Option>], + set_middle_name -> + middle_name[Option>], + set_nickname -> nickname[Option>], + set_preferred_username -> preferred_username[Option], + set_profile -> profile[Option>], + set_picture -> picture[Option>], + set_website -> website[Option>], + set_email -> email[Option], + set_email_verified -> email_verified[Option], + set_gender -> gender[Option], + set_birthday -> birthday[Option], + set_birthdate -> birthdate[Option], + set_zoneinfo -> zoneinfo[Option], + set_locale -> locale[Option], + set_phone_number -> phone_number[Option], + set_phone_number_verified -> phone_number_verified[Option], + set_address -> address[Option], + set_updated_at -> updated_at[Option>], + } + ]; + + /// Returns additional ID token claims. + pub fn additional_claims(&self) -> &AC { + self.additional_claims.as_ref() + } + /// Returns mutable additional ID token claims. + pub fn additional_claims_mut(&mut self) -> &mut AC { + self.additional_claims.as_mut() + } +} +impl AudiencesClaim for IdTokenClaims +where + AC: AdditionalClaims, + GC: GenderClaim, +{ + fn audiences(&self) -> Option<&Vec> { + Some(IdTokenClaims::audiences(self)) + } +} +impl<'a, AC, GC> AudiencesClaim for &'a IdTokenClaims +where + AC: AdditionalClaims, + GC: GenderClaim, +{ + fn audiences(&self) -> Option<&Vec> { + Some(IdTokenClaims::audiences(self)) + } +} +impl IssuerClaim for IdTokenClaims +where + AC: AdditionalClaims, + GC: GenderClaim, +{ + fn issuer(&self) -> Option<&IssuerUrl> { + Some(IdTokenClaims::issuer(self)) + } +} +impl<'a, AC, GC> IssuerClaim for &'a IdTokenClaims +where + AC: AdditionalClaims, + GC: GenderClaim, +{ + fn issuer(&self) -> Option<&IssuerUrl> { + Some(IdTokenClaims::issuer(self)) + } +} + +/// Extends the base OAuth2 token response with an ID token. +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub struct IdTokenFields +where + AC: AdditionalClaims, + EF: ExtraTokenFields, + GC: GenderClaim, + JE: JweContentEncryptionAlgorithm, + JS: JwsSigningAlgorithm, + JT: JsonWebKeyType, +{ + #[serde(bound = "AC: AdditionalClaims")] + id_token: Option>, + #[serde(bound = "EF: ExtraTokenFields", flatten)] + extra_fields: EF, + #[serde(skip)] + _phantom: PhantomData, +} +impl IdTokenFields +where + AC: AdditionalClaims, + EF: ExtraTokenFields, + GC: GenderClaim, + JE: JweContentEncryptionAlgorithm, + JS: JwsSigningAlgorithm, + JT: JsonWebKeyType, +{ + /// Initializes new ID token fields containing the specified [`IdToken`] and extra fields. + pub fn new(id_token: Option>, extra_fields: EF) -> Self { + Self { + id_token, + extra_fields, + _phantom: PhantomData, + } + } + + /// Returns the [`IdToken`] contained in the OAuth2 token response. + pub fn id_token(&self) -> Option<&IdToken> { + self.id_token.as_ref() + } + /// Returns the extra fields contained in the OAuth2 token response. + pub fn extra_fields(&self) -> &EF { + &self.extra_fields + } +} +impl ExtraTokenFields for IdTokenFields +where + AC: AdditionalClaims, + EF: ExtraTokenFields, + GC: GenderClaim, + JE: JweContentEncryptionAlgorithm, + JS: JwsSigningAlgorithm, + JT: JsonWebKeyType, +{ +} diff --git a/src/id_token/tests.rs b/src/id_token/tests.rs new file mode 100644 index 0000000..304ab8c --- /dev/null +++ b/src/id_token/tests.rs @@ -0,0 +1,716 @@ +use crate::claims::{AdditionalClaims, EmptyAdditionalClaims, StandardClaims}; +use crate::core::{ + CoreGenderClaim, CoreIdToken, CoreIdTokenClaims, CoreTokenResponse, CoreTokenType, +}; +use crate::jwt::JsonWebTokenAccess; +use crate::{ + AccessTokenHash, AddressClaim, AddressCountry, AddressLocality, AddressPostalCode, + AddressRegion, Audience, AudiencesClaim, AuthenticationContextClass, + AuthenticationMethodReference, AuthorizationCodeHash, ClientId, EndUserBirthday, EndUserEmail, + EndUserFamilyName, EndUserGivenName, EndUserMiddleName, EndUserName, EndUserNickname, + EndUserPhoneNumber, EndUserPictureUrl, EndUserProfileUrl, EndUserTimezone, EndUserUsername, + EndUserWebsiteUrl, FormattedAddress, IdTokenClaims, IssuerClaim, IssuerUrl, LanguageTag, Nonce, + StreetAddress, SubjectIdentifier, +}; + +use chrono::{TimeZone, Utc}; +use oauth2::TokenResponse; +use serde::{Deserialize, Serialize}; +use url::Url; + +use std::collections::HashMap; +use std::str::FromStr; + +#[test] +fn test_id_token() { + static ID_TOKEN: &str = concat!( + "eyJhbGciOiJSUzI1NiJ9.", + "eyJpc3MiOiJodHRwczovL3NlcnZlci5leGFtcGxlLmNvbSIsImF1ZCI6WyJzNkJoZ", + "FJrcXQzIl0sImV4cCI6MTMxMTI4MTk3MCwiaWF0IjoxMzExMjgwOTcwLCJzdWIiOi", + "IyNDQwMDMyMCIsInRmYV9tZXRob2QiOiJ1MmYifQ.", + "aW52YWxpZF9zaWduYXR1cmU" + ); + + // `serde::Deserialize` implementation is tested within the `FromStr` implementation + let id_token = CoreIdToken::from_str(ID_TOKEN).expect("failed to parse id_token"); + + let claims = id_token.0.unverified_payload_ref(); + + assert_eq!( + *claims.issuer().url(), + Url::parse("https://server.example.com").unwrap() + ); + assert_eq!( + *claims.audiences(), + vec![Audience::new("s6BhdRkqt3".to_string())] + ); + assert_eq!( + claims.expiration(), + Utc.timestamp_opt(1311281970, 0) + .single() + .expect("valid timestamp") + ); + assert_eq!( + claims.issue_time(), + Utc.timestamp_opt(1311280970, 0) + .single() + .expect("valid timestamp") + ); + assert_eq!( + *claims.subject(), + SubjectIdentifier::new("24400320".to_string()) + ); + + // test `ToString` implementation + assert_eq!(&id_token.to_string(), ID_TOKEN); + + // test `serde::Serialize` implementation too + let de = serde_json::to_string(&id_token).expect("failed to deserializee id token"); + assert_eq!(de, format!("\"{}\"", ID_TOKEN)); +} + +#[test] +fn test_oauth2_response() { + let response_str = "{\ + \"access_token\":\"foobar\",\ + \"token_type\":\"bearer\",\ + \"id_token\":\"eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJodHRwczovL3NlcnZlci5leGFtcGxlLmNvbSIsImF\ + 1ZCI6WyJzNkJoZFJrcXQzIl0sImV4cCI6MTMxMTI4MTk3MCwiaWF0IjoxMzExMjgwOTcwLCJzdWIiOiIyNDQwMD\ + MyMCIsInRmYV9tZXRob2QiOiJ1MmYifQ.aW52YWxpZF9zaWduYXR1cmU\"\ + }"; + let response = + serde_json::from_str::(response_str).expect("failed to deserialize"); + + assert_eq!(*response.access_token().secret(), "foobar"); + assert_eq!(*response.token_type(), CoreTokenType::Bearer); + + let id_token = response.extra_fields().id_token(); + let claims = id_token.unwrap().0.unverified_payload_ref(); + + assert_eq!( + *claims.issuer().url(), + Url::parse("https://server.example.com").unwrap() + ); + assert_eq!( + *claims.audiences(), + vec![Audience::new("s6BhdRkqt3".to_string())] + ); + assert_eq!( + claims.expiration(), + Utc.timestamp_opt(1311281970, 0) + .single() + .expect("valid timestamp") + ); + assert_eq!( + claims.issue_time(), + Utc.timestamp_opt(1311280970, 0) + .single() + .expect("valid timestamp") + ); + assert_eq!( + *claims.subject(), + SubjectIdentifier::new("24400320".to_string()) + ); + + assert_eq!( + serde_json::to_string(&response).expect("failed to serialize"), + response_str + ); +} + +#[test] +fn test_minimal_claims_serde() { + let new_claims = CoreIdTokenClaims::new( + IssuerUrl::new("https://server.example.com".to_string()).unwrap(), + vec![Audience::new("s6BhdRkqt3".to_string())], + Utc.timestamp_opt(1311281970, 0) + .single() + .expect("valid timestamp"), + Utc.timestamp_opt(1311280970, 0) + .single() + .expect("valid timestamp"), + StandardClaims::new(SubjectIdentifier::new("24400320".to_string())), + EmptyAdditionalClaims {}, + ); + let expected_serialized_claims = "\ + {\ + \"iss\":\"https://server.example.com\",\ + \"aud\":[\"s6BhdRkqt3\"],\ + \"exp\":1311281970,\ + \"iat\":1311280970,\ + \"sub\":\"24400320\"\ + }"; + + let new_serialized_claims = serde_json::to_string(&new_claims).expect("failed to serialize"); + assert_eq!(new_serialized_claims, expected_serialized_claims); + + let claims: CoreIdTokenClaims = serde_json::from_str( + "{ + \"iss\": \"https://server.example.com\", + \"sub\": \"24400320\", + \"aud\": \"s6BhdRkqt3\", + \"exp\": 1311281970, + \"iat\": 1311280970 + }", + ) + .expect("failed to deserialize"); + assert_eq!(claims, new_claims); + assert_eq!(claims.issuer().url(), new_claims.issuer().url()); + assert_eq!(claims.audiences(), new_claims.audiences()); + assert_eq!(claims.expiration(), new_claims.expiration()); + assert_eq!(claims.issue_time(), new_claims.issue_time()); + assert_eq!(claims.auth_time(), None); + assert!(claims.nonce().is_none()); + assert_eq!(claims.auth_context_ref(), None); + assert_eq!(claims.auth_method_refs(), None); + assert_eq!(claims.authorized_party(), None); + assert_eq!(claims.access_token_hash(), None); + assert_eq!(claims.code_hash(), None); + assert_eq!(*claims.additional_claims(), EmptyAdditionalClaims {}); + assert_eq!(claims.subject(), new_claims.subject()); + assert_eq!(claims.name(), None); + assert_eq!(claims.given_name(), None); + assert_eq!(claims.family_name(), None); + assert_eq!(claims.middle_name(), None); + assert_eq!(claims.nickname(), None); + assert_eq!(claims.preferred_username(), None); + assert_eq!(claims.profile(), None); + assert_eq!(claims.picture(), None); + assert_eq!(claims.website(), None); + assert_eq!(claims.email(), None); + assert_eq!(claims.email_verified(), None); + assert_eq!(claims.gender(), None); + assert_eq!(claims.birthday(), None); + assert_eq!(claims.birthdate(), None); + assert_eq!(claims.zoneinfo(), None); + assert_eq!(claims.locale(), None); + assert_eq!(claims.phone_number(), None); + assert_eq!(claims.phone_number_verified(), None); + assert_eq!(claims.address(), None); + assert_eq!(claims.updated_at(), None); + + let serialized_claims = serde_json::to_string(&claims).expect("failed to serialize"); + assert_eq!(serialized_claims, expected_serialized_claims); + + let claims_round_trip: CoreIdTokenClaims = + serde_json::from_str(&serialized_claims).expect("failed to deserialize"); + assert_eq!(claims, claims_round_trip); +} + +#[test] +fn test_complete_claims_serde() { + let claims_json = "{\ + \"iss\":\"https://server.example.com\",\ + \"aud\":[\"s6BhdRkqt3\"],\ + \"exp\":1311281970,\ + \"iat\":1311280970,\ + \"auth_time\":1311282970,\ + \"nonce\":\"Zm9vYmFy\",\ + \"acr\":\"urn:mace:incommon:iap:silver\",\ + \"amr\":[\"password\",\"totp\"],\ + \"azp\":\"dGhpc19jbGllbnQ\",\ + \"at_hash\":\"_JPLB-GtkomFJxAOWKHPHQ\",\ + \"c_hash\":\"VpTQii5T_8rgwxA-Wtb2Bw\",\ + \"sub\":\"24400320\",\ + \"name\":\"Homer Simpson\",\ + \"name#es\":\"Jomer Simpson\",\ + \"given_name\":\"Homer\",\ + \"given_name#es\":\"Jomer\",\ + \"family_name\":\"Simpson\",\ + \"family_name#es\":\"Simpson\",\ + \"middle_name\":\"Jay\",\ + \"middle_name#es\":\"Jay\",\ + \"nickname\":\"Homer\",\ + \"nickname#es\":\"Jomer\",\ + \"preferred_username\":\"homersimpson\",\ + \"profile\":\"https://example.com/profile?id=12345\",\ + \"profile#es\":\"https://example.com/profile?id=12345&lang=es\",\ + \"picture\":\"https://example.com/avatar?id=12345\",\ + \"picture#es\":\"https://example.com/avatar?id=12345&lang=es\",\ + \"website\":\"https://homersimpson.me\",\ + \"website#es\":\"https://homersimpson.me/?lang=es\",\ + \"email\":\"homer@homersimpson.me\",\ + \"email_verified\":true,\ + \"gender\":\"male\",\ + \"birthday\":\"1956-05-12\",\ + \"birthdate\":\"1956-07-12\",\ + \"zoneinfo\":\"America/Los_Angeles\",\ + \"locale\":\"en-US\",\ + \"phone_number\":\"+1 (555) 555-5555\",\ + \"phone_number_verified\":false,\ + \"address\":{\ + \"formatted\":\"1234 Hollywood Blvd., Los Angeles, CA 90210\",\ + \"street_address\":\"1234 Hollywood Blvd.\",\ + \"locality\":\"Los Angeles\",\ + \"region\":\"CA\",\ + \"postal_code\":\"90210\",\ + \"country\":\"US\"\ + },\ + \"updated_at\":1311283970\ + }"; + + let new_claims = CoreIdTokenClaims::new( + IssuerUrl::new("https://server.example.com".to_string()).unwrap(), + vec![Audience::new("s6BhdRkqt3".to_string())], + Utc.timestamp_opt(1311281970, 0) + .single() + .expect("valid timestamp"), + Utc.timestamp_opt(1311280970, 0) + .single() + .expect("valid timestamp"), + StandardClaims { + sub: SubjectIdentifier::new("24400320".to_string()), + name: Some( + vec![ + (None, EndUserName::new("Homer Simpson".to_string())), + ( + Some(LanguageTag::new("es".to_string())), + EndUserName::new("Jomer Simpson".to_string()), + ), + ] + .into_iter() + .collect(), + ), + given_name: Some( + vec![ + (None, EndUserGivenName::new("Homer".to_string())), + ( + Some(LanguageTag::new("es".to_string())), + EndUserGivenName::new("Jomer".to_string()), + ), + ] + .into_iter() + .collect(), + ), + family_name: Some( + vec![ + (None, EndUserFamilyName::new("Simpson".to_string())), + ( + Some(LanguageTag::new("es".to_string())), + EndUserFamilyName::new("Simpson".to_string()), + ), + ] + .into_iter() + .collect(), + ), + middle_name: Some( + vec![ + (None, EndUserMiddleName::new("Jay".to_string())), + ( + Some(LanguageTag::new("es".to_string())), + EndUserMiddleName::new("Jay".to_string()), + ), + ] + .into_iter() + .collect(), + ), + nickname: Some( + vec![ + (None, EndUserNickname::new("Homer".to_string())), + ( + Some(LanguageTag::new("es".to_string())), + EndUserNickname::new("Jomer".to_string()), + ), + ] + .into_iter() + .collect(), + ), + preferred_username: Some(EndUserUsername::new("homersimpson".to_string())), + profile: Some( + vec![ + ( + None, + EndUserProfileUrl::new("https://example.com/profile?id=12345".to_string()), + ), + ( + Some(LanguageTag::new("es".to_string())), + EndUserProfileUrl::new( + "https://example.com/profile?id=12345&lang=es".to_string(), + ), + ), + ] + .into_iter() + .collect(), + ), + picture: Some( + vec![ + ( + None, + EndUserPictureUrl::new("https://example.com/avatar?id=12345".to_string()), + ), + ( + Some(LanguageTag::new("es".to_string())), + EndUserPictureUrl::new( + "https://example.com/avatar?id=12345&lang=es".to_string(), + ), + ), + ] + .into_iter() + .collect(), + ), + website: Some( + vec![ + ( + None, + EndUserWebsiteUrl::new("https://homersimpson.me".to_string()), + ), + ( + Some(LanguageTag::new("es".to_string())), + EndUserWebsiteUrl::new("https://homersimpson.me/?lang=es".to_string()), + ), + ] + .into_iter() + .collect(), + ), + email: Some(EndUserEmail::new("homer@homersimpson.me".to_string())), + email_verified: Some(true), + gender: Some(CoreGenderClaim::new("male".to_string())), + birthday: Some(EndUserBirthday::new("1956-05-12".to_string())), + birthdate: Some(EndUserBirthday::new("1956-07-12".to_string())), + zoneinfo: Some(EndUserTimezone::new("America/Los_Angeles".to_string())), + locale: Some(LanguageTag::new("en-US".to_string())), + phone_number: Some(EndUserPhoneNumber::new("+1 (555) 555-5555".to_string())), + phone_number_verified: Some(false), + address: Some(AddressClaim { + formatted: Some(FormattedAddress::new( + "1234 Hollywood Blvd., Los Angeles, CA 90210".to_string(), + )), + street_address: Some(StreetAddress::new("1234 Hollywood Blvd.".to_string())), + locality: Some(AddressLocality::new("Los Angeles".to_string())), + region: Some(AddressRegion::new("CA".to_string())), + postal_code: Some(AddressPostalCode::new("90210".to_string())), + country: Some(AddressCountry::new("US".to_string())), + }), + updated_at: Some( + Utc.timestamp_opt(1311283970, 0) + .single() + .expect("valid timestamp"), + ), + }, + EmptyAdditionalClaims {}, + ) + .set_auth_time(Some( + Utc.timestamp_opt(1311282970, 0) + .single() + .expect("valid timestamp"), + )) + .set_nonce(Some(Nonce::new("Zm9vYmFy".to_string()))) + .set_auth_context_ref(Some(AuthenticationContextClass::new( + "urn:mace:incommon:iap:silver".to_string(), + ))) + .set_auth_method_refs(Some(vec![ + AuthenticationMethodReference::new("password".to_string()), + AuthenticationMethodReference::new("totp".to_string()), + ])) + .set_authorized_party(Some(ClientId::new("dGhpc19jbGllbnQ".to_string()))) + .set_access_token_hash(Some(AccessTokenHash::new( + "_JPLB-GtkomFJxAOWKHPHQ".to_string(), + ))) + .set_code_hash(Some(AuthorizationCodeHash::new( + "VpTQii5T_8rgwxA-Wtb2Bw".to_string(), + ))); + + let claims: CoreIdTokenClaims = + serde_json::from_str(claims_json).expect("failed to deserialize"); + assert_eq!(claims, new_claims); + assert_eq!(claims.issuer(), new_claims.issuer()); + assert_eq!(claims.issuer().url(), new_claims.issuer().url()); + assert_eq!(claims.audiences(), new_claims.audiences()); + assert_eq!(claims.expiration(), new_claims.expiration()); + assert_eq!(claims.issue_time(), new_claims.issue_time()); + assert_eq!(claims.auth_time(), new_claims.auth_time()); + assert_eq!( + claims.nonce().unwrap().secret(), + new_claims.nonce().unwrap().secret() + ); + assert_eq!(claims.auth_context_ref(), new_claims.auth_context_ref()); + assert_eq!(claims.auth_method_refs(), new_claims.auth_method_refs()); + assert_eq!(claims.authorized_party(), new_claims.authorized_party()); + assert_eq!(claims.access_token_hash(), new_claims.access_token_hash()); + assert_eq!(claims.code_hash(), new_claims.code_hash()); + assert_eq!(*claims.additional_claims(), EmptyAdditionalClaims {}); + assert_eq!(claims.subject(), new_claims.subject()); + assert_eq!(claims.name(), new_claims.name()); + assert_eq!(claims.given_name(), new_claims.given_name()); + assert_eq!(claims.family_name(), new_claims.family_name()); + assert_eq!(claims.middle_name(), new_claims.middle_name()); + assert_eq!(claims.nickname(), new_claims.nickname()); + assert_eq!(claims.preferred_username(), new_claims.preferred_username()); + assert_eq!(claims.preferred_username(), new_claims.preferred_username()); + assert_eq!(claims.profile(), new_claims.profile()); + assert_eq!(claims.picture(), new_claims.picture()); + assert_eq!(claims.website(), new_claims.website()); + assert_eq!(claims.email(), new_claims.email()); + assert_eq!(claims.email_verified(), new_claims.email_verified()); + assert_eq!(claims.gender(), new_claims.gender()); + assert_eq!(claims.birthday(), new_claims.birthday()); + assert_eq!(claims.birthdate(), new_claims.birthdate()); + assert_eq!(claims.zoneinfo(), new_claims.zoneinfo()); + assert_eq!(claims.locale(), new_claims.locale()); + assert_eq!(claims.phone_number(), new_claims.phone_number(),); + assert_eq!( + claims.phone_number_verified(), + new_claims.phone_number_verified() + ); + assert_eq!(claims.address(), new_claims.address()); + assert_eq!(claims.updated_at(), new_claims.updated_at()); + + let serialized_claims = serde_json::to_string(&claims).expect("failed to serialize"); + let claims_round_trip: CoreIdTokenClaims = + serde_json::from_str(&serialized_claims).expect("failed to deserialize"); + assert_eq!(claims, claims_round_trip); + + let serialized_new_claims = serde_json::to_string(&new_claims).expect("failed to serialize"); + assert_eq!(serialized_new_claims, claims_json); +} + +// See https://github.com/ramosbugs/openidconnect-rs/issues/23 +#[test] +#[cfg(feature = "accept-rfc3339-timestamps")] +fn test_accept_rfc3339_timestamp() { + let claims: CoreIdTokenClaims = serde_json::from_str( + "{ + \"iss\": \"https://server.example.com\", + \"sub\": \"24400320\", + \"aud\": \"s6BhdRkqt3\", + \"exp\": 1311281970, + \"iat\": 1311280970, + \"updated_at\": \"2021-12-22T02:10:37.000Z\" + }", + ) + .expect("failed to deserialize"); + assert_eq!( + claims.updated_at(), + Some( + Utc.timestamp_opt(1640139037, 0) + .single() + .expect("valid timestamp") + ) + ); +} + +#[test] +fn test_unknown_claims_serde() { + let expected_serialized_claims = "{\ + \"iss\":\"https://server.example.com\",\ + \"aud\":[\"s6BhdRkqt3\"],\ + \"exp\":1311281970,\ + \"iat\":1311280970,\ + \"sub\":\"24400320\"\ + }"; + + let claims: CoreIdTokenClaims = serde_json::from_str( + "{ + \"iss\": \"https://server.example.com\", + \"sub\": \"24400320\", + \"aud\": \"s6BhdRkqt3\", + \"exp\": 1311281970, + \"iat\": 1311280970, + \"some_other_field\":\"some_other_value\"\ + }", + ) + .expect("failed to deserialize"); + + let serialized_claims = serde_json::to_string(&claims).expect("failed to serialize"); + assert_eq!(serialized_claims, expected_serialized_claims); + + let claims_round_trip: CoreIdTokenClaims = + serde_json::from_str(&serialized_claims).expect("failed to deserialize"); + assert_eq!(claims, claims_round_trip); +} + +#[test] +fn test_audience() { + let single_aud_str_claims = serde_json::from_str::( + "{ + \"iss\": \"https://server.example.com\", + \"sub\": \"24400320\", + \"aud\": \"s6BhdRkqt3\", + \"exp\": 1311281970, + \"iat\": 1311280970 + }", + ) + .expect("failed to deserialize"); + assert_eq!( + *single_aud_str_claims.audiences(), + vec![Audience::new("s6BhdRkqt3".to_string())], + ); + + // We always serialize aud as an array, which is valid according to the spec. + assert_eq!( + serde_json::to_string(&single_aud_str_claims).expect("failed to serialize"), + "{\ + \"iss\":\"https://server.example.com\",\ + \"aud\":[\"s6BhdRkqt3\"],\ + \"exp\":1311281970,\ + \"iat\":1311280970,\ + \"sub\":\"24400320\"\ + }", + ); + + let single_aud_vec_claims = serde_json::from_str::( + "{ + \"iss\": \"https://server.example.com\", + \"sub\": \"24400320\", + \"aud\": [\"s6BhdRkqt3\"], + \"exp\": 1311281970, + \"iat\": 1311280970 + }", + ) + .expect("failed to deserialize"); + assert_eq!( + *single_aud_vec_claims.audiences(), + vec![Audience::new("s6BhdRkqt3".to_string())], + ); + assert_eq!( + serde_json::to_string(&single_aud_vec_claims).expect("failed to serialize"), + "{\ + \"iss\":\"https://server.example.com\",\ + \"aud\":[\"s6BhdRkqt3\"],\ + \"exp\":1311281970,\ + \"iat\":1311280970,\ + \"sub\":\"24400320\"\ + }", + ); + + let multi_aud_claims = serde_json::from_str::( + "{\ + \"iss\": \"https://server.example.com\", + \"sub\": \"24400320\", + \"aud\": [\"s6BhdRkqt3\", \"aud2\"], + \"exp\": 1311281970, + \"iat\": 1311280970 + }", + ) + .expect("failed to deserialize"); + assert_eq!( + *multi_aud_claims.audiences(), + vec![ + Audience::new("s6BhdRkqt3".to_string()), + Audience::new("aud2".to_string()) + ], + ); + assert_eq!( + serde_json::to_string(&multi_aud_claims).expect("failed to serialize"), + "{\ + \"iss\":\"https://server.example.com\",\ + \"aud\":[\"s6BhdRkqt3\",\"aud2\"],\ + \"exp\":1311281970,\ + \"iat\":1311280970,\ + \"sub\":\"24400320\"\ + }", + ); +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +struct TestClaims { + pub tfa_method: String, +} +impl AdditionalClaims for TestClaims {} + +#[test] +fn test_additional_claims() { + let claims = serde_json::from_str::>( + "{ + \"iss\": \"https://server.example.com\", + \"sub\": \"24400320\", + \"aud\": [\"s6BhdRkqt3\"], + \"exp\": 1311281970, + \"iat\": 1311280970, + \"tfa_method\": \"u2f\" + }", + ) + .expect("failed to deserialize"); + assert_eq!(claims.additional_claims().tfa_method, "u2f"); + assert_eq!( + serde_json::to_string(&claims).expect("failed to serialize"), + "{\ + \"iss\":\"https://server.example.com\",\ + \"aud\":[\"s6BhdRkqt3\"],\ + \"exp\":1311281970,\ + \"iat\":1311280970,\ + \"sub\":\"24400320\",\ + \"tfa_method\":\"u2f\"\ + }", + ); + + serde_json::from_str::>( + "{ + \"iss\": \"https://server.example.com\", + \"sub\": \"24400320\", + \"aud\": [\"s6BhdRkqt3\"], + \"exp\": 1311281970, + \"iat\": 1311280970 + }", + ) + .expect_err("missing claim should fail to deserialize"); +} + +#[derive(Debug, Deserialize, Serialize)] +struct AllOtherClaims(HashMap); +impl AdditionalClaims for AllOtherClaims {} + +#[test] +fn test_catch_all_additional_claims() { + let claims = serde_json::from_str::>( + "{ + \"iss\": \"https://server.example.com\", + \"sub\": \"24400320\", + \"aud\": [\"s6BhdRkqt3\"], + \"exp\": 1311281970, + \"iat\": 1311280970, + \"tfa_method\": \"u2f\", + \"updated_at\": 1000 + }", + ) + .expect("failed to deserialize"); + + assert_eq!(claims.additional_claims().0.len(), 1); + assert_eq!(claims.additional_claims().0["tfa_method"], "u2f"); +} + +#[test] +fn test_audiences_claim() { + let claims = serde_json::from_str::( + "{ + \"iss\": \"https://server.example.com\", + \"sub\": \"24400320\", + \"aud\": \"s6BhdRkqt3\", + \"exp\": 1311281970, + \"iat\": 1311280970 + }", + ) + .expect("failed to deserialize"); + + fn verify_audiences(audiences_claim: &A) { + assert_eq!( + (*audiences_claim).audiences(), + Some(&vec![Audience::new("s6BhdRkqt3".to_string())]), + ) + } + verify_audiences(&claims); + verify_audiences(&&claims); +} + +#[test] +fn test_issuer_claim() { + let claims = serde_json::from_str::( + "{ + \"iss\": \"https://server.example.com\", + \"sub\": \"24400320\", + \"aud\": \"s6BhdRkqt3\", + \"exp\": 1311281970, + \"iat\": 1311280970 + }", + ) + .expect("failed to deserialize"); + + fn verify_issuer(issuer_claim: &I) { + assert_eq!( + (*issuer_claim).issuer(), + Some(&IssuerUrl::new("https://server.example.com".to_string()).unwrap()), + ) + } + verify_issuer(&claims); + verify_issuer(&&claims); +} diff --git a/src/jwt.rs b/src/jwt.rs deleted file mode 100644 index 971a5f0..0000000 --- a/src/jwt.rs +++ /dev/null @@ -1,838 +0,0 @@ -use crate::{ - JsonWebKey, JsonWebKeyId, JsonWebKeyType, JsonWebKeyUse, JweContentEncryptionAlgorithm, - JwsSigningAlgorithm, PrivateSigningKey, SignatureVerificationError, SigningError, -}; - -use serde::de::{DeserializeOwned, Error as _, Visitor}; -use serde::{Deserialize, Deserializer, Serialize, Serializer}; -use thiserror::Error; - -use std::fmt::{Debug, Formatter, Result as FormatterResult}; -use std::marker::PhantomData; -use std::ops::Deref; -use std::str; - -new_type![ - #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] - JsonWebTokenContentType(String) -]; - -new_type![ - #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] - JsonWebTokenType(String) -]; - -#[derive(Clone, Debug, PartialEq, Eq)] -#[non_exhaustive] -pub enum JsonWebTokenAlgorithm -where - JE: JweContentEncryptionAlgorithm, - JS: JwsSigningAlgorithm, - JT: JsonWebKeyType, -{ - Encryption(JE), - // This is ugly, but we don't expose this module via the public API, so it's fine. - Signature(JS, PhantomData), - /// No digital signature or MAC performed. - /// - /// # Security Warning - /// - /// This algorithm provides no security over the integrity of the JSON Web Token. Clients - /// should be careful not to rely on unsigned JWT's for security purposes. See - /// [Critical vulnerabilities in JSON Web Token libraries]( - /// https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/) for - /// further discussion. - None, -} -impl<'de, JE, JS, JT> Deserialize<'de> for JsonWebTokenAlgorithm -where - JE: JweContentEncryptionAlgorithm, - JS: JwsSigningAlgorithm, - JT: JsonWebKeyType, -{ - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let value: serde_json::Value = Deserialize::deserialize(deserializer)?; - // TODO: get rid of this clone() (see below) - let s: String = serde_json::from_value(value.clone()).map_err(D::Error::custom)?; - - // NB: These comparisons are case sensitive. Section 4.1.1 of RFC 7515 states: "The "alg" - // value is a case-sensitive ASCII string containing a StringOrURI value." - if s == "none" { - Ok(JsonWebTokenAlgorithm::None) - // TODO: Figure out a way to deserialize the enums without giving up ownership - } else if let Ok(val) = serde_json::from_value::(value.clone()) { - Ok(JsonWebTokenAlgorithm::Encryption(val)) - } else if let Ok(val) = serde_json::from_value::(value) { - Ok(JsonWebTokenAlgorithm::Signature(val, PhantomData)) - } else { - Err(D::Error::custom(format!( - "unrecognized JSON Web Algorithm `{}`", - s - ))) - } - } -} -impl Serialize for JsonWebTokenAlgorithm -where - JE: JweContentEncryptionAlgorithm, - JS: JwsSigningAlgorithm, - JT: JsonWebKeyType, -{ - fn serialize(&self, serializer: SE) -> Result - where - SE: Serializer, - { - match self { - JsonWebTokenAlgorithm::Encryption(ref enc) => enc.serialize(serializer), - JsonWebTokenAlgorithm::Signature(ref sig, _) => sig.serialize(serializer), - JsonWebTokenAlgorithm::None => serializer.serialize_str("none"), - } - } -} - -#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)] -pub struct JsonWebTokenHeader -where - JE: JweContentEncryptionAlgorithm, - JS: JwsSigningAlgorithm, - JT: JsonWebKeyType, -{ - #[serde( - bound = "JE: JweContentEncryptionAlgorithm, JS: JwsSigningAlgorithm, JT: JsonWebKeyType" - )] - pub alg: JsonWebTokenAlgorithm, - // Additional critical header parameters that must be understood by this implementation. Since - // we don't understand any such extensions, we reject any JWT with this value present (the - // spec specifically prohibits including public (standard) headers in this field). - // See https://tools.ietf.org/html/rfc7515#section-4.1.11. - #[serde(skip_serializing_if = "Option::is_none")] - pub crit: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub cty: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub kid: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub typ: Option, - // Other JOSE header fields are omitted since the OpenID Connect spec specifically says that - // the "x5u", "x5c", "jku", "jwk" header parameter fields SHOULD NOT be used. - // See http://openid.net/specs/openid-connect-core-1_0-final.html#IDToken. - #[serde(skip)] - _phantom_jt: PhantomData, -} - -pub trait JsonWebTokenPayloadSerde

: Debug -where - P: Debug + DeserializeOwned + Serialize, -{ - fn deserialize(payload: &[u8]) -> Result; - fn serialize(payload: &P) -> Result; -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct JsonWebTokenJsonPayloadSerde; -impl

JsonWebTokenPayloadSerde

for JsonWebTokenJsonPayloadSerde -where - P: Debug + DeserializeOwned + Serialize, -{ - fn deserialize(payload: &[u8]) -> Result { - serde_json::from_slice(payload) - .map_err(|err| DE::custom(format!("Failed to parse payload JSON: {:?}", err))) - } - - fn serialize(payload: &P) -> Result { - serde_json::to_string(payload).map_err(Into::into) - } -} - -// Helper trait so that we can get borrowed payload when we have a reference to the JWT and owned -// payload when we own the JWT. -pub trait JsonWebTokenAccess -where - JE: JweContentEncryptionAlgorithm, - JS: JwsSigningAlgorithm, - JT: JsonWebKeyType, - P: Debug + DeserializeOwned + Serialize, -{ - type ReturnType; - - fn unverified_header(&self) -> &JsonWebTokenHeader; - fn unverified_payload(self) -> Self::ReturnType; - fn unverified_payload_ref(&self) -> &P; - - fn payload( - self, - signature_alg: &JS, - key: &JW, - ) -> Result - where - JU: JsonWebKeyUse, - JW: JsonWebKey; -} - -/// Error creating a JSON Web Token. -#[derive(Debug, Error)] -#[non_exhaustive] -pub enum JsonWebTokenError { - /// Failed to serialize JWT. - #[error("Failed to serialize JWT")] - SerializationError(#[source] serde_json::Error), - /// Failed to sign JWT. - #[error("Failed to sign JWT")] - SigningError(#[source] SigningError), -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct JsonWebToken -where - JE: JweContentEncryptionAlgorithm, - JS: JwsSigningAlgorithm, - JT: JsonWebKeyType, - P: Debug + DeserializeOwned + Serialize, - S: JsonWebTokenPayloadSerde

, -{ - header: JsonWebTokenHeader, - payload: P, - signature: Vec, - signing_input: String, - _phantom: PhantomData, -} -impl JsonWebToken -where - JE: JweContentEncryptionAlgorithm, - JS: JwsSigningAlgorithm, - JT: JsonWebKeyType, - P: Debug + DeserializeOwned + Serialize, - S: JsonWebTokenPayloadSerde

, -{ - pub fn new(payload: P, signing_key: &SK, alg: &JS) -> Result - where - JU: JsonWebKeyUse, - K: JsonWebKey, - SK: PrivateSigningKey, - { - let header = JsonWebTokenHeader:: { - alg: JsonWebTokenAlgorithm::Signature(alg.clone(), PhantomData), - crit: None, - cty: None, - kid: signing_key.as_verification_key().key_id().cloned(), - typ: None, - _phantom_jt: PhantomData, - }; - - let header_json = - serde_json::to_string(&header).map_err(JsonWebTokenError::SerializationError)?; - let header_base64 = base64::encode_config(header_json, base64::URL_SAFE_NO_PAD); - - let serialized_payload = - S::serialize(&payload).map_err(JsonWebTokenError::SerializationError)?; - let payload_base64 = base64::encode_config(serialized_payload, base64::URL_SAFE_NO_PAD); - - let signing_input = format!("{}.{}", header_base64, payload_base64); - - let signature = signing_key - .sign(alg, signing_input.as_bytes()) - .map_err(JsonWebTokenError::SigningError)?; - - Ok(JsonWebToken { - header, - payload, - signature, - signing_input, - _phantom: PhantomData, - }) - } -} -// Owned JWT. -impl JsonWebTokenAccess for JsonWebToken -where - JE: JweContentEncryptionAlgorithm, - JS: JwsSigningAlgorithm, - JT: JsonWebKeyType, - P: Debug + DeserializeOwned + Serialize, - S: JsonWebTokenPayloadSerde

, -{ - type ReturnType = P; - fn unverified_header(&self) -> &JsonWebTokenHeader { - &self.header - } - fn unverified_payload(self) -> Self::ReturnType { - self.payload - } - fn unverified_payload_ref(&self) -> &P { - &self.payload - } - fn payload( - self, - signature_alg: &JS, - key: &JW, - ) -> Result - where - JU: JsonWebKeyUse, - JW: JsonWebKey, - { - key.verify_signature( - signature_alg, - self.signing_input.as_bytes(), - &self.signature, - )?; - Ok(self.payload) - } -} -// Borrowed JWT. -impl<'a, JE, JS, JT, P, S> JsonWebTokenAccess for &'a JsonWebToken -where - JE: JweContentEncryptionAlgorithm, - JS: JwsSigningAlgorithm, - JT: JsonWebKeyType, - P: Debug + DeserializeOwned + Serialize, - S: JsonWebTokenPayloadSerde

, -{ - type ReturnType = &'a P; - fn unverified_header(&self) -> &JsonWebTokenHeader { - &self.header - } - fn unverified_payload(self) -> Self::ReturnType { - &self.payload - } - fn unverified_payload_ref(&self) -> &P { - &self.payload - } - fn payload( - self, - signature_alg: &JS, - key: &JW, - ) -> Result - where - JU: JsonWebKeyUse, - JW: JsonWebKey, - { - key.verify_signature( - signature_alg, - self.signing_input.as_bytes(), - &self.signature, - )?; - Ok(&self.payload) - } -} -impl<'de, JE, JS, JT, P, S> Deserialize<'de> for JsonWebToken -where - JE: JweContentEncryptionAlgorithm, - JS: JwsSigningAlgorithm, - JT: JsonWebKeyType, - P: Debug + DeserializeOwned + Serialize, - S: JsonWebTokenPayloadSerde

, -{ - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - struct JsonWebTokenVisitor< - JE: JweContentEncryptionAlgorithm, - JS: JwsSigningAlgorithm, - JT: JsonWebKeyType, - P: Debug + DeserializeOwned + Serialize, - S: JsonWebTokenPayloadSerde

, - >( - PhantomData, - PhantomData, - PhantomData, - PhantomData

, - PhantomData, - ); - impl<'de, JE, JS, JT, P, S> Visitor<'de> for JsonWebTokenVisitor - where - JE: JweContentEncryptionAlgorithm, - JS: JwsSigningAlgorithm, - JT: JsonWebKeyType, - P: Debug + DeserializeOwned + Serialize, - S: JsonWebTokenPayloadSerde

, - { - type Value = JsonWebToken; - - fn expecting(&self, formatter: &mut Formatter) -> FormatterResult { - formatter.write_str("JsonWebToken") - } - - fn visit_str(self, v: &str) -> Result - where - DE: serde::de::Error, - { - let raw_token = v.to_string(); - let header: JsonWebTokenHeader; - let payload: P; - let signature; - let signing_input; - - { - let parts = raw_token.split('.').collect::>(); - - // NB: We avoid including the full payload encoding in the error output to avoid - // clients potentially logging sensitive values. - if parts.len() != 3 { - return Err(DE::custom(format!( - "Invalid JSON web token: found {} parts (expected 3)", - parts.len() - ))); - } - - let header_json = - base64::decode_config(parts[0], crate::core::base64_url_safe_no_pad()) - .map_err(|err| { - DE::custom(format!("Invalid base64url header encoding: {:?}", err)) - })?; - header = serde_json::from_slice(&header_json).map_err(|err| { - DE::custom(format!("Failed to parse header JSON: {:?}", err)) - })?; - - let raw_payload = - base64::decode_config(parts[1], crate::core::base64_url_safe_no_pad()) - .map_err(|err| { - DE::custom(format!("Invalid base64url payload encoding: {:?}", err)) - })?; - payload = S::deserialize::(&raw_payload)?; - - signature = - base64::decode_config(parts[2], crate::core::base64_url_safe_no_pad()) - .map_err(|err| { - DE::custom(format!( - "Invalid base64url signature encoding: {:?}", - err - )) - })?; - - signing_input = format!("{}.{}", parts[0], parts[1]); - } - - Ok(JsonWebToken { - header, - payload, - signature, - signing_input, - _phantom: PhantomData, - }) - } - } - deserializer.deserialize_str(JsonWebTokenVisitor( - PhantomData, - PhantomData, - PhantomData, - PhantomData, - PhantomData, - )) - } -} -impl Serialize for JsonWebToken -where - JE: JweContentEncryptionAlgorithm, - JS: JwsSigningAlgorithm, - JT: JsonWebKeyType, - P: Debug + DeserializeOwned + Serialize, - S: JsonWebTokenPayloadSerde

, -{ - fn serialize(&self, serializer: SE) -> Result - where - SE: Serializer, - { - let signature_base64 = base64::encode_config(&self.signature, base64::URL_SAFE_NO_PAD); - serializer.serialize_str(&format!("{}.{}", self.signing_input, signature_base64)) - } -} - -#[cfg(test)] -pub mod tests { - use crate::core::{ - CoreJsonWebKey, CoreJsonWebKeyType, CoreJweContentEncryptionAlgorithm, - CoreJwsSigningAlgorithm, CoreRsaPrivateSigningKey, - }; - use crate::jwt::{ - JsonWebToken, JsonWebTokenAccess, JsonWebTokenAlgorithm, JsonWebTokenJsonPayloadSerde, - JsonWebTokenPayloadSerde, - }; - use crate::JsonWebKeyId; - - use serde::{Deserialize, Serialize}; - - use std::marker::PhantomData; - use std::string::ToString; - - type CoreAlgorithm = JsonWebTokenAlgorithm< - CoreJweContentEncryptionAlgorithm, - CoreJwsSigningAlgorithm, - CoreJsonWebKeyType, - >; - - pub const TEST_JWT: &str = - "eyJhbGciOiJSUzI1NiIsImtpZCI6ImJpbGJvLmJhZ2dpbnNAaG9iYml0b24uZXhhbXBsZSJ9.SXTigJlzIGEgZ\ - GFuZ2Vyb3VzIGJ1c2luZXNzLCBGcm9kbywgZ29pbmcgb3V0IHlvdXIgZG9vci4gWW91IHN0ZXAgb250byB0aGU\ - gcm9hZCwgYW5kIGlmIHlvdSBkb24ndCBrZWVwIHlvdXIgZmVldCwgdGhlcmXigJlzIG5vIGtub3dpbmcgd2hlc\ - mUgeW91IG1pZ2h0IGJlIHN3ZXB0IG9mZiB0by4.MRjdkly7_-oTPTS3AXP41iQIGKa80A0ZmTuV5MEaHoxnW2e\ - 5CZ5NlKtainoFmKZopdHM1O2U4mwzJdQx996ivp83xuglII7PNDi84wnB-BDkoBwA78185hX-Es4JIwmDLJK3l\ - fWRa-XtL0RnltuYv746iYTh_qHRD68BNt1uSNCrUCTJDt5aAE6x8wW1Kt9eRo4QPocSadnHXFxnt8Is9UzpERV\ - 0ePPQdLuW3IS_de3xyIrDaLGdjluPxUAhb6L2aXic1U12podGU0KLUQSE_oI-ZnmKJ3F4uOZDnd6QZWJushZ41\ - Axf_fcIe8u9ipH84ogoree7vjbU5y18kDquDg"; - - const TEST_JWT_PAYLOAD: &str = "It\u{2019}s a dangerous business, Frodo, going out your \ - door. You step onto the road, and if you don't keep your feet, \ - there\u{2019}s no knowing where you might be swept off \ - to."; - - pub const TEST_RSA_PUB_KEY: &str = "{ - \"kty\": \"RSA\", - \"kid\": \"bilbo.baggins@hobbiton.example\", - \"use\": \"sig\", - \"n\": \"n4EPtAOCc9AlkeQHPzHStgAbgs7bTZLwUBZdR8_KuKPEHLd4rHVTeT\ - -O-XV2jRojdNhxJWTDvNd7nqQ0VEiZQHz_AJmSCpMaJMRBSFKrKb2wqV\ - wGU_NsYOYL-QtiWN2lbzcEe6XC0dApr5ydQLrHqkHHig3RBordaZ6Aj-\ - oBHqFEHYpPe7Tpe-OfVfHd1E6cS6M1FZcD1NNLYD5lFHpPI9bTwJlsde\ - 3uhGqC0ZCuEHg8lhzwOHrtIQbS0FVbb9k3-tVTU4fg_3L_vniUFAKwuC\ - LqKnS2BYwdq_mzSnbLY7h_qixoR7jig3__kRhuaxwUkRz5iaiQkqgc5g\ - HdrNP5zw\", - \"e\": \"AQAB\" - }"; - - pub const TEST_ED_PUB_KEY_ED25519: &str = r#"{ - "kty": "OKP", - "use": "sig", - "alg": "Ed25519", - "crv": "Ed25519", - "x": "sfliRRhciU_d5qsuC5Vcydi-t8bRfxTg_4qulVatW4A" - }"#; - - pub const TEST_EC_PUB_KEY_P256: &str = r#"{ - "kty": "EC", - "kid": "bilbo.baggins@hobbiton.example", - "use": "sig", - "crv": "P-256", - "x": "t6PHivOTggpaX9lkMkis2p8kMhy-CktJAFTz6atReZw", - "y": "ODobXupKlD0DeM1yRd7bX4XFNBO1HOgCT1UCu0KY3lc" - }"#; - pub const TEST_EC_PUB_KEY_P384: &str = r#"{ - "kty": "EC", - "kid": "bilbo.baggins@hobbiton.example", - "use": "sig", - "crv" : "P-384", - "x": "9ywsUbxX59kJXFRiWHcx97wRKNiF8Hc9F5wI08n8h2ek_qAl0veEc36k1Qz6KLiL", - "y": "6PWlqjRbaV7V8ohDscM243IneuLZmxDGLiGNA1w69fQhEDsvZtKLUQ5KiHLgR3op" - }"#; - - // This is the PEM form of the test private key from: - // https://tools.ietf.org/html/rfc7520#section-3.4 - pub const TEST_RSA_PRIV_KEY: &str = "-----BEGIN RSA PRIVATE KEY-----\n\ - MIIEowIBAAKCAQEAn4EPtAOCc9AlkeQHPzHStgAbgs7bTZLwUBZdR8/KuKPEHLd4\n\ - rHVTeT+O+XV2jRojdNhxJWTDvNd7nqQ0VEiZQHz/AJmSCpMaJMRBSFKrKb2wqVwG\n\ - U/NsYOYL+QtiWN2lbzcEe6XC0dApr5ydQLrHqkHHig3RBordaZ6Aj+oBHqFEHYpP\n\ - e7Tpe+OfVfHd1E6cS6M1FZcD1NNLYD5lFHpPI9bTwJlsde3uhGqC0ZCuEHg8lhzw\n\ - OHrtIQbS0FVbb9k3+tVTU4fg/3L/vniUFAKwuCLqKnS2BYwdq/mzSnbLY7h/qixo\n\ - R7jig3//kRhuaxwUkRz5iaiQkqgc5gHdrNP5zwIDAQABAoIBAG1lAvQfhBUSKPJK\n\ - Rn4dGbshj7zDSr2FjbQf4pIh/ZNtHk/jtavyO/HomZKV8V0NFExLNi7DUUvvLiW7\n\ - 0PgNYq5MDEjJCtSd10xoHa4QpLvYEZXWO7DQPwCmRofkOutf+NqyDS0QnvFvp2d+\n\ - Lov6jn5C5yvUFgw6qWiLAPmzMFlkgxbtjFAWMJB0zBMy2BqjntOJ6KnqtYRMQUxw\n\ - TgXZDF4rhYVKtQVOpfg6hIlsaoPNrF7dofizJ099OOgDmCaEYqM++bUlEHxgrIVk\n\ - wZz+bg43dfJCocr9O5YX0iXaz3TOT5cpdtYbBX+C/5hwrqBWru4HbD3xz8cY1TnD\n\ - qQa0M8ECgYEA3Slxg/DwTXJcb6095RoXygQCAZ5RnAvZlno1yhHtnUex/fp7AZ/9\n\ - nRaO7HX/+SFfGQeutao2TDjDAWU4Vupk8rw9JR0AzZ0N2fvuIAmr/WCsmGpeNqQn\n\ - ev1T7IyEsnh8UMt+n5CafhkikzhEsrmndH6LxOrvRJlsPp6Zv8bUq0kCgYEAuKE2\n\ - dh+cTf6ERF4k4e/jy78GfPYUIaUyoSSJuBzp3Cubk3OCqs6grT8bR/cu0Dm1MZwW\n\ - mtdqDyI95HrUeq3MP15vMMON8lHTeZu2lmKvwqW7anV5UzhM1iZ7z4yMkuUwFWoB\n\ - vyY898EXvRD+hdqRxHlSqAZ192zB3pVFJ0s7pFcCgYAHw9W9eS8muPYv4ZhDu/fL\n\ - 2vorDmD1JqFcHCxZTOnX1NWWAj5hXzmrU0hvWvFC0P4ixddHf5Nqd6+5E9G3k4E5\n\ - 2IwZCnylu3bqCWNh8pT8T3Gf5FQsfPT5530T2BcsoPhUaeCnP499D+rb2mTnFYeg\n\ - mnTT1B/Ue8KGLFFfn16GKQKBgAiw5gxnbocpXPaO6/OKxFFZ+6c0OjxfN2PogWce\n\ - TU/k6ZzmShdaRKwDFXisxRJeNQ5Rx6qgS0jNFtbDhW8E8WFmQ5urCOqIOYk28EBi\n\ - At4JySm4v+5P7yYBh8B8YD2l9j57z/s8hJAxEbn/q8uHP2ddQqvQKgtsni+pHSk9\n\ - XGBfAoGBANz4qr10DdM8DHhPrAb2YItvPVz/VwkBd1Vqj8zCpyIEKe/07oKOvjWQ\n\ - SgkLDH9x2hBgY01SbP43CvPk0V72invu2TGkI/FXwXWJLLG7tDSgw4YyfhrYrHmg\n\ - 1Vre3XB9HH8MYBVB6UIexaAq4xSeoemRKTBesZro7OKjKT8/GmiO\n\ - -----END RSA PRIVATE KEY-----"; - - #[test] - fn test_jwt_algorithm_deserialization() { - assert_eq!( - serde_json::from_str::("\"A128CBC-HS256\"") - .expect("failed to deserialize"), - JsonWebTokenAlgorithm::Encryption( - CoreJweContentEncryptionAlgorithm::Aes128CbcHmacSha256 - ), - ); - assert_eq!( - serde_json::from_str::("\"A128GCM\"").expect("failed to deserialize"), - JsonWebTokenAlgorithm::Encryption(CoreJweContentEncryptionAlgorithm::Aes128Gcm), - ); - assert_eq!( - serde_json::from_str::("\"HS256\"").expect("failed to deserialize"), - JsonWebTokenAlgorithm::Signature(CoreJwsSigningAlgorithm::HmacSha256, PhantomData), - ); - assert_eq!( - serde_json::from_str::("\"RS256\"").expect("failed to deserialize"), - JsonWebTokenAlgorithm::Signature( - CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256, - PhantomData, - ), - ); - assert_eq!( - serde_json::from_str::("\"none\"").expect("failed to deserialize"), - JsonWebTokenAlgorithm::None, - ); - - serde_json::from_str::("\"invalid\"") - .expect_err("deserialization should have failed"); - } - - #[test] - fn test_jwt_algorithm_serialization() { - assert_eq!( - serde_json::to_string::(&JsonWebTokenAlgorithm::Encryption( - CoreJweContentEncryptionAlgorithm::Aes128CbcHmacSha256 - )) - .expect("failed to serialize"), - "\"A128CBC-HS256\"", - ); - assert_eq!( - serde_json::to_string::(&JsonWebTokenAlgorithm::Encryption( - CoreJweContentEncryptionAlgorithm::Aes128Gcm - )) - .expect("failed to serialize"), - "\"A128GCM\"", - ); - assert_eq!( - serde_json::to_string::(&JsonWebTokenAlgorithm::Signature( - CoreJwsSigningAlgorithm::HmacSha256, - PhantomData, - )) - .expect("failed to serialize"), - "\"HS256\"", - ); - assert_eq!( - serde_json::to_string::(&JsonWebTokenAlgorithm::Signature( - CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256, - PhantomData, - )) - .expect("failed to serialize"), - "\"RS256\"", - ); - assert_eq!( - serde_json::to_string::(&JsonWebTokenAlgorithm::None) - .expect("failed to serialize"), - "\"none\"", - ); - } - - #[derive(Clone, Debug)] - pub struct JsonWebTokenStringPayloadSerde; - impl JsonWebTokenPayloadSerde for JsonWebTokenStringPayloadSerde { - fn deserialize(payload: &[u8]) -> Result { - Ok(String::from_utf8(payload.to_owned()).unwrap()) - } - fn serialize(payload: &String) -> Result { - Ok(payload.to_string()) - } - } - - #[test] - fn test_jwt_basic() { - fn verify_jwt(jwt_access: A, key: &CoreJsonWebKey, expected_payload: &str) - where - A: JsonWebTokenAccess< - CoreJweContentEncryptionAlgorithm, - CoreJwsSigningAlgorithm, - CoreJsonWebKeyType, - String, - >, - A::ReturnType: ToString, - { - { - let header = jwt_access.unverified_header(); - assert_eq!( - header.alg, - JsonWebTokenAlgorithm::Signature( - CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256, - PhantomData, - ) - ); - assert_eq!(header.crit, None); - assert_eq!(header.cty, None); - assert_eq!( - header.kid, - Some(JsonWebKeyId::new( - "bilbo.baggins@hobbiton.example".to_string() - )) - ); - assert_eq!(header.typ, None); - } - assert_eq!(jwt_access.unverified_payload_ref(), expected_payload); - - assert_eq!( - jwt_access - .payload(&CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256, key) - .expect("failed to validate payload") - .to_string(), - expected_payload - ); - } - - let key: CoreJsonWebKey = - serde_json::from_str(TEST_RSA_PUB_KEY).expect("deserialization failed"); - - let jwt: JsonWebToken< - CoreJweContentEncryptionAlgorithm, - CoreJwsSigningAlgorithm, - CoreJsonWebKeyType, - String, - JsonWebTokenStringPayloadSerde, - > = serde_json::from_value(serde_json::Value::String(TEST_JWT.to_string())) - .expect("failed to deserialize"); - - assert_eq!( - serde_json::to_value(&jwt).expect("failed to serialize"), - serde_json::Value::String(TEST_JWT.to_string()) - ); - - verify_jwt(&jwt, &key, TEST_JWT_PAYLOAD); - assert_eq!((&jwt).unverified_payload(), TEST_JWT_PAYLOAD); - - verify_jwt(jwt, &key, TEST_JWT_PAYLOAD); - } - - #[test] - fn test_new_jwt() { - let signing_key = CoreRsaPrivateSigningKey::from_pem( - TEST_RSA_PRIV_KEY, - Some(JsonWebKeyId::new( - "bilbo.baggins@hobbiton.example".to_string(), - )), - ) - .unwrap(); - let new_jwt = JsonWebToken::< - CoreJweContentEncryptionAlgorithm, - _, - _, - _, - JsonWebTokenStringPayloadSerde, - >::new( - TEST_JWT_PAYLOAD.to_owned(), - &signing_key, - &CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256, - ) - .unwrap(); - assert_eq!( - serde_json::to_value(new_jwt).expect("failed to serialize"), - serde_json::Value::String(TEST_JWT.to_string()) - ); - } - - #[test] - fn test_invalid_signature() { - let corrupted_jwt_str = TEST_JWT - .to_string() - .chars() - .take(TEST_JWT.len() - 1) - .collect::() - + "f"; - let jwt: JsonWebToken< - CoreJweContentEncryptionAlgorithm, - CoreJwsSigningAlgorithm, - CoreJsonWebKeyType, - String, - JsonWebTokenStringPayloadSerde, - > = serde_json::from_value(serde_json::Value::String(corrupted_jwt_str)) - .expect("failed to deserialize"); - let key: CoreJsonWebKey = - serde_json::from_str(TEST_RSA_PUB_KEY).expect("deserialization failed"); - - // JsonWebTokenAccess for reference. - (&jwt) - .payload(&CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256, &key) - .expect_err("signature verification should have failed"); - - // JsonWebTokenAccess for owned value. - jwt.payload(&CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256, &key) - .expect_err("signature verification should have failed"); - } - - #[test] - fn test_invalid_deserialization() { - #[derive(Debug, Deserialize, Serialize)] - struct TestPayload { - foo: String, - } - - fn expect_deserialization_err>(jwt_str: I, pattern: &str) { - let err = serde_json::from_value::< - JsonWebToken< - CoreJweContentEncryptionAlgorithm, - CoreJwsSigningAlgorithm, - CoreJsonWebKeyType, - TestPayload, - JsonWebTokenJsonPayloadSerde, - >, - >(serde_json::Value::String(jwt_str.into())) - .expect_err("deserialization should have failed"); - - assert!( - err.to_string().contains(pattern), - "Error `{}` must contain string `{}`", - err, - pattern, - ); - } - - // Too many dots - expect_deserialization_err("a.b.c.d", "found 4 parts (expected 3)"); - - // Invalid header base64 - expect_deserialization_err("a!.b.c", "Invalid base64url header encoding"); - - // Invalid header utf-8 (after base64 decoding) - expect_deserialization_err("gA.b.c", "Error(\"expected value\", line: 1, column: 1)"); - - // Invalid header JSON - expect_deserialization_err("bm90X2pzb24.b.c", "Failed to parse header JSON"); - - let valid_header = - "eyJhbGciOiJSUzI1NiIsImtpZCI6ImJpbGJvLmJhZ2dpbnNAaG9iYml0b24uZXhhbXBsZSJ9"; - - // Invalid payload base64 - expect_deserialization_err( - format!("{}.b!.c", valid_header), - "Invalid base64url payload encoding", - ); - - // Invalid payload utf-8 (after base64 decoding) - expect_deserialization_err( - format!("{}.gA.c", valid_header), - "Error(\"expected value\", line: 1, column: 1)", - ); - - // Invalid payload JSON - expect_deserialization_err( - format!("{}.bm90X2pzb24.c", valid_header), - "Failed to parse payload JSON", - ); - - let valid_body = "eyJmb28iOiAiYmFyIn0"; - - // Invalid signature base64 - expect_deserialization_err( - format!("{}.{}.c!", valid_header, valid_body), - "Invalid base64url signature encoding", - ); - - let deserialized = serde_json::from_value::< - JsonWebToken< - CoreJweContentEncryptionAlgorithm, - CoreJwsSigningAlgorithm, - CoreJsonWebKeyType, - TestPayload, - JsonWebTokenJsonPayloadSerde, - >, - >(serde_json::Value::String(format!( - "{}.{}.e2FiY30", - valid_header, valid_body - ))) - .expect("failed to deserialize"); - assert_eq!(deserialized.unverified_payload().foo, "bar"); - } -} diff --git a/src/jwt/mod.rs b/src/jwt/mod.rs new file mode 100644 index 0000000..fb8cce3 --- /dev/null +++ b/src/jwt/mod.rs @@ -0,0 +1,443 @@ +use crate::{ + JsonWebKey, JsonWebKeyId, JsonWebKeyType, JsonWebKeyUse, JweContentEncryptionAlgorithm, + JwsSigningAlgorithm, PrivateSigningKey, SignatureVerificationError, SigningError, +}; + +use serde::de::{DeserializeOwned, Error as _, Visitor}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use thiserror::Error; + +use std::fmt::Debug; +use std::marker::PhantomData; + +#[cfg(test)] +pub(crate) mod tests; + +new_type![ + #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] + JsonWebTokenContentType(String) +]; + +new_type![ + #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] + JsonWebTokenType(String) +]; + +#[derive(Clone, Debug, PartialEq, Eq)] +#[non_exhaustive] +pub enum JsonWebTokenAlgorithm +where + JE: JweContentEncryptionAlgorithm, + JS: JwsSigningAlgorithm, + JT: JsonWebKeyType, +{ + Encryption(JE), + // This is ugly, but we don't expose this module via the public API, so it's fine. + Signature(JS, PhantomData), + /// No digital signature or MAC performed. + /// + /// # Security Warning + /// + /// This algorithm provides no security over the integrity of the JSON Web Token. Clients + /// should be careful not to rely on unsigned JWT's for security purposes. See + /// [Critical vulnerabilities in JSON Web Token libraries]( + /// https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/) for + /// further discussion. + None, +} +impl<'de, JE, JS, JT> Deserialize<'de> for JsonWebTokenAlgorithm +where + JE: JweContentEncryptionAlgorithm, + JS: JwsSigningAlgorithm, + JT: JsonWebKeyType, +{ + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let value: serde_json::Value = Deserialize::deserialize(deserializer)?; + // TODO: get rid of this clone() (see below) + let s: String = serde_json::from_value(value.clone()).map_err(D::Error::custom)?; + + // NB: These comparisons are case-sensitive. Section 4.1.1 of RFC 7515 states: "The "alg" + // value is a case-sensitive ASCII string containing a StringOrURI value." + if s == "none" { + Ok(JsonWebTokenAlgorithm::None) + // TODO: Figure out a way to deserialize the enums without giving up ownership + } else if let Ok(val) = serde_json::from_value::(value.clone()) { + Ok(JsonWebTokenAlgorithm::Encryption(val)) + } else if let Ok(val) = serde_json::from_value::(value) { + Ok(JsonWebTokenAlgorithm::Signature(val, PhantomData)) + } else { + Err(D::Error::custom(format!( + "unrecognized JSON Web Algorithm `{}`", + s + ))) + } + } +} +impl Serialize for JsonWebTokenAlgorithm +where + JE: JweContentEncryptionAlgorithm, + JS: JwsSigningAlgorithm, + JT: JsonWebKeyType, +{ + fn serialize(&self, serializer: SE) -> Result + where + SE: Serializer, + { + match self { + JsonWebTokenAlgorithm::Encryption(ref enc) => enc.serialize(serializer), + JsonWebTokenAlgorithm::Signature(ref sig, _) => sig.serialize(serializer), + JsonWebTokenAlgorithm::None => serializer.serialize_str("none"), + } + } +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)] +pub struct JsonWebTokenHeader +where + JE: JweContentEncryptionAlgorithm, + JS: JwsSigningAlgorithm, + JT: JsonWebKeyType, +{ + #[serde( + bound = "JE: JweContentEncryptionAlgorithm, JS: JwsSigningAlgorithm, JT: JsonWebKeyType" + )] + pub alg: JsonWebTokenAlgorithm, + // Additional critical header parameters that must be understood by this implementation. Since + // we don't understand any such extensions, we reject any JWT with this value present (the + // spec specifically prohibits including public (standard) headers in this field). + // See https://tools.ietf.org/html/rfc7515#section-4.1.11. + #[serde(skip_serializing_if = "Option::is_none")] + pub crit: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub cty: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub kid: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub typ: Option, + // Other JOSE header fields are omitted since the OpenID Connect spec specifically says that + // the "x5u", "x5c", "jku", "jwk" header parameter fields SHOULD NOT be used. + // See http://openid.net/specs/openid-connect-core-1_0-final.html#IDToken. + #[serde(skip)] + _phantom_jt: PhantomData, +} + +pub trait JsonWebTokenPayloadSerde

: Debug +where + P: Debug + DeserializeOwned + Serialize, +{ + fn deserialize(payload: &[u8]) -> Result; + fn serialize(payload: &P) -> Result; +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct JsonWebTokenJsonPayloadSerde; +impl

JsonWebTokenPayloadSerde

for JsonWebTokenJsonPayloadSerde +where + P: Debug + DeserializeOwned + Serialize, +{ + fn deserialize(payload: &[u8]) -> Result { + serde_json::from_slice(payload) + .map_err(|err| DE::custom(format!("Failed to parse payload JSON: {:?}", err))) + } + + fn serialize(payload: &P) -> Result { + serde_json::to_string(payload).map_err(Into::into) + } +} + +// Helper trait so that we can get borrowed payload when we have a reference to the JWT and owned +// payload when we own the JWT. +pub trait JsonWebTokenAccess +where + JE: JweContentEncryptionAlgorithm, + JS: JwsSigningAlgorithm, + JT: JsonWebKeyType, + P: Debug + DeserializeOwned + Serialize, +{ + type ReturnType; + + fn unverified_header(&self) -> &JsonWebTokenHeader; + fn unverified_payload(self) -> Self::ReturnType; + fn unverified_payload_ref(&self) -> &P; + + fn payload( + self, + signature_alg: &JS, + key: &JW, + ) -> Result + where + JU: JsonWebKeyUse, + JW: JsonWebKey; +} + +/// Error creating a JSON Web Token. +#[derive(Debug, Error)] +#[non_exhaustive] +pub enum JsonWebTokenError { + /// Failed to serialize JWT. + #[error("Failed to serialize JWT")] + SerializationError(#[source] serde_json::Error), + /// Failed to sign JWT. + #[error("Failed to sign JWT")] + SigningError(#[source] SigningError), +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct JsonWebToken +where + JE: JweContentEncryptionAlgorithm, + JS: JwsSigningAlgorithm, + JT: JsonWebKeyType, + P: Debug + DeserializeOwned + Serialize, + S: JsonWebTokenPayloadSerde

, +{ + header: JsonWebTokenHeader, + payload: P, + signature: Vec, + signing_input: String, + _phantom: PhantomData, +} +impl JsonWebToken +where + JE: JweContentEncryptionAlgorithm, + JS: JwsSigningAlgorithm, + JT: JsonWebKeyType, + P: Debug + DeserializeOwned + Serialize, + S: JsonWebTokenPayloadSerde

, +{ + pub fn new(payload: P, signing_key: &SK, alg: &JS) -> Result + where + JU: JsonWebKeyUse, + K: JsonWebKey, + SK: PrivateSigningKey, + { + let header = JsonWebTokenHeader:: { + alg: JsonWebTokenAlgorithm::Signature(alg.clone(), PhantomData), + crit: None, + cty: None, + kid: signing_key.as_verification_key().key_id().cloned(), + typ: None, + _phantom_jt: PhantomData, + }; + + let header_json = + serde_json::to_string(&header).map_err(JsonWebTokenError::SerializationError)?; + let header_base64 = base64::encode_config(header_json, base64::URL_SAFE_NO_PAD); + + let serialized_payload = + S::serialize(&payload).map_err(JsonWebTokenError::SerializationError)?; + let payload_base64 = base64::encode_config(serialized_payload, base64::URL_SAFE_NO_PAD); + + let signing_input = format!("{}.{}", header_base64, payload_base64); + + let signature = signing_key + .sign(alg, signing_input.as_bytes()) + .map_err(JsonWebTokenError::SigningError)?; + + Ok(JsonWebToken { + header, + payload, + signature, + signing_input, + _phantom: PhantomData, + }) + } +} +// Owned JWT. +impl JsonWebTokenAccess for JsonWebToken +where + JE: JweContentEncryptionAlgorithm, + JS: JwsSigningAlgorithm, + JT: JsonWebKeyType, + P: Debug + DeserializeOwned + Serialize, + S: JsonWebTokenPayloadSerde

, +{ + type ReturnType = P; + fn unverified_header(&self) -> &JsonWebTokenHeader { + &self.header + } + fn unverified_payload(self) -> Self::ReturnType { + self.payload + } + fn unverified_payload_ref(&self) -> &P { + &self.payload + } + fn payload( + self, + signature_alg: &JS, + key: &JW, + ) -> Result + where + JU: JsonWebKeyUse, + JW: JsonWebKey, + { + key.verify_signature( + signature_alg, + self.signing_input.as_bytes(), + &self.signature, + )?; + Ok(self.payload) + } +} +// Borrowed JWT. +impl<'a, JE, JS, JT, P, S> JsonWebTokenAccess for &'a JsonWebToken +where + JE: JweContentEncryptionAlgorithm, + JS: JwsSigningAlgorithm, + JT: JsonWebKeyType, + P: Debug + DeserializeOwned + Serialize, + S: JsonWebTokenPayloadSerde

, +{ + type ReturnType = &'a P; + fn unverified_header(&self) -> &JsonWebTokenHeader { + &self.header + } + fn unverified_payload(self) -> Self::ReturnType { + &self.payload + } + fn unverified_payload_ref(&self) -> &P { + &self.payload + } + fn payload( + self, + signature_alg: &JS, + key: &JW, + ) -> Result + where + JU: JsonWebKeyUse, + JW: JsonWebKey, + { + key.verify_signature( + signature_alg, + self.signing_input.as_bytes(), + &self.signature, + )?; + Ok(&self.payload) + } +} +impl<'de, JE, JS, JT, P, S> Deserialize<'de> for JsonWebToken +where + JE: JweContentEncryptionAlgorithm, + JS: JwsSigningAlgorithm, + JT: JsonWebKeyType, + P: Debug + DeserializeOwned + Serialize, + S: JsonWebTokenPayloadSerde

, +{ + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct JsonWebTokenVisitor< + JE: JweContentEncryptionAlgorithm, + JS: JwsSigningAlgorithm, + JT: JsonWebKeyType, + P: Debug + DeserializeOwned + Serialize, + S: JsonWebTokenPayloadSerde

, + >( + PhantomData, + PhantomData, + PhantomData, + PhantomData

, + PhantomData, + ); + impl<'de, JE, JS, JT, P, S> Visitor<'de> for JsonWebTokenVisitor + where + JE: JweContentEncryptionAlgorithm, + JS: JwsSigningAlgorithm, + JT: JsonWebKeyType, + P: Debug + DeserializeOwned + Serialize, + S: JsonWebTokenPayloadSerde

, + { + type Value = JsonWebToken; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("JsonWebToken") + } + + fn visit_str(self, v: &str) -> Result + where + DE: serde::de::Error, + { + let raw_token = v.to_string(); + let header: JsonWebTokenHeader; + let payload: P; + let signature; + let signing_input; + + { + let parts = raw_token.split('.').collect::>(); + + // NB: We avoid including the full payload encoding in the error output to avoid + // clients potentially logging sensitive values. + if parts.len() != 3 { + return Err(DE::custom(format!( + "Invalid JSON web token: found {} parts (expected 3)", + parts.len() + ))); + } + + let header_json = + base64::decode_config(parts[0], crate::core::base64_url_safe_no_pad()) + .map_err(|err| { + DE::custom(format!("Invalid base64url header encoding: {:?}", err)) + })?; + header = serde_json::from_slice(&header_json).map_err(|err| { + DE::custom(format!("Failed to parse header JSON: {:?}", err)) + })?; + + let raw_payload = + base64::decode_config(parts[1], crate::core::base64_url_safe_no_pad()) + .map_err(|err| { + DE::custom(format!("Invalid base64url payload encoding: {:?}", err)) + })?; + payload = S::deserialize::(&raw_payload)?; + + signature = + base64::decode_config(parts[2], crate::core::base64_url_safe_no_pad()) + .map_err(|err| { + DE::custom(format!( + "Invalid base64url signature encoding: {:?}", + err + )) + })?; + + signing_input = format!("{}.{}", parts[0], parts[1]); + } + + Ok(JsonWebToken { + header, + payload, + signature, + signing_input, + _phantom: PhantomData, + }) + } + } + deserializer.deserialize_str(JsonWebTokenVisitor( + PhantomData, + PhantomData, + PhantomData, + PhantomData, + PhantomData, + )) + } +} +impl Serialize for JsonWebToken +where + JE: JweContentEncryptionAlgorithm, + JS: JwsSigningAlgorithm, + JT: JsonWebKeyType, + P: Debug + DeserializeOwned + Serialize, + S: JsonWebTokenPayloadSerde

, +{ + fn serialize(&self, serializer: SE) -> Result + where + SE: Serializer, + { + let signature_base64 = base64::encode_config(&self.signature, base64::URL_SAFE_NO_PAD); + serializer.serialize_str(&format!("{}.{}", self.signing_input, signature_base64)) + } +} diff --git a/src/jwt/tests.rs b/src/jwt/tests.rs new file mode 100644 index 0000000..45d53ff --- /dev/null +++ b/src/jwt/tests.rs @@ -0,0 +1,388 @@ +use crate::core::{ + CoreJsonWebKey, CoreJsonWebKeyType, CoreJweContentEncryptionAlgorithm, CoreJwsSigningAlgorithm, + CoreRsaPrivateSigningKey, +}; +use crate::jwt::{ + JsonWebToken, JsonWebTokenAccess, JsonWebTokenAlgorithm, JsonWebTokenJsonPayloadSerde, + JsonWebTokenPayloadSerde, +}; +use crate::JsonWebKeyId; + +use serde::{Deserialize, Serialize}; + +use std::marker::PhantomData; +use std::string::ToString; + +type CoreAlgorithm = JsonWebTokenAlgorithm< + CoreJweContentEncryptionAlgorithm, + CoreJwsSigningAlgorithm, + CoreJsonWebKeyType, +>; + +pub const TEST_JWT: &str = + "eyJhbGciOiJSUzI1NiIsImtpZCI6ImJpbGJvLmJhZ2dpbnNAaG9iYml0b24uZXhhbXBsZSJ9.SXTigJlzIGEgZ\ + GFuZ2Vyb3VzIGJ1c2luZXNzLCBGcm9kbywgZ29pbmcgb3V0IHlvdXIgZG9vci4gWW91IHN0ZXAgb250byB0aGU\ + gcm9hZCwgYW5kIGlmIHlvdSBkb24ndCBrZWVwIHlvdXIgZmVldCwgdGhlcmXigJlzIG5vIGtub3dpbmcgd2hlc\ + mUgeW91IG1pZ2h0IGJlIHN3ZXB0IG9mZiB0by4.MRjdkly7_-oTPTS3AXP41iQIGKa80A0ZmTuV5MEaHoxnW2e\ + 5CZ5NlKtainoFmKZopdHM1O2U4mwzJdQx996ivp83xuglII7PNDi84wnB-BDkoBwA78185hX-Es4JIwmDLJK3l\ + fWRa-XtL0RnltuYv746iYTh_qHRD68BNt1uSNCrUCTJDt5aAE6x8wW1Kt9eRo4QPocSadnHXFxnt8Is9UzpERV\ + 0ePPQdLuW3IS_de3xyIrDaLGdjluPxUAhb6L2aXic1U12podGU0KLUQSE_oI-ZnmKJ3F4uOZDnd6QZWJushZ41\ + Axf_fcIe8u9ipH84ogoree7vjbU5y18kDquDg"; + +const TEST_JWT_PAYLOAD: &str = "It\u{2019}s a dangerous business, Frodo, going out your \ + door. You step onto the road, and if you don't keep your feet, \ + there\u{2019}s no knowing where you might be swept off \ + to."; + +pub const TEST_RSA_PUB_KEY: &str = "{ + \"kty\": \"RSA\", + \"kid\": \"bilbo.baggins@hobbiton.example\", + \"use\": \"sig\", + \"n\": \"n4EPtAOCc9AlkeQHPzHStgAbgs7bTZLwUBZdR8_KuKPEHLd4rHVTeT\ + -O-XV2jRojdNhxJWTDvNd7nqQ0VEiZQHz_AJmSCpMaJMRBSFKrKb2wqV\ + wGU_NsYOYL-QtiWN2lbzcEe6XC0dApr5ydQLrHqkHHig3RBordaZ6Aj-\ + oBHqFEHYpPe7Tpe-OfVfHd1E6cS6M1FZcD1NNLYD5lFHpPI9bTwJlsde\ + 3uhGqC0ZCuEHg8lhzwOHrtIQbS0FVbb9k3-tVTU4fg_3L_vniUFAKwuC\ + LqKnS2BYwdq_mzSnbLY7h_qixoR7jig3__kRhuaxwUkRz5iaiQkqgc5g\ + HdrNP5zw\", + \"e\": \"AQAB\" + }"; + +pub const TEST_ED_PUB_KEY_ED25519: &str = r#"{ + "kty": "OKP", + "use": "sig", + "alg": "Ed25519", + "crv": "Ed25519", + "x": "sfliRRhciU_d5qsuC5Vcydi-t8bRfxTg_4qulVatW4A" + }"#; + +pub const TEST_EC_PUB_KEY_P256: &str = r#"{ + "kty": "EC", + "kid": "bilbo.baggins@hobbiton.example", + "use": "sig", + "crv": "P-256", + "x": "t6PHivOTggpaX9lkMkis2p8kMhy-CktJAFTz6atReZw", + "y": "ODobXupKlD0DeM1yRd7bX4XFNBO1HOgCT1UCu0KY3lc" + }"#; +pub const TEST_EC_PUB_KEY_P384: &str = r#"{ + "kty": "EC", + "kid": "bilbo.baggins@hobbiton.example", + "use": "sig", + "crv" : "P-384", + "x": "9ywsUbxX59kJXFRiWHcx97wRKNiF8Hc9F5wI08n8h2ek_qAl0veEc36k1Qz6KLiL", + "y": "6PWlqjRbaV7V8ohDscM243IneuLZmxDGLiGNA1w69fQhEDsvZtKLUQ5KiHLgR3op" + }"#; + +// This is the PEM form of the test private key from: +// https://tools.ietf.org/html/rfc7520#section-3.4 +pub const TEST_RSA_PRIV_KEY: &str = "-----BEGIN RSA PRIVATE KEY-----\n\ + MIIEowIBAAKCAQEAn4EPtAOCc9AlkeQHPzHStgAbgs7bTZLwUBZdR8/KuKPEHLd4\n\ + rHVTeT+O+XV2jRojdNhxJWTDvNd7nqQ0VEiZQHz/AJmSCpMaJMRBSFKrKb2wqVwG\n\ + U/NsYOYL+QtiWN2lbzcEe6XC0dApr5ydQLrHqkHHig3RBordaZ6Aj+oBHqFEHYpP\n\ + e7Tpe+OfVfHd1E6cS6M1FZcD1NNLYD5lFHpPI9bTwJlsde3uhGqC0ZCuEHg8lhzw\n\ + OHrtIQbS0FVbb9k3+tVTU4fg/3L/vniUFAKwuCLqKnS2BYwdq/mzSnbLY7h/qixo\n\ + R7jig3//kRhuaxwUkRz5iaiQkqgc5gHdrNP5zwIDAQABAoIBAG1lAvQfhBUSKPJK\n\ + Rn4dGbshj7zDSr2FjbQf4pIh/ZNtHk/jtavyO/HomZKV8V0NFExLNi7DUUvvLiW7\n\ + 0PgNYq5MDEjJCtSd10xoHa4QpLvYEZXWO7DQPwCmRofkOutf+NqyDS0QnvFvp2d+\n\ + Lov6jn5C5yvUFgw6qWiLAPmzMFlkgxbtjFAWMJB0zBMy2BqjntOJ6KnqtYRMQUxw\n\ + TgXZDF4rhYVKtQVOpfg6hIlsaoPNrF7dofizJ099OOgDmCaEYqM++bUlEHxgrIVk\n\ + wZz+bg43dfJCocr9O5YX0iXaz3TOT5cpdtYbBX+C/5hwrqBWru4HbD3xz8cY1TnD\n\ + qQa0M8ECgYEA3Slxg/DwTXJcb6095RoXygQCAZ5RnAvZlno1yhHtnUex/fp7AZ/9\n\ + nRaO7HX/+SFfGQeutao2TDjDAWU4Vupk8rw9JR0AzZ0N2fvuIAmr/WCsmGpeNqQn\n\ + ev1T7IyEsnh8UMt+n5CafhkikzhEsrmndH6LxOrvRJlsPp6Zv8bUq0kCgYEAuKE2\n\ + dh+cTf6ERF4k4e/jy78GfPYUIaUyoSSJuBzp3Cubk3OCqs6grT8bR/cu0Dm1MZwW\n\ + mtdqDyI95HrUeq3MP15vMMON8lHTeZu2lmKvwqW7anV5UzhM1iZ7z4yMkuUwFWoB\n\ + vyY898EXvRD+hdqRxHlSqAZ192zB3pVFJ0s7pFcCgYAHw9W9eS8muPYv4ZhDu/fL\n\ + 2vorDmD1JqFcHCxZTOnX1NWWAj5hXzmrU0hvWvFC0P4ixddHf5Nqd6+5E9G3k4E5\n\ + 2IwZCnylu3bqCWNh8pT8T3Gf5FQsfPT5530T2BcsoPhUaeCnP499D+rb2mTnFYeg\n\ + mnTT1B/Ue8KGLFFfn16GKQKBgAiw5gxnbocpXPaO6/OKxFFZ+6c0OjxfN2PogWce\n\ + TU/k6ZzmShdaRKwDFXisxRJeNQ5Rx6qgS0jNFtbDhW8E8WFmQ5urCOqIOYk28EBi\n\ + At4JySm4v+5P7yYBh8B8YD2l9j57z/s8hJAxEbn/q8uHP2ddQqvQKgtsni+pHSk9\n\ + XGBfAoGBANz4qr10DdM8DHhPrAb2YItvPVz/VwkBd1Vqj8zCpyIEKe/07oKOvjWQ\n\ + SgkLDH9x2hBgY01SbP43CvPk0V72invu2TGkI/FXwXWJLLG7tDSgw4YyfhrYrHmg\n\ + 1Vre3XB9HH8MYBVB6UIexaAq4xSeoemRKTBesZro7OKjKT8/GmiO\n\ + -----END RSA PRIVATE KEY-----"; + +#[test] +fn test_jwt_algorithm_deserialization() { + assert_eq!( + serde_json::from_str::("\"A128CBC-HS256\"").expect("failed to deserialize"), + JsonWebTokenAlgorithm::Encryption(CoreJweContentEncryptionAlgorithm::Aes128CbcHmacSha256), + ); + assert_eq!( + serde_json::from_str::("\"A128GCM\"").expect("failed to deserialize"), + JsonWebTokenAlgorithm::Encryption(CoreJweContentEncryptionAlgorithm::Aes128Gcm), + ); + assert_eq!( + serde_json::from_str::("\"HS256\"").expect("failed to deserialize"), + JsonWebTokenAlgorithm::Signature(CoreJwsSigningAlgorithm::HmacSha256, PhantomData), + ); + assert_eq!( + serde_json::from_str::("\"RS256\"").expect("failed to deserialize"), + JsonWebTokenAlgorithm::Signature( + CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256, + PhantomData, + ), + ); + assert_eq!( + serde_json::from_str::("\"none\"").expect("failed to deserialize"), + JsonWebTokenAlgorithm::None, + ); + + serde_json::from_str::("\"invalid\"") + .expect_err("deserialization should have failed"); +} + +#[test] +fn test_jwt_algorithm_serialization() { + assert_eq!( + serde_json::to_string::(&JsonWebTokenAlgorithm::Encryption( + CoreJweContentEncryptionAlgorithm::Aes128CbcHmacSha256 + )) + .expect("failed to serialize"), + "\"A128CBC-HS256\"", + ); + assert_eq!( + serde_json::to_string::(&JsonWebTokenAlgorithm::Encryption( + CoreJweContentEncryptionAlgorithm::Aes128Gcm + )) + .expect("failed to serialize"), + "\"A128GCM\"", + ); + assert_eq!( + serde_json::to_string::(&JsonWebTokenAlgorithm::Signature( + CoreJwsSigningAlgorithm::HmacSha256, + PhantomData, + )) + .expect("failed to serialize"), + "\"HS256\"", + ); + assert_eq!( + serde_json::to_string::(&JsonWebTokenAlgorithm::Signature( + CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256, + PhantomData, + )) + .expect("failed to serialize"), + "\"RS256\"", + ); + assert_eq!( + serde_json::to_string::(&JsonWebTokenAlgorithm::None) + .expect("failed to serialize"), + "\"none\"", + ); +} + +#[derive(Clone, Debug)] +pub struct JsonWebTokenStringPayloadSerde; +impl JsonWebTokenPayloadSerde for JsonWebTokenStringPayloadSerde { + fn deserialize(payload: &[u8]) -> Result { + Ok(String::from_utf8(payload.to_owned()).unwrap()) + } + fn serialize(payload: &String) -> Result { + Ok(payload.to_string()) + } +} + +#[test] +fn test_jwt_basic() { + fn verify_jwt(jwt_access: A, key: &CoreJsonWebKey, expected_payload: &str) + where + A: JsonWebTokenAccess< + CoreJweContentEncryptionAlgorithm, + CoreJwsSigningAlgorithm, + CoreJsonWebKeyType, + String, + >, + A::ReturnType: ToString, + { + { + let header = jwt_access.unverified_header(); + assert_eq!( + header.alg, + JsonWebTokenAlgorithm::Signature( + CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256, + PhantomData, + ) + ); + assert_eq!(header.crit, None); + assert_eq!(header.cty, None); + assert_eq!( + header.kid, + Some(JsonWebKeyId::new( + "bilbo.baggins@hobbiton.example".to_string() + )) + ); + assert_eq!(header.typ, None); + } + assert_eq!(jwt_access.unverified_payload_ref(), expected_payload); + + assert_eq!( + jwt_access + .payload(&CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256, key) + .expect("failed to validate payload") + .to_string(), + expected_payload + ); + } + + let key: CoreJsonWebKey = + serde_json::from_str(TEST_RSA_PUB_KEY).expect("deserialization failed"); + + let jwt: JsonWebToken< + CoreJweContentEncryptionAlgorithm, + CoreJwsSigningAlgorithm, + CoreJsonWebKeyType, + String, + JsonWebTokenStringPayloadSerde, + > = serde_json::from_value(serde_json::Value::String(TEST_JWT.to_string())) + .expect("failed to deserialize"); + + assert_eq!( + serde_json::to_value(&jwt).expect("failed to serialize"), + serde_json::Value::String(TEST_JWT.to_string()) + ); + + verify_jwt(&jwt, &key, TEST_JWT_PAYLOAD); + assert_eq!((&jwt).unverified_payload(), TEST_JWT_PAYLOAD); + + verify_jwt(jwt, &key, TEST_JWT_PAYLOAD); +} + +#[test] +fn test_new_jwt() { + let signing_key = CoreRsaPrivateSigningKey::from_pem( + TEST_RSA_PRIV_KEY, + Some(JsonWebKeyId::new( + "bilbo.baggins@hobbiton.example".to_string(), + )), + ) + .unwrap(); + let new_jwt = JsonWebToken::< + CoreJweContentEncryptionAlgorithm, + _, + _, + _, + JsonWebTokenStringPayloadSerde, + >::new( + TEST_JWT_PAYLOAD.to_owned(), + &signing_key, + &CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256, + ) + .unwrap(); + assert_eq!( + serde_json::to_value(new_jwt).expect("failed to serialize"), + serde_json::Value::String(TEST_JWT.to_string()) + ); +} + +#[test] +fn test_invalid_signature() { + let corrupted_jwt_str = TEST_JWT + .to_string() + .chars() + .take(TEST_JWT.len() - 1) + .collect::() + + "f"; + let jwt: JsonWebToken< + CoreJweContentEncryptionAlgorithm, + CoreJwsSigningAlgorithm, + CoreJsonWebKeyType, + String, + JsonWebTokenStringPayloadSerde, + > = serde_json::from_value(serde_json::Value::String(corrupted_jwt_str)) + .expect("failed to deserialize"); + let key: CoreJsonWebKey = + serde_json::from_str(TEST_RSA_PUB_KEY).expect("deserialization failed"); + + // JsonWebTokenAccess for reference. + (&jwt) + .payload(&CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256, &key) + .expect_err("signature verification should have failed"); + + // JsonWebTokenAccess for owned value. + jwt.payload(&CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256, &key) + .expect_err("signature verification should have failed"); +} + +#[test] +fn test_invalid_deserialization() { + #[derive(Debug, Deserialize, Serialize)] + struct TestPayload { + foo: String, + } + + fn expect_deserialization_err>(jwt_str: I, pattern: &str) { + let err = serde_json::from_value::< + JsonWebToken< + CoreJweContentEncryptionAlgorithm, + CoreJwsSigningAlgorithm, + CoreJsonWebKeyType, + TestPayload, + JsonWebTokenJsonPayloadSerde, + >, + >(serde_json::Value::String(jwt_str.into())) + .expect_err("deserialization should have failed"); + + assert!( + err.to_string().contains(pattern), + "Error `{}` must contain string `{}`", + err, + pattern, + ); + } + + // Too many dots + expect_deserialization_err("a.b.c.d", "found 4 parts (expected 3)"); + + // Invalid header base64 + expect_deserialization_err("a!.b.c", "Invalid base64url header encoding"); + + // Invalid header utf-8 (after base64 decoding) + expect_deserialization_err("gA.b.c", "Error(\"expected value\", line: 1, column: 1)"); + + // Invalid header JSON + expect_deserialization_err("bm90X2pzb24.b.c", "Failed to parse header JSON"); + + let valid_header = "eyJhbGciOiJSUzI1NiIsImtpZCI6ImJpbGJvLmJhZ2dpbnNAaG9iYml0b24uZXhhbXBsZSJ9"; + + // Invalid payload base64 + expect_deserialization_err( + format!("{}.b!.c", valid_header), + "Invalid base64url payload encoding", + ); + + // Invalid payload utf-8 (after base64 decoding) + expect_deserialization_err( + format!("{}.gA.c", valid_header), + "Error(\"expected value\", line: 1, column: 1)", + ); + + // Invalid payload JSON + expect_deserialization_err( + format!("{}.bm90X2pzb24.c", valid_header), + "Failed to parse payload JSON", + ); + + let valid_body = "eyJmb28iOiAiYmFyIn0"; + + // Invalid signature base64 + expect_deserialization_err( + format!("{}.{}.c!", valid_header, valid_body), + "Invalid base64url signature encoding", + ); + + let deserialized = serde_json::from_value::< + JsonWebToken< + CoreJweContentEncryptionAlgorithm, + CoreJwsSigningAlgorithm, + CoreJsonWebKeyType, + TestPayload, + JsonWebTokenJsonPayloadSerde, + >, + >(serde_json::Value::String(format!( + "{}.{}.e2FiY30", + valid_header, valid_body + ))) + .expect("failed to deserialize"); + assert_eq!(deserialized.unverified_payload().foo, "bar"); +} diff --git a/src/lib.rs b/src/lib.rs index f9b7cdf..0af0673 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -565,19 +565,10 @@ //! # Ok(()) //! # } //! ``` -//! use crate::jwt::{JsonWebToken, JsonWebTokenAccess, JsonWebTokenAlgorithm, JsonWebTokenHeader}; use crate::verification::{AudiencesClaim, IssuerClaim}; -use oauth2::ResponseType as OAuth2ResponseType; -use url::Url; - -use std::borrow::Cow; -use std::marker::PhantomData; -use std::str; -use std::time::Duration; - // Defined first since other modules need the macros, and definition order is significant for // macros. This module is private. #[macro_use] @@ -591,12 +582,15 @@ pub mod registration; // Private modules since we may move types between different modules; these are exported publicly // via the pub use above. +mod authorization; mod claims; +mod client; mod discovery; mod helpers; mod id_token; mod logout; -pub(crate) mod types; +mod token; +mod types; mod user_info; mod verification; @@ -637,9 +631,11 @@ pub use oauth2::reqwest; #[cfg(feature = "ureq")] pub use oauth2::ureq; +pub use crate::authorization::{AuthenticationFlow, AuthorizationRequest}; pub use crate::claims::{ AdditionalClaims, AddressClaim, EmptyAdditionalClaims, GenderClaim, StandardClaims, }; +pub use crate::client::Client; pub use crate::discovery::{ AdditionalProviderMetadata, DiscoveryError, EmptyAdditionalProviderMetadata, ProviderMetadata, }; @@ -647,8 +643,16 @@ pub use crate::id_token::IdTokenFields; pub use crate::id_token::{IdToken, IdTokenClaims}; pub use crate::jwt::JsonWebTokenError; pub use crate::logout::{LogoutProviderMetadata, LogoutRequest, ProviderMetadataWithLogout}; +pub use crate::token::TokenResponse; // Flatten the module hierarchy involving types. They're only separated to improve code // organization. +pub use crate::types::jwk::{ + JsonWebKey, JsonWebKeyAlgorithm, JsonWebKeyId, JsonWebKeyType, JsonWebKeyUse, + JweContentEncryptionAlgorithm, JweKeyManagementAlgorithm, JwsSigningAlgorithm, + PrivateSigningKey, +}; +pub use crate::types::jwks::{JsonWebKeySet, JsonWebKeySetUrl}; +pub use crate::types::localized::{LanguageTag, LocalizedClaim}; pub use crate::types::{ AccessTokenHash, AddressCountry, AddressLocality, AddressPostalCode, AddressRegion, ApplicationType, Audience, AuthDisplay, AuthPrompt, AuthenticationContextClass, @@ -657,12 +661,10 @@ pub use crate::types::{ EndUserEmail, EndUserFamilyName, EndUserGivenName, EndUserMiddleName, EndUserName, EndUserNickname, EndUserPhoneNumber, EndUserPictureUrl, EndUserProfileUrl, EndUserTimezone, EndUserUsername, EndUserWebsiteUrl, FormattedAddress, GrantType, InitiateLoginUrl, IssuerUrl, - JsonWebKey, JsonWebKeyAlgorithm, JsonWebKeyId, JsonWebKeySet, JsonWebKeySetUrl, JsonWebKeyType, - JsonWebKeyUse, JweContentEncryptionAlgorithm, JweKeyManagementAlgorithm, JwsSigningAlgorithm, - LanguageTag, LocalizedClaim, LoginHint, LogoUrl, LogoutHint, Nonce, OpPolicyUrl, OpTosUrl, - PolicyUrl, PostLogoutRedirectUrl, PrivateSigningKey, RegistrationAccessToken, RegistrationUrl, - RequestUrl, ResponseMode, ResponseType, ResponseTypes, SectorIdentifierUrl, ServiceDocUrl, - SigningError, StreetAddress, SubjectIdentifier, SubjectIdentifierType, ToSUrl, + LoginHint, LogoUrl, LogoutHint, Nonce, OpPolicyUrl, OpTosUrl, PolicyUrl, PostLogoutRedirectUrl, + RegistrationAccessToken, RegistrationUrl, RequestUrl, ResponseMode, ResponseType, + ResponseTypes, SectorIdentifierUrl, ServiceDocUrl, SigningError, StreetAddress, + SubjectIdentifier, SubjectIdentifierType, ToSUrl, }; pub use crate::user_info::{ UserInfoClaims, UserInfoError, UserInfoJsonWebToken, UserInfoRequest, UserInfoResponseType, @@ -672,1015 +674,3 @@ pub use crate::verification::{ ClaimsVerificationError, IdTokenVerifier, NonceVerifier, SignatureVerificationError, UserInfoVerifier, }; - -const CONFIG_URL_SUFFIX: &str = ".well-known/openid-configuration"; -const OPENID_SCOPE: &str = "openid"; - -/// Authentication flow, which determines how the Authorization Server returns the OpenID Connect -/// ID token and OAuth2 access token to the Relying Party. -#[derive(Clone, Debug, PartialEq, Eq)] -#[non_exhaustive] -pub enum AuthenticationFlow { - /// Authorization Code Flow. - /// - /// The authorization server will return an OAuth2 authorization code. Clients must subsequently - /// call `Client::exchange_code()` with the authorization code in order to retrieve an - /// OpenID Connect ID token and OAuth2 access token. - AuthorizationCode, - /// Implicit Flow. - /// - /// Boolean value indicates whether an OAuth2 access token should also be returned. If `true`, - /// the Authorization Server will return both an OAuth2 access token and OpenID Connect ID - /// token. If `false`, it will return only an OpenID Connect ID token. - Implicit(bool), - /// Hybrid Flow. - /// - /// A hybrid flow according to [OAuth 2.0 Multiple Response Type Encoding Practices]( - /// https://openid.net/specs/oauth-v2-multiple-response-types-1_0.html). The enum value - /// contains the desired `response_type`s. See - /// [Section 3](https://openid.net/specs/openid-connect-core-1_0.html#Authentication) for - /// details. - Hybrid(Vec), -} - -/// OpenID Connect client. -/// -/// # Error Types -/// -/// To enable compile time verification that only the correct and complete set of errors for the `Client` function being -/// invoked are exposed to the caller, the `Client` type is specialized on multiple implementations of the -/// [`ErrorResponse`] trait. The exact [`ErrorResponse`] implementation returned varies by the RFC that the invoked -/// `Client` function implements: -/// -/// - Generic type `TE` (aka Token Error) for errors defined by [RFC 6749 OAuth 2.0 Authorization Framework](https://tools.ietf.org/html/rfc6749). -/// - Generic type `TRE` (aka Token Revocation Error) for errors defined by [RFC 7009 OAuth 2.0 Token Revocation](https://tools.ietf.org/html/rfc7009). -/// -/// For example when revoking a token, error code `unsupported_token_type` (from RFC 7009) may be returned: -/// ```rust -/// # use http::status::StatusCode; -/// # use http::header::{HeaderValue, CONTENT_TYPE}; -/// # use openidconnect::core::CoreClient; -/// # use openidconnect::{ -/// # AccessToken, -/// # AuthUrl, -/// # ClientId, -/// # ClientSecret, -/// # HttpResponse, -/// # IssuerUrl, -/// # JsonWebKeySet, -/// # RequestTokenError, -/// # RevocationErrorResponseType, -/// # RevocationUrl, -/// # TokenUrl, -/// # }; -/// # use thiserror::Error; -/// # -/// # let client = CoreClient::new( -/// # ClientId::new("aaa".to_string()), -/// # Some(ClientSecret::new("bbb".to_string())), -/// # IssuerUrl::new("https://example".to_string()).unwrap(), -/// # AuthUrl::new("https://example/authorize".to_string()).unwrap(), -/// # Some(TokenUrl::new("https://example/token".to_string()).unwrap()), -/// # None, -/// # JsonWebKeySet::default(), -/// # ) -/// # .set_revocation_uri(RevocationUrl::new("https://revocation/url".to_string()).unwrap()); -/// # -/// # #[derive(Debug, Error)] -/// # enum FakeError { -/// # #[error("error")] -/// # Err, -/// # } -/// # -/// # let http_client = |_| -> Result { -/// # Ok(HttpResponse { -/// # status_code: StatusCode::BAD_REQUEST, -/// # headers: vec![( -/// # CONTENT_TYPE, -/// # HeaderValue::from_str("application/json").unwrap(), -/// # )] -/// # .into_iter() -/// # .collect(), -/// # body: "{\"error\": \"unsupported_token_type\", \"error_description\": \"stuff happened\", \ -/// # \"error_uri\": \"https://errors\"}" -/// # .to_string() -/// # .into_bytes(), -/// # }) -/// # }; -/// # -/// let res = client -/// .revoke_token(AccessToken::new("some token".to_string()).into()) -/// .unwrap() -/// .request(http_client); -/// -/// assert!(matches!(res, Err( -/// RequestTokenError::ServerResponse(err)) if matches!(err.error(), -/// RevocationErrorResponseType::UnsupportedTokenType))); -/// ``` -#[derive(Clone, Debug)] -pub struct Client -where - AC: AdditionalClaims, - AD: AuthDisplay, - GC: GenderClaim, - JE: JweContentEncryptionAlgorithm, - JS: JwsSigningAlgorithm, - JT: JsonWebKeyType, - JU: JsonWebKeyUse, - K: JsonWebKey, - P: AuthPrompt, - TE: ErrorResponse, - TR: TokenResponse, - TT: TokenType + 'static, - TIR: TokenIntrospectionResponse, - RT: RevocableToken, - TRE: ErrorResponse, -{ - oauth2_client: oauth2::Client, - client_id: ClientId, - client_secret: Option, - issuer: IssuerUrl, - userinfo_endpoint: Option, - jwks: JsonWebKeySet, - id_token_signing_algs: Option>, - use_openid_scope: bool, - _phantom: PhantomData<(AC, AD, GC, JE, P)>, -} -impl - Client -where - 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, -{ - /// Initializes an OpenID Connect client. - pub fn new( - client_id: ClientId, - client_secret: Option, - issuer: IssuerUrl, - auth_url: AuthUrl, - token_url: Option, - userinfo_endpoint: Option, - jwks: JsonWebKeySet, - ) -> Self { - Client { - oauth2_client: oauth2::Client::new( - client_id.clone(), - client_secret.clone(), - auth_url, - token_url, - ), - client_id, - client_secret, - issuer, - userinfo_endpoint, - jwks, - id_token_signing_algs: None, - use_openid_scope: true, - _phantom: PhantomData, - } - } - - /// Initializes an OpenID Connect client from OpenID Connect Discovery provider metadata. - /// - /// Use [`ProviderMetadata::discover`] or - /// [`ProviderMetadata::discover_async`] to fetch the provider metadata. - pub fn from_provider_metadata( - provider_metadata: ProviderMetadata, - client_id: ClientId, - client_secret: Option, - ) -> Self - where - A: AdditionalProviderMetadata, - CA: ClientAuthMethod, - CN: ClaimName, - CT: ClaimType, - G: GrantType, - JK: JweKeyManagementAlgorithm, - RM: ResponseMode, - RS: ResponseType, - S: SubjectIdentifierType, - { - Client { - oauth2_client: oauth2::Client::new( - client_id.clone(), - client_secret.clone(), - provider_metadata.authorization_endpoint().clone(), - provider_metadata.token_endpoint().cloned(), - ), - client_id, - client_secret, - issuer: provider_metadata.issuer().clone(), - userinfo_endpoint: provider_metadata.userinfo_endpoint().cloned(), - jwks: provider_metadata.jwks().to_owned(), - id_token_signing_algs: Some( - provider_metadata - .id_token_signing_alg_values_supported() - .to_owned(), - ), - use_openid_scope: true, - _phantom: PhantomData, - } - } - - /// Configures the type of client authentication used for communicating with the authorization - /// server. - /// - /// The default is to use HTTP Basic authentication, as recommended in - /// [Section 2.3.1 of RFC 6749](https://tools.ietf.org/html/rfc6749#section-2.3.1). Note that - /// if a client secret is omitted (i.e., `client_secret` is set to `None` when calling - /// [`Client::new`]), [`AuthType::RequestBody`] is used regardless of the `auth_type` passed to - /// this function. - pub fn set_auth_type(mut self, auth_type: AuthType) -> Self { - self.oauth2_client = self.oauth2_client.set_auth_type(auth_type); - self - } - - /// Sets the the redirect URL used by the authorization endpoint. - pub fn set_redirect_uri(mut self, redirect_url: RedirectUrl) -> Self { - self.oauth2_client = self.oauth2_client.set_redirect_uri(redirect_url); - self - } - - /// Sets the introspection URL for contacting the ([RFC 7662](https://tools.ietf.org/html/rfc7662)) - /// introspection endpoint. - pub fn set_introspection_uri(mut self, introspection_url: IntrospectionUrl) -> Self { - self.oauth2_client = self.oauth2_client.set_introspection_uri(introspection_url); - self - } - - /// Sets the revocation URL for contacting the revocation endpoint ([RFC 7009](https://tools.ietf.org/html/rfc7009)). - /// - /// See: [`revoke_token()`](Self::revoke_token()) - pub fn set_revocation_uri(mut self, revocation_url: RevocationUrl) -> Self { - self.oauth2_client = self.oauth2_client.set_revocation_uri(revocation_url); - self - } - - /// Sets the device authorization URL for contacting the device authorization endpoint ([RFC 8628](https://tools.ietf.org/html/rfc8628)). - pub fn set_device_authorization_uri( - mut self, - device_authorization_url: DeviceAuthorizationUrl, - ) -> Self { - self.oauth2_client = self - .oauth2_client - .set_device_authorization_url(device_authorization_url); - self - } - - /// Enables the `openid` scope to be requested automatically. - /// - /// This scope is requested by default, so this function is only useful after previous calls to - /// [`disable_openid_scope`][Client::disable_openid_scope]. - pub fn enable_openid_scope(mut self) -> Self { - self.use_openid_scope = true; - self - } - - /// Disables the `openid` scope from being requested automatically. - pub fn disable_openid_scope(mut self) -> Self { - self.use_openid_scope = false; - self - } - - /// Returns an ID token verifier for use with the [`IdToken::claims`] method. - pub fn id_token_verifier(&self) -> IdTokenVerifier { - let verifier = if let Some(ref client_secret) = self.client_secret { - IdTokenVerifier::new_confidential_client( - self.client_id.clone(), - client_secret.clone(), - self.issuer.clone(), - self.jwks.clone(), - ) - } else { - IdTokenVerifier::new_public_client( - self.client_id.clone(), - self.issuer.clone(), - self.jwks.clone(), - ) - }; - - if let Some(id_token_signing_algs) = self.id_token_signing_algs.clone() { - verifier.set_allowed_algs(id_token_signing_algs) - } else { - verifier - } - } - - /// Generates an authorization URL for a new authorization request. - /// - /// NOTE: [Passing authorization request parameters as a JSON Web Token - /// ](https://openid.net/specs/openid-connect-core-1_0.html#JWTRequests) - /// instead of URL query parameters is not currently supported. The - /// [`claims` parameter](https://openid.net/specs/openid-connect-core-1_0.html#ClaimsParameter) - /// is also not directly supported, although the [`AuthorizationRequest::add_extra_param`] - /// method can be used to add custom parameters, including `claims`. - /// - /// # Arguments - /// - /// * `authentication_flow` - The authentication flow to use (code, implicit, or hybrid). - /// * `state_fn` - A function that returns an opaque value used by the client to maintain state - /// between the request and callback. The authorization server includes this value when - /// redirecting the user-agent back to the client. - /// * `nonce_fn` - Similar to `state_fn`, but used to generate an opaque nonce to be used - /// when verifying the ID token returned by the OpenID Connect Provider. - /// - /// # Security Warning - /// - /// Callers should use a fresh, unpredictable `state` for each authorization request and verify - /// that this value matches the `state` parameter passed by the authorization server to the - /// redirect URI. Doing so mitigates - /// [Cross-Site Request Forgery](https://tools.ietf.org/html/rfc6749#section-10.12) - /// attacks. - /// - /// Similarly, callers should use a fresh, unpredictable `nonce` to help protect against ID - /// token reuse and forgery. - pub fn authorize_url( - &self, - authentication_flow: AuthenticationFlow, - state_fn: SF, - nonce_fn: NF, - ) -> AuthorizationRequest - where - NF: FnOnce() -> Nonce + 'static, - RS: ResponseType, - SF: FnOnce() -> CsrfToken + 'static, - { - let request = AuthorizationRequest { - inner: self.oauth2_client.authorize_url(state_fn), - acr_values: Vec::new(), - authentication_flow, - claims_locales: Vec::new(), - display: None, - id_token_hint: None, - login_hint: None, - max_age: None, - nonce: nonce_fn(), - prompts: Vec::new(), - ui_locales: Vec::new(), - }; - if self.use_openid_scope { - request.add_scope(Scope::new(OPENID_SCOPE.to_string())) - } else { - request - } - } - - /// Creates a request builder for exchanging an authorization code for an access token. - /// - /// Acquires ownership of the `code` because authorization codes may only be used once to - /// retrieve an access token from the authorization server. - /// - /// See - pub fn exchange_code(&self, code: AuthorizationCode) -> CodeTokenRequest { - self.oauth2_client.exchange_code(code) - } - - /// Creates a request builder for device authorization. - /// - /// See - pub fn exchange_device_code( - &self, - ) -> Result, ConfigurationError> { - let request = self.oauth2_client.exchange_device_code(); - if self.use_openid_scope { - Ok(request?.add_scope(Scope::new(OPENID_SCOPE.to_string()))) - } else { - request - } - } - - /// Creates a request builder for exchanging a device code for an access token. - /// - /// See - pub fn exchange_device_access_token<'a, 'b, 'c, EF>( - &'a self, - auth_response: &'b DeviceAuthorizationResponse, - ) -> DeviceAccessTokenRequest<'b, 'c, TR, TT, EF> - where - 'a: 'b, - EF: ExtraDeviceAuthorizationFields, - { - self.oauth2_client - .exchange_device_access_token(auth_response) - } - - /// Creates a request builder for exchanging a refresh token for an access token. - /// - /// See - pub fn exchange_refresh_token<'a, 'b>( - &'a self, - refresh_token: &'b RefreshToken, - ) -> RefreshTokenRequest<'b, TE, TR, TT> - where - 'a: 'b, - { - self.oauth2_client.exchange_refresh_token(refresh_token) - } - - /// Creates a request builder for exchanging credentials for an access token. - /// - /// See - pub fn exchange_password<'a, 'b>( - &'a self, - username: &'b ResourceOwnerUsername, - password: &'b ResourceOwnerPassword, - ) -> PasswordTokenRequest<'b, TE, TR, TT> - where - 'a: 'b, - { - self.oauth2_client.exchange_password(username, password) - } - - /// Creates a request builder for exchanging client credentials for an access token. - /// - /// See - pub fn exchange_client_credentials<'a, 'b>( - &'a self, - ) -> ClientCredentialsTokenRequest<'b, TE, TR, TT> - where - 'a: 'b, - { - self.oauth2_client.exchange_client_credentials() - } - - /// Creates a request builder for info about the user associated with the given access token. - /// - /// This function requires that this [`Client`] be configured with a user info endpoint, - /// which is an optional feature for OpenID Connect Providers to implement. If this `Client` - /// does not know the provider's user info endpoint, it returns the [`ConfigurationError`] - /// error. - /// - /// To help protect against token substitution attacks, this function optionally allows clients - /// to provide the subject identifier whose user info they expect to receive. If provided and - /// the subject returned by the OpenID Connect Provider does not match, the - /// [`UserInfoRequest::request`] or [`UserInfoRequest::request_async`] functions will return - /// [`UserInfoError::ClaimsVerification`]. If set to `None`, any subject is accepted. - pub fn user_info( - &self, - access_token: AccessToken, - expected_subject: Option, - ) -> Result, ConfigurationError> { - Ok(UserInfoRequest { - url: self - .userinfo_endpoint - .as_ref() - .ok_or(ConfigurationError::MissingUrl("userinfo"))?, - access_token, - require_signed_response: false, - response_type: UserInfoResponseType::Json, - signed_response_verifier: UserInfoVerifier::new( - self.client_id.clone(), - self.issuer.clone(), - self.jwks.clone(), - expected_subject, - ), - }) - } - - /// Creates a request builder for obtaining metadata about a previously received token. - /// - /// See - pub fn introspect<'a>( - &'a self, - token: &'a AccessToken, - ) -> Result, ConfigurationError> { - self.oauth2_client.introspect(token) - } - - /// Creates a request builder for revoking a previously received token. - /// - /// Requires that [`set_revocation_uri()`](Self::set_revocation_uri()) have already been called to set the - /// revocation endpoint URL. - /// - /// Attempting to submit the generated request without calling [`set_revocation_uri()`](Self::set_revocation_uri()) - /// first will result in an error. - /// - /// See - pub fn revoke_token( - &self, - token: RT, - ) -> Result, ConfigurationError> { - self.oauth2_client.revoke_token(token) - } -} - -/// A request to the authorization endpoint. -pub struct AuthorizationRequest<'a, AD, P, RT> -where - AD: AuthDisplay, - P: AuthPrompt, - RT: ResponseType, -{ - inner: oauth2::AuthorizationRequest<'a>, - acr_values: Vec, - authentication_flow: AuthenticationFlow, - claims_locales: Vec, - display: Option, - id_token_hint: Option, - login_hint: Option, - max_age: Option, - nonce: Nonce, - prompts: Vec

, - ui_locales: Vec, -} -impl<'a, AD, P, RT> AuthorizationRequest<'a, AD, P, RT> -where - AD: AuthDisplay, - P: AuthPrompt, - RT: ResponseType, -{ - /// Appends a new scope to the authorization URL. - pub fn add_scope(mut self, scope: Scope) -> Self { - self.inner = self.inner.add_scope(scope); - self - } - - /// Appends a collection of scopes to the authorization URL. - pub fn add_scopes(mut self, scopes: I) -> Self - where - I: IntoIterator, - { - self.inner = self.inner.add_scopes(scopes); - self - } - - /// Appends an extra param to the authorization URL. - /// - /// This method allows extensions to be used without direct support from - /// this crate. If `name` conflicts with a parameter managed by this crate, the - /// behavior is undefined. In particular, do not set parameters defined by - /// [RFC 6749](https://tools.ietf.org/html/rfc6749) or - /// [RFC 7636](https://tools.ietf.org/html/rfc7636). - /// - /// # Security Warning - /// - /// Callers should follow the security recommendations for any OAuth2 extensions used with - /// this function, which are beyond the scope of - /// [RFC 6749](https://tools.ietf.org/html/rfc6749). - pub fn add_extra_param(mut self, name: N, value: V) -> Self - where - N: Into>, - V: Into>, - { - self.inner = self.inner.add_extra_param(name, value); - self - } - - /// Enables the use of [Proof Key for Code Exchange](https://tools.ietf.org/html/rfc7636) - /// (PKCE). - /// - /// PKCE is *highly recommended* for all public clients (i.e., those for which there - /// is no client secret or for which the client secret is distributed with the client, - /// such as in a native, mobile app, or browser app). - pub fn set_pkce_challenge(mut self, pkce_code_challenge: PkceCodeChallenge) -> Self { - self.inner = self.inner.set_pkce_challenge(pkce_code_challenge); - self - } - - /// Requests Authentication Context Class Reference values. - /// - /// ACR values should be added in order of preference. The Authentication Context Class - /// satisfied by the authentication performed is accessible from the ID token via the - /// [`IdTokenClaims::auth_context_ref`] method. - pub fn add_auth_context_value(mut self, acr_value: AuthenticationContextClass) -> Self { - self.acr_values.push(acr_value); - self - } - - /// Requests the preferred languages for claims returned by the OpenID Connect Provider. - /// - /// Languages should be added in order of preference. - pub fn add_claims_locale(mut self, claims_locale: LanguageTag) -> Self { - self.claims_locales.push(claims_locale); - self - } - - // TODO: support 'claims' parameter - // https://openid.net/specs/openid-connect-core-1_0.html#ClaimsParameter - - /// Specifies how the OpenID Connect Provider displays the authentication and consent user - /// interfaces to the end user. - pub fn set_display(mut self, display: AD) -> Self { - self.display = Some(display); - self - } - - /// Provides an ID token previously issued by this OpenID Connect Provider as a hint about - /// the user's identity. - /// - /// This field should be set whenever [`core::CoreAuthPrompt::None`] is used (see - /// [`AuthorizationRequest::add_prompt`]), it but may be provided for any authorization - /// request. - pub fn set_id_token_hint( - mut self, - id_token_hint: &'a IdToken, - ) -> Self - where - AC: AdditionalClaims, - GC: GenderClaim, - JE: JweContentEncryptionAlgorithm, - JS: JwsSigningAlgorithm, - JT: JsonWebKeyType, - { - self.id_token_hint = Some(id_token_hint.to_string()); - self - } - - /// Provides the OpenID Connect Provider with a hint about the user's identity. - /// - /// The nature of this hint is specific to each provider. - pub fn set_login_hint(mut self, login_hint: LoginHint) -> Self { - self.login_hint = Some(login_hint); - self - } - - /// Sets a maximum amount of time since the user has last authenticated with the OpenID - /// Connect Provider. - /// - /// If more time has elapsed, the provider forces the user to re-authenticate. - pub fn set_max_age(mut self, max_age: Duration) -> Self { - self.max_age = Some(max_age); - self - } - - /// Specifies what level of authentication and consent prompts the OpenID Connect Provider - /// should present to the user. - pub fn add_prompt(mut self, prompt: P) -> Self { - self.prompts.push(prompt); - self - } - - /// Requests the preferred languages for the user interface presented by the OpenID Connect - /// Provider. - /// - /// Languages should be added in order of preference. - pub fn add_ui_locale(mut self, ui_locale: LanguageTag) -> Self { - self.ui_locales.push(ui_locale); - self - } - - /// Overrides the `redirect_url` to the one specified. - pub fn set_redirect_uri(mut self, redirect_url: Cow<'a, RedirectUrl>) -> Self { - self.inner = self.inner.set_redirect_uri(redirect_url); - self - } - - /// Returns the full authorization URL and CSRF state for this authorization - /// request. - pub fn url(self) -> (Url, CsrfToken, Nonce) { - let response_type = match self.authentication_flow { - AuthenticationFlow::AuthorizationCode => core::CoreResponseType::Code.to_oauth2(), - AuthenticationFlow::Implicit(include_token) => { - if include_token { - OAuth2ResponseType::new( - [ - core::CoreResponseType::IdToken, - core::CoreResponseType::Token, - ] - .iter() - .map(|response_type| response_type.as_ref()) - .collect::>() - .join(" "), - ) - } else { - core::CoreResponseType::IdToken.to_oauth2() - } - } - AuthenticationFlow::Hybrid(ref response_types) => OAuth2ResponseType::new( - response_types - .iter() - .map(|response_type| response_type.as_ref()) - .collect::>() - .join(" "), - ), - }; - let (mut inner, nonce) = ( - self.inner - .set_response_type(&response_type) - .add_extra_param("nonce", self.nonce.secret().clone()), - self.nonce, - ); - if !self.acr_values.is_empty() { - inner = inner.add_extra_param("acr_values", join_vec(&self.acr_values)); - } - if !self.claims_locales.is_empty() { - inner = inner.add_extra_param("claims_locales", join_vec(&self.claims_locales)); - } - if let Some(ref display) = self.display { - inner = inner.add_extra_param("display", display.as_ref()); - } - if let Some(ref id_token_hint) = self.id_token_hint { - inner = inner.add_extra_param("id_token_hint", id_token_hint); - } - if let Some(ref login_hint) = self.login_hint { - inner = inner.add_extra_param("login_hint", login_hint.secret()); - } - if let Some(max_age) = self.max_age { - inner = inner.add_extra_param("max_age", max_age.as_secs().to_string()); - } - if !self.prompts.is_empty() { - inner = inner.add_extra_param("prompt", join_vec(&self.prompts)); - } - if !self.ui_locales.is_empty() { - inner = inner.add_extra_param("ui_locales", join_vec(&self.ui_locales)); - } - - let (url, state) = inner.url(); - (url, state, nonce) - } -} - -/// Extends the base OAuth2 token response with an ID token. -pub trait TokenResponse: OAuth2TokenResponse -where - AC: AdditionalClaims, - GC: GenderClaim, - JE: JweContentEncryptionAlgorithm, - JS: JwsSigningAlgorithm, - JT: JsonWebKeyType, - TT: TokenType, -{ - /// Returns the ID token provided by the token response. - /// - /// OpenID Connect authorization servers should always return this field, but it is optional - /// to allow for interoperability with authorization servers that only support OAuth2. - fn id_token(&self) -> Option<&IdToken>; -} - -impl TokenResponse - for StandardTokenResponse, TT> -where - AC: AdditionalClaims, - EF: ExtraTokenFields, - GC: GenderClaim, - JE: JweContentEncryptionAlgorithm, - JS: JwsSigningAlgorithm, - JT: JsonWebKeyType, - TT: TokenType, -{ - fn id_token(&self) -> Option<&IdToken> { - self.extra_fields().id_token() - } -} - -fn join_vec(entries: &[T]) -> String -where - T: AsRef, -{ - entries - .iter() - .map(AsRef::as_ref) - .collect::>() - .join(" ") -} - -#[cfg(test)] -mod tests { - use crate::core::CoreAuthenticationFlow; - use crate::core::{CoreAuthDisplay, CoreAuthPrompt, CoreClient, CoreIdToken, CoreResponseType}; - use crate::IssuerUrl; - use crate::{ - AuthUrl, AuthenticationContextClass, AuthenticationFlow, ClientId, ClientSecret, CsrfToken, - JsonWebKeySet, LanguageTag, LoginHint, Nonce, RedirectUrl, Scope, TokenUrl, - }; - - use std::borrow::Cow; - use std::time::Duration; - - fn new_client() -> CoreClient { - color_backtrace::install(); - CoreClient::new( - ClientId::new("aaa".to_string()), - Some(ClientSecret::new("bbb".to_string())), - IssuerUrl::new("https://example".to_string()).unwrap(), - AuthUrl::new("https://example/authorize".to_string()).unwrap(), - Some(TokenUrl::new("https://example/token".to_string()).unwrap()), - None, - JsonWebKeySet::default(), - ) - } - - #[test] - fn test_authorize_url_minimal() { - let client = new_client(); - - let (authorize_url, _, _) = client - .authorize_url( - AuthenticationFlow::AuthorizationCode::, - || CsrfToken::new("CSRF123".to_string()), - || Nonce::new("NONCE456".to_string()), - ) - .url(); - - assert_eq!( - "https://example/authorize?response_type=code&client_id=aaa&\ - state=CSRF123&scope=openid&nonce=NONCE456", - authorize_url.to_string() - ); - } - - #[test] - fn test_authorize_url_implicit_with_access_token() { - let client = new_client(); - - let (authorize_url, _, _) = client - .authorize_url( - AuthenticationFlow::::Implicit(true), - || CsrfToken::new("CSRF123".to_string()), - || Nonce::new("NONCE456".to_string()), - ) - .url(); - - assert_eq!( - "https://example/authorize?response_type=id_token+token&client_id=aaa&\ - state=CSRF123&scope=openid&nonce=NONCE456", - authorize_url.to_string() - ); - } - - #[test] - fn test_authorize_url_hybrid() { - let client = new_client(); - - let (authorize_url, _, _) = client - .authorize_url( - AuthenticationFlow::Hybrid(vec![ - CoreResponseType::Code, - CoreResponseType::Extension("other".to_string()), - ]), - || CsrfToken::new("CSRF123".to_string()), - || Nonce::new("NONCE456".to_string()), - ) - .url(); - - assert_eq!( - "https://example/authorize?response_type=code+other&client_id=aaa&\ - state=CSRF123&scope=openid&nonce=NONCE456", - authorize_url.to_string() - ); - } - - #[test] - fn test_authorize_url_full() { - let client = new_client() - .set_redirect_uri(RedirectUrl::new("http://localhost:8888/".to_string()).unwrap()); - - let flow = CoreAuthenticationFlow::AuthorizationCode; - - fn new_csrf() -> CsrfToken { - CsrfToken::new("CSRF123".to_string()) - } - fn new_nonce() -> Nonce { - Nonce::new("NONCE456".to_string()) - } - - let (authorize_url, _, _) = client - .authorize_url(flow.clone(), new_csrf, new_nonce) - .add_scope(Scope::new("email".to_string())) - .set_display(CoreAuthDisplay::Touch) - .add_prompt(CoreAuthPrompt::Login) - .add_prompt(CoreAuthPrompt::Consent) - .set_max_age(Duration::from_secs(1800)) - .add_ui_locale(LanguageTag::new("fr-CA".to_string())) - .add_ui_locale(LanguageTag::new("fr".to_string())) - .add_ui_locale(LanguageTag::new("en".to_string())) - .add_auth_context_value(AuthenticationContextClass::new( - "urn:mace:incommon:iap:silver".to_string(), - )) - .url(); - assert_eq!( - "https://example/authorize?response_type=code&client_id=aaa&\ - state=CSRF123&redirect_uri=http%3A%2F%2Flocalhost%3A8888%2F&scope=openid+email&\ - nonce=NONCE456&acr_values=urn%3Amace%3Aincommon%3Aiap%3Asilver&display=touch&\ - max_age=1800&prompt=login+consent&ui_locales=fr-CA+fr+en", - authorize_url.to_string() - ); - - let serialized_jwt = - "eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJodHRwczovL2V4YW1wbGUuY29tIiwiYXVkIjpbIm15X2NsaWVudCJdL\ - CJleHAiOjE1NDQ5MzIxNDksImlhdCI6MTU0NDkyODU0OSwiYXV0aF90aW1lIjoxNTQ0OTI4NTQ4LCJub25jZSI\ - 6InRoZV9ub25jZSIsImFjciI6InRoZV9hY3IiLCJzdWIiOiJzdWJqZWN0In0.gb5HuuyDMu-LvYvG-jJNIJPEZ\ - 823qNwvgNjdAtW0HJpgwJWhJq0hOHUuZz6lvf8ud5xbg5GOo0Q37v3Ke08TvGu6E1USWjecZzp1aYVm9BiMvw5\ - EBRUrwAaOCG2XFjuOKUVfglSMJnRnoNqVVIWpCAr1ETjZzRIbkU3n5GQRguC5CwN5n45I3dtjoKuNGc2Ni-IMl\ - J2nRiCJOl2FtStdgs-doc-A9DHtO01x-5HCwytXvcE28Snur1JnqpUgmWrQ8gZMGuijKirgNnze2Dd5BsZRHZ2\ - CLGIwBsCnauBrJy_NNlQg4hUcSlGsuTa0dmZY7mCf4BN2WCpyOh0wgtkAgQ"; - let id_token = serde_json::from_value::(serde_json::Value::String( - serialized_jwt.to_string(), - )) - .unwrap(); - - let (authorize_url, _, _) = client - .authorize_url(flow.clone(), new_csrf, new_nonce) - .add_scope(Scope::new("email".to_string())) - .set_display(CoreAuthDisplay::Touch) - .set_id_token_hint(&id_token) - .set_login_hint(LoginHint::new("foo@bar.com".to_string())) - .add_prompt(CoreAuthPrompt::Login) - .add_prompt(CoreAuthPrompt::Consent) - .set_max_age(Duration::from_secs(1800)) - .add_ui_locale(LanguageTag::new("fr-CA".to_string())) - .add_ui_locale(LanguageTag::new("fr".to_string())) - .add_ui_locale(LanguageTag::new("en".to_string())) - .add_auth_context_value(AuthenticationContextClass::new( - "urn:mace:incommon:iap:silver".to_string(), - )) - .add_extra_param("foo", "bar") - .url(); - assert_eq!( - format!( - "https://example/authorize?response_type=code&client_id=aaa&state=CSRF123&\ - redirect_uri=http%3A%2F%2Flocalhost%3A8888%2F&scope=openid+email&foo=bar&\ - nonce=NONCE456&acr_values=urn%3Amace%3Aincommon%3Aiap%3Asilver&display=touch&\ - id_token_hint={}&login_hint=foo%40bar.com&\ - max_age=1800&prompt=login+consent&ui_locales=fr-CA+fr+en", - serialized_jwt - ), - authorize_url.to_string() - ); - - let (authorize_url, _, _) = client - .authorize_url(flow, new_csrf, new_nonce) - .add_scopes(vec![ - Scope::new("email".to_string()), - Scope::new("profile".to_string()), - ]) - .set_display(CoreAuthDisplay::Touch) - .set_id_token_hint(&id_token) - .set_login_hint(LoginHint::new("foo@bar.com".to_string())) - .add_prompt(CoreAuthPrompt::Login) - .add_prompt(CoreAuthPrompt::Consent) - .set_max_age(Duration::from_secs(1800)) - .add_ui_locale(LanguageTag::new("fr-CA".to_string())) - .add_ui_locale(LanguageTag::new("fr".to_string())) - .add_ui_locale(LanguageTag::new("en".to_string())) - .add_auth_context_value(AuthenticationContextClass::new( - "urn:mace:incommon:iap:silver".to_string(), - )) - .add_extra_param("foo", "bar") - .url(); - assert_eq!( - format!( - "https://example/authorize?response_type=code&client_id=aaa&state=CSRF123&\ - redirect_uri=http%3A%2F%2Flocalhost%3A8888%2F&scope=openid+email+profile&foo=bar&\ - nonce=NONCE456&acr_values=urn%3Amace%3Aincommon%3Aiap%3Asilver&display=touch&\ - id_token_hint={}&login_hint=foo%40bar.com&\ - max_age=1800&prompt=login+consent&ui_locales=fr-CA+fr+en", - serialized_jwt - ), - authorize_url.to_string() - ); - } - - #[test] - fn test_authorize_url_redirect_url_override() { - let client = new_client() - .set_redirect_uri(RedirectUrl::new("http://localhost:8888/".to_string()).unwrap()); - - let flow = CoreAuthenticationFlow::AuthorizationCode; - - fn new_csrf() -> CsrfToken { - CsrfToken::new("CSRF123".to_string()) - } - fn new_nonce() -> Nonce { - Nonce::new("NONCE456".to_string()) - } - - let (authorize_url, _, _) = client - .authorize_url(flow, new_csrf, new_nonce) - .add_scope(Scope::new("email".to_string())) - .set_display(CoreAuthDisplay::Touch) - .add_prompt(CoreAuthPrompt::Login) - .add_prompt(CoreAuthPrompt::Consent) - .set_max_age(Duration::from_secs(1800)) - .add_ui_locale(LanguageTag::new("fr-CA".to_string())) - .add_ui_locale(LanguageTag::new("fr".to_string())) - .add_ui_locale(LanguageTag::new("en".to_string())) - .add_auth_context_value(AuthenticationContextClass::new( - "urn:mace:incommon:iap:silver".to_string(), - )) - .set_redirect_uri(Cow::Owned( - RedirectUrl::new("http://localhost:8888/alternative".to_string()).unwrap(), - )) - .url(); - assert_eq!( - "https://example/authorize?response_type=code&client_id=aaa&\ - state=CSRF123&redirect_uri=http%3A%2F%2Flocalhost%3A8888%2Falternative&scope=openid+email&\ - nonce=NONCE456&acr_values=urn%3Amace%3Aincommon%3Aiap%3Asilver&display=touch&\ - max_age=1800&prompt=login+consent&ui_locales=fr-CA+fr+en", - authorize_url.to_string() - ); - } -} diff --git a/src/logout.rs b/src/logout.rs index 93ddf05..219227a 100644 --- a/src/logout.rs +++ b/src/logout.rs @@ -4,9 +4,10 @@ use crate::core::{ CoreJweKeyManagementAlgorithm, CoreJwsSigningAlgorithm, CoreResponseMode, CoreResponseType, CoreSubjectIdentifierType, }; +use crate::helpers::join_vec; use crate::types::{LogoutHint, PostLogoutRedirectUrl}; use crate::{ - join_vec, AdditionalClaims, AdditionalProviderMetadata, ClientId, CsrfToken, + AdditionalClaims, AdditionalProviderMetadata, ClientId, CsrfToken, EmptyAdditionalProviderMetadata, EndSessionUrl, GenderClaim, IdToken, JsonWebKeyType, JweContentEncryptionAlgorithm, JwsSigningAlgorithm, LanguageTag, ProviderMetadata, }; diff --git a/src/macros.rs b/src/macros.rs index 4fe62c2..83ea04d 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -133,7 +133,7 @@ macro_rules! new_type { $name(s) } } - impl Deref for $name { + impl std::ops::Deref for $name { type Target = $type; fn deref(&self) -> &$type { &self.0 @@ -171,7 +171,7 @@ macro_rules! new_type { $name(s) } } - impl Deref for $name { + impl std::ops::Deref for $name { type Target = $type; fn deref(&self) -> &$type { &self.0 @@ -248,7 +248,7 @@ macro_rules! new_secret_type { pub fn secret(&self) -> &$type { &self.0 } } impl Debug for $name { - fn fmt(&self, f: &mut Formatter) -> Result<(), FormatterError> { + fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { write!(f, concat!(stringify!($name), "([redacted])")) } } @@ -312,30 +312,30 @@ macro_rules! new_url_type { ) => { $(#[$attr])* #[derive(Clone)] - pub struct $name(Url, String); + pub struct $name(url::Url, String); impl $name { #[doc = $new_doc] pub fn new(url: String) -> Result { - Ok($name(Url::parse(&url)?, url)) + Ok($name(url::Url::parse(&url)?, url)) } #[doc = $from_url_doc] - pub fn from_url(url: Url) -> Self { + pub fn from_url(url: url::Url) -> Self { let s = url.to_string(); Self(url, s) } #[doc = $url_doc] - pub fn url(&self) -> &Url { + pub fn url(&self) -> &url::Url { return &self.0; } $($item)* } - impl Deref for $name { + impl std::ops::Deref for $name { type Target = String; fn deref(&self) -> &String { &self.1 } } - impl From<$name> for Url { - fn from(t: $name) -> Url { + impl From<$name> for url::Url { + fn from(t: $name) -> url::Url { t.0 } } diff --git a/src/registration.rs b/src/registration/mod.rs similarity index 55% rename from src/registration.rs rename to src/registration/mod.rs index 5192785..15e94ae 100644 --- a/src/registration.rs +++ b/src/registration/mod.rs @@ -1,16 +1,17 @@ +use crate::helpers::serde_utc_seconds_opt; use crate::http_utils::{auth_bearer, check_content_type, MIME_TYPE_JSON}; -use crate::types::helpers::{serde_utc_seconds_opt, split_language_tag_key}; +use crate::types::localized::split_language_tag_key; use crate::types::{ ApplicationType, AuthenticationContextClass, ClientAuthMethod, ClientConfigUrl, - ClientContactEmail, ClientName, ClientUrl, GrantType, InitiateLoginUrl, JsonWebKeySetUrl, - JsonWebKeyType, JsonWebKeyUse, JweContentEncryptionAlgorithm, JweKeyManagementAlgorithm, - JwsSigningAlgorithm, LocalizedClaim, LogoUrl, PolicyUrl, RegistrationAccessToken, - RegistrationUrl, RequestUrl, ResponseType, ResponseTypes, SectorIdentifierUrl, - SubjectIdentifierType, ToSUrl, + ClientContactEmail, ClientName, ClientUrl, GrantType, InitiateLoginUrl, LogoUrl, PolicyUrl, + RegistrationAccessToken, RegistrationUrl, RequestUrl, ResponseType, ResponseTypes, + SectorIdentifierUrl, SubjectIdentifierType, ToSUrl, }; use crate::{ AccessToken, ClientId, ClientSecret, ErrorResponseType, HttpRequest, HttpResponse, JsonWebKey, - JsonWebKeySet, RedirectUrl, StandardErrorResponse, + JsonWebKeySet, JsonWebKeySetUrl, JsonWebKeyType, JsonWebKeyUse, JweContentEncryptionAlgorithm, + JweKeyManagementAlgorithm, JwsSigningAlgorithm, LocalizedClaim, RedirectUrl, + StandardErrorResponse, }; use chrono::{DateTime, Utc}; @@ -27,6 +28,9 @@ use std::future::Future; use std::marker::PhantomData; use std::time::Duration; +#[cfg(test)] +mod tests; + /// Trait for adding extra fields to [`ClientMetadata`]. pub trait AdditionalClientMetadata: Debug + DeserializeOwned + Serialize {} @@ -825,651 +829,3 @@ where #[error("Server returned error: {0}")] ServerResponse(StandardErrorResponse), } - -#[cfg(test)] -mod tests { - use crate::core::{ - CoreApplicationType, CoreClientAuthMethod, CoreClientMetadata, - CoreClientRegistrationResponse, CoreGrantType, CoreJweContentEncryptionAlgorithm, - CoreJweKeyManagementAlgorithm, CoreJwsSigningAlgorithm, CoreResponseType, - CoreSubjectIdentifierType, - }; - use crate::jwt::tests::TEST_RSA_PUB_KEY; - use crate::{ - AuthenticationContextClass, ClientConfigUrl, ClientContactEmail, ClientName, ClientUrl, - JsonWebKeySet, JsonWebKeySetUrl, LanguageTag, LogoUrl, PolicyUrl, RequestUrl, - ResponseTypes, SectorIdentifierUrl, ToSUrl, - }; - use crate::{ClientId, RedirectUrl}; - - use chrono::{TimeZone, Utc}; - use itertools::sorted; - - use std::time::Duration; - - #[test] - fn test_metadata_serialization() { - // `jwks_uri` and `jwks` aren't supposed to be used together, but this test is just for - // serialization/deserialization. - let json_response = format!("{{ - \"redirect_uris\": [\"https://example.com/redirect-1\", \"https://example.com/redirect-2\"], - \"response_types\": [\"code\", \"code token id_token\"], - \"grant_types\": [\"authorization_code\", \"client_credentials\", \"implicit\", \ - \"password\", \"refresh_token\"], - \"application_type\": \"web\", - \"contacts\": [\"user@example.com\", \"admin@openidconnect.local\"], - \"client_name\": \"Example\", - \"client_name#es\": \"Ejemplo\", - \"logo_uri\": \"https://example.com/logo.png\", - \"logo_uri#fr\": \"https://example.com/logo-fr.png\", - \"client_uri\": \"https://example.com/client-app\", - \"client_uri#de\": \"https://example.com/client-app-de\", - \"policy_uri\": \"https://example.com/policy\", - \"policy_uri#sr-Latn\": \"https://example.com/policy-sr-latin\", - \"tos_uri\": \"https://example.com/tos\", - \"tos_uri#sr-Cyrl\": \"https://example.com/tos-sr-cyrl\", - \"jwks_uri\": \"https://example.com/jwks\", - \"jwks\": {{\"keys\": [{}]}}, - \"sector_identifier_uri\": \"https://example.com/sector\", - \"subject_type\": \"pairwise\", - \"id_token_signed_response_alg\": \"HS256\", - \"id_token_encrypted_response_alg\": \"RSA1_5\", - \"id_token_encrypted_response_enc\": \"A128CBC-HS256\", - \"userinfo_signed_response_alg\": \"RS384\", - \"userinfo_encrypted_response_alg\": \"RSA-OAEP\", - \"userinfo_encrypted_response_enc\": \"A256CBC-HS512\", - \"request_object_signing_alg\": \"ES512\", - \"request_object_encryption_alg\": \"ECDH-ES+A128KW\", - \"request_object_encryption_enc\": \"A256GCM\", - \"token_endpoint_auth_method\": \"client_secret_basic\", - \"token_endpoint_auth_signing_alg\": \"PS512\", - \"default_max_age\": 3600, - \"require_auth_time\": true, - \"default_acr_values\": [\"0\", \"urn:mace:incommon:iap:silver\", \ - \"urn:mace:incommon:iap:bronze\"], - \"initiate_login_uri\": \"https://example.com/login\", - \"request_uris\": [\"https://example.com/request-1\", \"https://example.com/request-2\"] - }}", TEST_RSA_PUB_KEY); - - let client_metadata: CoreClientMetadata = serde_json::from_str(&json_response).unwrap(); - - assert_eq!( - *client_metadata.redirect_uris(), - vec![ - RedirectUrl::new("https://example.com/redirect-1".to_string()).unwrap(), - RedirectUrl::new("https://example.com/redirect-2".to_string()).unwrap(), - ] - ); - assert_eq!( - *client_metadata.response_types().unwrap(), - vec![ - ResponseTypes::new(vec![CoreResponseType::Code]), - ResponseTypes::new(vec![ - CoreResponseType::Code, - CoreResponseType::Token, - CoreResponseType::IdToken, - ]), - ] - ); - assert_eq!( - client_metadata.grant_types().unwrap(), - &vec![ - CoreGrantType::AuthorizationCode, - CoreGrantType::ClientCredentials, - CoreGrantType::Implicit, - CoreGrantType::Password, - CoreGrantType::RefreshToken, - ] - ); - assert_eq!( - *client_metadata.application_type().unwrap(), - CoreApplicationType::Web - ); - assert_eq!( - *client_metadata.contacts().unwrap(), - vec![ - ClientContactEmail::new("user@example.com".to_string()), - ClientContactEmail::new("admin@openidconnect.local".to_string()), - ] - ); - assert_eq!( - sorted(client_metadata.client_name().unwrap().clone()) - .collect::, ClientName)>>(), - vec![ - (None, ClientName::new("Example".to_string())), - ( - Some(LanguageTag::new("es".to_string())), - ClientName::new("Ejemplo".to_string()), - ), - ] - ); - assert_eq!( - sorted(client_metadata.logo_uri().unwrap().clone()) - .collect::, LogoUrl)>>(), - vec![ - ( - None, - LogoUrl::new("https://example.com/logo.png".to_string()).unwrap(), - ), - ( - Some(LanguageTag::new("fr".to_string())), - LogoUrl::new("https://example.com/logo-fr.png".to_string()).unwrap(), - ), - ] - ); - assert_eq!( - sorted(client_metadata.client_uri().unwrap().clone()) - .collect::, ClientUrl)>>(), - vec![ - ( - None, - ClientUrl::new("https://example.com/client-app".to_string()).unwrap(), - ), - ( - Some(LanguageTag::new("de".to_string())), - ClientUrl::new("https://example.com/client-app-de".to_string()).unwrap(), - ), - ] - ); - assert_eq!( - sorted(client_metadata.policy_uri().unwrap().clone()) - .collect::, PolicyUrl)>>(), - vec![ - ( - None, - PolicyUrl::new("https://example.com/policy".to_string()).unwrap(), - ), - ( - Some(LanguageTag::new("sr-Latn".to_string())), - PolicyUrl::new("https://example.com/policy-sr-latin".to_string()).unwrap(), - ), - ] - ); - assert_eq!( - sorted(client_metadata.tos_uri().unwrap().clone()) - .collect::, ToSUrl)>>(), - vec![ - ( - None, - ToSUrl::new("https://example.com/tos".to_string()).unwrap(), - ), - ( - Some(LanguageTag::new("sr-Cyrl".to_string())), - ToSUrl::new("https://example.com/tos-sr-cyrl".to_string()).unwrap(), - ), - ] - ); - assert_eq!( - *client_metadata.jwks_uri().unwrap(), - JsonWebKeySetUrl::new("https://example.com/jwks".to_string()).unwrap() - ); - assert_eq!( - client_metadata.jwks(), - Some(&JsonWebKeySet::new(vec![serde_json::from_str( - TEST_RSA_PUB_KEY - ) - .unwrap()],)) - ); - assert_eq!( - *client_metadata.sector_identifier_uri().unwrap(), - SectorIdentifierUrl::new("https://example.com/sector".to_string()).unwrap() - ); - assert_eq!( - *client_metadata.subject_type().unwrap(), - CoreSubjectIdentifierType::Pairwise - ); - assert_eq!( - *client_metadata.id_token_signed_response_alg().unwrap(), - CoreJwsSigningAlgorithm::HmacSha256 - ); - assert_eq!( - *client_metadata.id_token_encrypted_response_alg().unwrap(), - CoreJweKeyManagementAlgorithm::RsaPkcs1V15 - ); - assert_eq!( - *client_metadata.id_token_encrypted_response_enc().unwrap(), - CoreJweContentEncryptionAlgorithm::Aes128CbcHmacSha256 - ); - assert_eq!( - *client_metadata.userinfo_signed_response_alg().unwrap(), - CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha384 - ); - assert_eq!( - *client_metadata.userinfo_encrypted_response_alg().unwrap(), - CoreJweKeyManagementAlgorithm::RsaOaep - ); - assert_eq!( - *client_metadata.userinfo_encrypted_response_enc().unwrap(), - CoreJweContentEncryptionAlgorithm::Aes256CbcHmacSha512 - ); - assert_eq!( - *client_metadata.request_object_signing_alg().unwrap(), - CoreJwsSigningAlgorithm::EcdsaP521Sha512 - ); - assert_eq!( - *client_metadata.request_object_encryption_alg().unwrap(), - CoreJweKeyManagementAlgorithm::EcdhEsAesKeyWrap128 - ); - assert_eq!( - *client_metadata.request_object_encryption_enc().unwrap(), - CoreJweContentEncryptionAlgorithm::Aes256Gcm - ); - assert_eq!( - *client_metadata.token_endpoint_auth_method().unwrap(), - CoreClientAuthMethod::ClientSecretBasic - ); - assert_eq!( - *client_metadata.token_endpoint_auth_signing_alg().unwrap(), - CoreJwsSigningAlgorithm::RsaSsaPssSha512 - ); - assert_eq!( - *client_metadata.default_max_age().unwrap(), - Duration::from_secs(3600) - ); - assert!(client_metadata.require_auth_time().unwrap()); - assert_eq!( - *client_metadata.default_acr_values().unwrap(), - vec![ - AuthenticationContextClass::new("0".to_string()), - AuthenticationContextClass::new("urn:mace:incommon:iap:silver".to_string()), - AuthenticationContextClass::new("urn:mace:incommon:iap:bronze".to_string()), - ] - ); - assert_eq!( - *client_metadata.sector_identifier_uri().unwrap(), - SectorIdentifierUrl::new("https://example.com/sector".to_string()).unwrap() - ); - assert_eq!( - *client_metadata.request_uris().unwrap(), - vec![ - RequestUrl::new("https://example.com/request-1".to_string()).unwrap(), - RequestUrl::new("https://example.com/request-2".to_string()).unwrap(), - ] - ); - let serialized_json = serde_json::to_string(&client_metadata).unwrap(); - - assert_eq!( - client_metadata, - serde_json::from_str(&serialized_json).unwrap() - ); - } - - #[test] - fn test_metadata_serialization_minimal() { - let json_response = "{\"redirect_uris\": [\"https://example.com/redirect-1\"]}"; - - let client_metadata: CoreClientMetadata = serde_json::from_str(json_response).unwrap(); - - assert_eq!( - *client_metadata.redirect_uris(), - vec![RedirectUrl::new("https://example.com/redirect-1".to_string()).unwrap(),] - ); - assert_eq!(client_metadata.response_types(), None); - assert_eq!(client_metadata.grant_types(), None); - assert_eq!(client_metadata.application_type(), None); - assert_eq!(client_metadata.contacts(), None); - assert_eq!(client_metadata.client_name(), None); - assert_eq!(client_metadata.logo_uri(), None); - assert_eq!(client_metadata.client_uri(), None); - assert_eq!(client_metadata.policy_uri(), None); - assert_eq!(client_metadata.tos_uri(), None); - assert_eq!(client_metadata.jwks_uri(), None); - assert_eq!(client_metadata.jwks(), None); - assert_eq!(client_metadata.sector_identifier_uri(), None); - assert_eq!(client_metadata.subject_type(), None); - assert_eq!(client_metadata.id_token_signed_response_alg(), None); - assert_eq!(client_metadata.id_token_encrypted_response_alg(), None); - assert_eq!(client_metadata.id_token_encrypted_response_enc(), None); - assert_eq!(client_metadata.userinfo_signed_response_alg(), None); - assert_eq!(client_metadata.userinfo_encrypted_response_alg(), None); - assert_eq!(client_metadata.userinfo_encrypted_response_enc(), None); - assert_eq!(client_metadata.request_object_signing_alg(), None); - assert_eq!(client_metadata.request_object_encryption_alg(), None); - assert_eq!(client_metadata.request_object_encryption_enc(), None); - assert_eq!(client_metadata.token_endpoint_auth_method(), None); - assert_eq!(client_metadata.token_endpoint_auth_signing_alg(), None); - assert_eq!(client_metadata.default_max_age(), None); - assert_eq!(client_metadata.require_auth_time(), None); - assert_eq!(client_metadata.default_acr_values(), None); - assert_eq!(client_metadata.sector_identifier_uri(), None); - assert_eq!(client_metadata.request_uris(), None); - - let serialized_json = serde_json::to_string(&client_metadata).unwrap(); - - assert_eq!( - client_metadata, - serde_json::from_str(&serialized_json).unwrap() - ); - } - - #[test] - fn test_response_serialization() { - let json_response = format!("{{ - \"client_id\": \"abcdefgh\", - \"client_secret\": \"shhhh\", - \"registration_access_token\": \"use_me_to_update_registration\", - \"registration_client_uri\": \"https://example-provider.com/registration\", - \"client_id_issued_at\": 1523953306, - \"client_secret_expires_at\": 1526545306, - \"redirect_uris\": [\"https://example.com/redirect-1\", \"https://example.com/redirect-2\"], - \"response_types\": [\"code\", \"code token id_token\"], - \"grant_types\": [\"authorization_code\", \"client_credentials\", \"implicit\", \ - \"password\", \"refresh_token\"], - \"application_type\": \"web\", - \"contacts\": [\"user@example.com\", \"admin@openidconnect.local\"], - \"client_name\": \"Example\", - \"client_name#es\": \"Ejemplo\", - \"logo_uri\": \"https://example.com/logo.png\", - \"logo_uri#fr\": \"https://example.com/logo-fr.png\", - \"client_uri\": \"https://example.com/client-app\", - \"client_uri#de\": \"https://example.com/client-app-de\", - \"policy_uri\": \"https://example.com/policy\", - \"policy_uri#sr-Latn\": \"https://example.com/policy-sr-latin\", - \"tos_uri\": \"https://example.com/tos\", - \"tos_uri#sr-Cyrl\": \"https://example.com/tos-sr-cyrl\", - \"jwks_uri\": \"https://example.com/jwks\", - \"jwks\": {{\"keys\": [{}]}}, - \"sector_identifier_uri\": \"https://example.com/sector\", - \"subject_type\": \"pairwise\", - \"id_token_signed_response_alg\": \"HS256\", - \"id_token_encrypted_response_alg\": \"RSA1_5\", - \"id_token_encrypted_response_enc\": \"A128CBC-HS256\", - \"userinfo_signed_response_alg\": \"RS384\", - \"userinfo_encrypted_response_alg\": \"RSA-OAEP\", - \"userinfo_encrypted_response_enc\": \"A256CBC-HS512\", - \"request_object_signing_alg\": \"ES512\", - \"request_object_encryption_alg\": \"ECDH-ES+A128KW\", - \"request_object_encryption_enc\": \"A256GCM\", - \"token_endpoint_auth_method\": \"client_secret_basic\", - \"token_endpoint_auth_signing_alg\": \"PS512\", - \"default_max_age\": 3600, - \"require_auth_time\": true, - \"default_acr_values\": [\"0\", \"urn:mace:incommon:iap:silver\", \ - \"urn:mace:incommon:iap:bronze\"], - \"initiate_login_uri\": \"https://example.com/login\", - \"request_uris\": [\"https://example.com/request-1\", \"https://example.com/request-2\"] - }}", TEST_RSA_PUB_KEY); - - let registration_response: CoreClientRegistrationResponse = - serde_json::from_str(&json_response).unwrap(); - - assert_eq!( - *registration_response.client_id(), - ClientId::new("abcdefgh".to_string()) - ); - assert_eq!( - *registration_response.client_secret().unwrap().secret(), - "shhhh" - ); - assert_eq!( - *registration_response - .registration_access_token() - .unwrap() - .secret(), - "use_me_to_update_registration", - ); - assert_eq!( - *registration_response.registration_client_uri().unwrap(), - ClientConfigUrl::new("https://example-provider.com/registration".to_string()).unwrap() - ); - assert_eq!( - registration_response.client_id_issued_at().unwrap(), - Utc.timestamp_opt(1523953306, 0) - .single() - .expect("valid timestamp") - ); - assert_eq!( - registration_response.client_secret_expires_at().unwrap(), - Utc.timestamp_opt(1526545306, 0) - .single() - .expect("valid timestamp") - ); - assert_eq!( - *registration_response.redirect_uris(), - vec![ - RedirectUrl::new("https://example.com/redirect-1".to_string()).unwrap(), - RedirectUrl::new("https://example.com/redirect-2".to_string()).unwrap(), - ] - ); - assert_eq!( - *registration_response.response_types().unwrap(), - vec![ - ResponseTypes::new(vec![CoreResponseType::Code]), - ResponseTypes::new(vec![ - CoreResponseType::Code, - CoreResponseType::Token, - CoreResponseType::IdToken, - ]), - ] - ); - assert_eq!( - registration_response.grant_types().unwrap(), - &vec![ - CoreGrantType::AuthorizationCode, - CoreGrantType::ClientCredentials, - CoreGrantType::Implicit, - CoreGrantType::Password, - CoreGrantType::RefreshToken, - ] - ); - assert_eq!( - *registration_response.application_type().unwrap(), - CoreApplicationType::Web - ); - assert_eq!( - *registration_response.contacts().unwrap(), - vec![ - ClientContactEmail::new("user@example.com".to_string()), - ClientContactEmail::new("admin@openidconnect.local".to_string()), - ] - ); - assert_eq!( - sorted(registration_response.client_name().unwrap().clone()) - .collect::, ClientName)>>(), - vec![ - (None, ClientName::new("Example".to_string())), - ( - Some(LanguageTag::new("es".to_string())), - ClientName::new("Ejemplo".to_string()), - ), - ] - ); - assert_eq!( - sorted(registration_response.logo_uri().unwrap().clone()) - .collect::, LogoUrl)>>(), - vec![ - ( - None, - LogoUrl::new("https://example.com/logo.png".to_string()).unwrap(), - ), - ( - Some(LanguageTag::new("fr".to_string())), - LogoUrl::new("https://example.com/logo-fr.png".to_string()).unwrap(), - ), - ] - ); - assert_eq!( - sorted(registration_response.client_uri().unwrap().clone()) - .collect::, ClientUrl)>>(), - vec![ - ( - None, - ClientUrl::new("https://example.com/client-app".to_string()).unwrap(), - ), - ( - Some(LanguageTag::new("de".to_string())), - ClientUrl::new("https://example.com/client-app-de".to_string()).unwrap(), - ), - ] - ); - assert_eq!( - sorted(registration_response.policy_uri().unwrap().clone()) - .collect::, PolicyUrl)>>(), - vec![ - ( - None, - PolicyUrl::new("https://example.com/policy".to_string()).unwrap(), - ), - ( - Some(LanguageTag::new("sr-Latn".to_string())), - PolicyUrl::new("https://example.com/policy-sr-latin".to_string()).unwrap(), - ), - ] - ); - assert_eq!( - sorted(registration_response.tos_uri().unwrap().clone()) - .collect::, ToSUrl)>>(), - vec![ - ( - None, - ToSUrl::new("https://example.com/tos".to_string()).unwrap(), - ), - ( - Some(LanguageTag::new("sr-Cyrl".to_string())), - ToSUrl::new("https://example.com/tos-sr-cyrl".to_string()).unwrap(), - ), - ] - ); - assert_eq!( - *registration_response.jwks_uri().unwrap(), - JsonWebKeySetUrl::new("https://example.com/jwks".to_string()).unwrap() - ); - assert_eq!( - registration_response.jwks(), - Some(&JsonWebKeySet::new(vec![serde_json::from_str( - TEST_RSA_PUB_KEY - ) - .unwrap()],)), - ); - assert_eq!( - *registration_response.sector_identifier_uri().unwrap(), - SectorIdentifierUrl::new("https://example.com/sector".to_string()).unwrap() - ); - assert_eq!( - *registration_response.subject_type().unwrap(), - CoreSubjectIdentifierType::Pairwise - ); - assert_eq!( - *registration_response - .id_token_signed_response_alg() - .unwrap(), - CoreJwsSigningAlgorithm::HmacSha256 - ); - assert_eq!( - *registration_response - .id_token_encrypted_response_alg() - .unwrap(), - CoreJweKeyManagementAlgorithm::RsaPkcs1V15 - ); - assert_eq!( - *registration_response - .id_token_encrypted_response_enc() - .unwrap(), - CoreJweContentEncryptionAlgorithm::Aes128CbcHmacSha256 - ); - assert_eq!( - *registration_response - .userinfo_signed_response_alg() - .unwrap(), - CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha384 - ); - assert_eq!( - *registration_response - .userinfo_encrypted_response_alg() - .unwrap(), - CoreJweKeyManagementAlgorithm::RsaOaep - ); - assert_eq!( - *registration_response - .userinfo_encrypted_response_enc() - .unwrap(), - CoreJweContentEncryptionAlgorithm::Aes256CbcHmacSha512 - ); - assert_eq!( - *registration_response.request_object_signing_alg().unwrap(), - CoreJwsSigningAlgorithm::EcdsaP521Sha512 - ); - assert_eq!( - *registration_response - .request_object_encryption_alg() - .unwrap(), - CoreJweKeyManagementAlgorithm::EcdhEsAesKeyWrap128 - ); - assert_eq!( - *registration_response - .request_object_encryption_enc() - .unwrap(), - CoreJweContentEncryptionAlgorithm::Aes256Gcm - ); - assert_eq!( - *registration_response.token_endpoint_auth_method().unwrap(), - CoreClientAuthMethod::ClientSecretBasic - ); - assert_eq!( - *registration_response - .token_endpoint_auth_signing_alg() - .unwrap(), - CoreJwsSigningAlgorithm::RsaSsaPssSha512 - ); - assert_eq!( - *registration_response.default_max_age().unwrap(), - Duration::from_secs(3600) - ); - assert!(registration_response.require_auth_time().unwrap()); - assert_eq!( - *registration_response.default_acr_values().unwrap(), - vec![ - AuthenticationContextClass::new("0".to_string()), - AuthenticationContextClass::new("urn:mace:incommon:iap:silver".to_string()), - AuthenticationContextClass::new("urn:mace:incommon:iap:bronze".to_string()), - ] - ); - assert_eq!( - *registration_response.sector_identifier_uri().unwrap(), - SectorIdentifierUrl::new("https://example.com/sector".to_string()).unwrap() - ); - assert_eq!( - *registration_response.request_uris().unwrap(), - vec![ - RequestUrl::new("https://example.com/request-1".to_string()).unwrap(), - RequestUrl::new("https://example.com/request-2".to_string()).unwrap(), - ] - ); - let serialized_json = serde_json::to_string(®istration_response).unwrap(); - - let deserialized: CoreClientRegistrationResponse = - serde_json::from_str(&serialized_json).unwrap(); - assert_eq!(registration_response.client_id, deserialized.client_id); - assert_eq!( - registration_response.client_secret.unwrap().secret(), - deserialized.client_secret.unwrap().secret(), - ); - assert_eq!( - registration_response - .registration_access_token - .unwrap() - .secret(), - deserialized.registration_access_token.unwrap().secret(), - ); - assert_eq!( - registration_response.registration_client_uri, - deserialized.registration_client_uri, - ); - assert_eq!( - registration_response.client_id_issued_at, - deserialized.client_id_issued_at, - ); - assert_eq!( - registration_response.client_secret_expires_at, - deserialized.client_secret_expires_at, - ); - assert_eq!( - registration_response.client_metadata, - deserialized.client_metadata, - ); - assert_eq!( - registration_response.additional_response, - deserialized.additional_response, - ); - } -} diff --git a/src/registration/tests.rs b/src/registration/tests.rs new file mode 100644 index 0000000..59d3d26 --- /dev/null +++ b/src/registration/tests.rs @@ -0,0 +1,643 @@ +use crate::core::{ + CoreApplicationType, CoreClientAuthMethod, CoreClientMetadata, CoreClientRegistrationResponse, + CoreGrantType, CoreJweContentEncryptionAlgorithm, CoreJweKeyManagementAlgorithm, + CoreJwsSigningAlgorithm, CoreResponseType, CoreSubjectIdentifierType, +}; +use crate::jwt::tests::TEST_RSA_PUB_KEY; +use crate::{ + AuthenticationContextClass, ClientConfigUrl, ClientContactEmail, ClientName, ClientUrl, + JsonWebKeySet, JsonWebKeySetUrl, LanguageTag, LogoUrl, PolicyUrl, RequestUrl, ResponseTypes, + SectorIdentifierUrl, ToSUrl, +}; +use crate::{ClientId, RedirectUrl}; + +use chrono::{TimeZone, Utc}; +use itertools::sorted; + +use std::time::Duration; + +#[test] +fn test_metadata_serialization() { + // `jwks_uri` and `jwks` aren't supposed to be used together, but this test is just for + // serialization/deserialization. + let json_response = format!("{{ + \"redirect_uris\": [\"https://example.com/redirect-1\", \"https://example.com/redirect-2\"], + \"response_types\": [\"code\", \"code token id_token\"], + \"grant_types\": [\"authorization_code\", \"client_credentials\", \"implicit\", \ + \"password\", \"refresh_token\"], + \"application_type\": \"web\", + \"contacts\": [\"user@example.com\", \"admin@openidconnect.local\"], + \"client_name\": \"Example\", + \"client_name#es\": \"Ejemplo\", + \"logo_uri\": \"https://example.com/logo.png\", + \"logo_uri#fr\": \"https://example.com/logo-fr.png\", + \"client_uri\": \"https://example.com/client-app\", + \"client_uri#de\": \"https://example.com/client-app-de\", + \"policy_uri\": \"https://example.com/policy\", + \"policy_uri#sr-Latn\": \"https://example.com/policy-sr-latin\", + \"tos_uri\": \"https://example.com/tos\", + \"tos_uri#sr-Cyrl\": \"https://example.com/tos-sr-cyrl\", + \"jwks_uri\": \"https://example.com/jwks\", + \"jwks\": {{\"keys\": [{}]}}, + \"sector_identifier_uri\": \"https://example.com/sector\", + \"subject_type\": \"pairwise\", + \"id_token_signed_response_alg\": \"HS256\", + \"id_token_encrypted_response_alg\": \"RSA1_5\", + \"id_token_encrypted_response_enc\": \"A128CBC-HS256\", + \"userinfo_signed_response_alg\": \"RS384\", + \"userinfo_encrypted_response_alg\": \"RSA-OAEP\", + \"userinfo_encrypted_response_enc\": \"A256CBC-HS512\", + \"request_object_signing_alg\": \"ES512\", + \"request_object_encryption_alg\": \"ECDH-ES+A128KW\", + \"request_object_encryption_enc\": \"A256GCM\", + \"token_endpoint_auth_method\": \"client_secret_basic\", + \"token_endpoint_auth_signing_alg\": \"PS512\", + \"default_max_age\": 3600, + \"require_auth_time\": true, + \"default_acr_values\": [\"0\", \"urn:mace:incommon:iap:silver\", \ + \"urn:mace:incommon:iap:bronze\"], + \"initiate_login_uri\": \"https://example.com/login\", + \"request_uris\": [\"https://example.com/request-1\", \"https://example.com/request-2\"] + }}", TEST_RSA_PUB_KEY); + + let client_metadata: CoreClientMetadata = serde_json::from_str(&json_response).unwrap(); + + assert_eq!( + *client_metadata.redirect_uris(), + vec![ + RedirectUrl::new("https://example.com/redirect-1".to_string()).unwrap(), + RedirectUrl::new("https://example.com/redirect-2".to_string()).unwrap(), + ] + ); + assert_eq!( + *client_metadata.response_types().unwrap(), + vec![ + ResponseTypes::new(vec![CoreResponseType::Code]), + ResponseTypes::new(vec![ + CoreResponseType::Code, + CoreResponseType::Token, + CoreResponseType::IdToken, + ]), + ] + ); + assert_eq!( + client_metadata.grant_types().unwrap(), + &vec![ + CoreGrantType::AuthorizationCode, + CoreGrantType::ClientCredentials, + CoreGrantType::Implicit, + CoreGrantType::Password, + CoreGrantType::RefreshToken, + ] + ); + assert_eq!( + *client_metadata.application_type().unwrap(), + CoreApplicationType::Web + ); + assert_eq!( + *client_metadata.contacts().unwrap(), + vec![ + ClientContactEmail::new("user@example.com".to_string()), + ClientContactEmail::new("admin@openidconnect.local".to_string()), + ] + ); + assert_eq!( + sorted(client_metadata.client_name().unwrap().clone()) + .collect::, ClientName)>>(), + vec![ + (None, ClientName::new("Example".to_string())), + ( + Some(LanguageTag::new("es".to_string())), + ClientName::new("Ejemplo".to_string()), + ), + ] + ); + assert_eq!( + sorted(client_metadata.logo_uri().unwrap().clone()) + .collect::, LogoUrl)>>(), + vec![ + ( + None, + LogoUrl::new("https://example.com/logo.png".to_string()).unwrap(), + ), + ( + Some(LanguageTag::new("fr".to_string())), + LogoUrl::new("https://example.com/logo-fr.png".to_string()).unwrap(), + ), + ] + ); + assert_eq!( + sorted(client_metadata.client_uri().unwrap().clone()) + .collect::, ClientUrl)>>(), + vec![ + ( + None, + ClientUrl::new("https://example.com/client-app".to_string()).unwrap(), + ), + ( + Some(LanguageTag::new("de".to_string())), + ClientUrl::new("https://example.com/client-app-de".to_string()).unwrap(), + ), + ] + ); + assert_eq!( + sorted(client_metadata.policy_uri().unwrap().clone()) + .collect::, PolicyUrl)>>(), + vec![ + ( + None, + PolicyUrl::new("https://example.com/policy".to_string()).unwrap(), + ), + ( + Some(LanguageTag::new("sr-Latn".to_string())), + PolicyUrl::new("https://example.com/policy-sr-latin".to_string()).unwrap(), + ), + ] + ); + assert_eq!( + sorted(client_metadata.tos_uri().unwrap().clone()) + .collect::, ToSUrl)>>(), + vec![ + ( + None, + ToSUrl::new("https://example.com/tos".to_string()).unwrap(), + ), + ( + Some(LanguageTag::new("sr-Cyrl".to_string())), + ToSUrl::new("https://example.com/tos-sr-cyrl".to_string()).unwrap(), + ), + ] + ); + assert_eq!( + *client_metadata.jwks_uri().unwrap(), + JsonWebKeySetUrl::new("https://example.com/jwks".to_string()).unwrap() + ); + assert_eq!( + client_metadata.jwks(), + Some(&JsonWebKeySet::new(vec![serde_json::from_str( + TEST_RSA_PUB_KEY + ) + .unwrap()],)) + ); + assert_eq!( + *client_metadata.sector_identifier_uri().unwrap(), + SectorIdentifierUrl::new("https://example.com/sector".to_string()).unwrap() + ); + assert_eq!( + *client_metadata.subject_type().unwrap(), + CoreSubjectIdentifierType::Pairwise + ); + assert_eq!( + *client_metadata.id_token_signed_response_alg().unwrap(), + CoreJwsSigningAlgorithm::HmacSha256 + ); + assert_eq!( + *client_metadata.id_token_encrypted_response_alg().unwrap(), + CoreJweKeyManagementAlgorithm::RsaPkcs1V15 + ); + assert_eq!( + *client_metadata.id_token_encrypted_response_enc().unwrap(), + CoreJweContentEncryptionAlgorithm::Aes128CbcHmacSha256 + ); + assert_eq!( + *client_metadata.userinfo_signed_response_alg().unwrap(), + CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha384 + ); + assert_eq!( + *client_metadata.userinfo_encrypted_response_alg().unwrap(), + CoreJweKeyManagementAlgorithm::RsaOaep + ); + assert_eq!( + *client_metadata.userinfo_encrypted_response_enc().unwrap(), + CoreJweContentEncryptionAlgorithm::Aes256CbcHmacSha512 + ); + assert_eq!( + *client_metadata.request_object_signing_alg().unwrap(), + CoreJwsSigningAlgorithm::EcdsaP521Sha512 + ); + assert_eq!( + *client_metadata.request_object_encryption_alg().unwrap(), + CoreJweKeyManagementAlgorithm::EcdhEsAesKeyWrap128 + ); + assert_eq!( + *client_metadata.request_object_encryption_enc().unwrap(), + CoreJweContentEncryptionAlgorithm::Aes256Gcm + ); + assert_eq!( + *client_metadata.token_endpoint_auth_method().unwrap(), + CoreClientAuthMethod::ClientSecretBasic + ); + assert_eq!( + *client_metadata.token_endpoint_auth_signing_alg().unwrap(), + CoreJwsSigningAlgorithm::RsaSsaPssSha512 + ); + assert_eq!( + *client_metadata.default_max_age().unwrap(), + Duration::from_secs(3600) + ); + assert!(client_metadata.require_auth_time().unwrap()); + assert_eq!( + *client_metadata.default_acr_values().unwrap(), + vec![ + AuthenticationContextClass::new("0".to_string()), + AuthenticationContextClass::new("urn:mace:incommon:iap:silver".to_string()), + AuthenticationContextClass::new("urn:mace:incommon:iap:bronze".to_string()), + ] + ); + assert_eq!( + *client_metadata.sector_identifier_uri().unwrap(), + SectorIdentifierUrl::new("https://example.com/sector".to_string()).unwrap() + ); + assert_eq!( + *client_metadata.request_uris().unwrap(), + vec![ + RequestUrl::new("https://example.com/request-1".to_string()).unwrap(), + RequestUrl::new("https://example.com/request-2".to_string()).unwrap(), + ] + ); + let serialized_json = serde_json::to_string(&client_metadata).unwrap(); + + assert_eq!( + client_metadata, + serde_json::from_str(&serialized_json).unwrap() + ); +} + +#[test] +fn test_metadata_serialization_minimal() { + let json_response = "{\"redirect_uris\": [\"https://example.com/redirect-1\"]}"; + + let client_metadata: CoreClientMetadata = serde_json::from_str(json_response).unwrap(); + + assert_eq!( + *client_metadata.redirect_uris(), + vec![RedirectUrl::new("https://example.com/redirect-1".to_string()).unwrap(),] + ); + assert_eq!(client_metadata.response_types(), None); + assert_eq!(client_metadata.grant_types(), None); + assert_eq!(client_metadata.application_type(), None); + assert_eq!(client_metadata.contacts(), None); + assert_eq!(client_metadata.client_name(), None); + assert_eq!(client_metadata.logo_uri(), None); + assert_eq!(client_metadata.client_uri(), None); + assert_eq!(client_metadata.policy_uri(), None); + assert_eq!(client_metadata.tos_uri(), None); + assert_eq!(client_metadata.jwks_uri(), None); + assert_eq!(client_metadata.jwks(), None); + assert_eq!(client_metadata.sector_identifier_uri(), None); + assert_eq!(client_metadata.subject_type(), None); + assert_eq!(client_metadata.id_token_signed_response_alg(), None); + assert_eq!(client_metadata.id_token_encrypted_response_alg(), None); + assert_eq!(client_metadata.id_token_encrypted_response_enc(), None); + assert_eq!(client_metadata.userinfo_signed_response_alg(), None); + assert_eq!(client_metadata.userinfo_encrypted_response_alg(), None); + assert_eq!(client_metadata.userinfo_encrypted_response_enc(), None); + assert_eq!(client_metadata.request_object_signing_alg(), None); + assert_eq!(client_metadata.request_object_encryption_alg(), None); + assert_eq!(client_metadata.request_object_encryption_enc(), None); + assert_eq!(client_metadata.token_endpoint_auth_method(), None); + assert_eq!(client_metadata.token_endpoint_auth_signing_alg(), None); + assert_eq!(client_metadata.default_max_age(), None); + assert_eq!(client_metadata.require_auth_time(), None); + assert_eq!(client_metadata.default_acr_values(), None); + assert_eq!(client_metadata.sector_identifier_uri(), None); + assert_eq!(client_metadata.request_uris(), None); + + let serialized_json = serde_json::to_string(&client_metadata).unwrap(); + + assert_eq!( + client_metadata, + serde_json::from_str(&serialized_json).unwrap() + ); +} + +#[test] +fn test_response_serialization() { + let json_response = format!("{{ + \"client_id\": \"abcdefgh\", + \"client_secret\": \"shhhh\", + \"registration_access_token\": \"use_me_to_update_registration\", + \"registration_client_uri\": \"https://example-provider.com/registration\", + \"client_id_issued_at\": 1523953306, + \"client_secret_expires_at\": 1526545306, + \"redirect_uris\": [\"https://example.com/redirect-1\", \"https://example.com/redirect-2\"], + \"response_types\": [\"code\", \"code token id_token\"], + \"grant_types\": [\"authorization_code\", \"client_credentials\", \"implicit\", \ + \"password\", \"refresh_token\"], + \"application_type\": \"web\", + \"contacts\": [\"user@example.com\", \"admin@openidconnect.local\"], + \"client_name\": \"Example\", + \"client_name#es\": \"Ejemplo\", + \"logo_uri\": \"https://example.com/logo.png\", + \"logo_uri#fr\": \"https://example.com/logo-fr.png\", + \"client_uri\": \"https://example.com/client-app\", + \"client_uri#de\": \"https://example.com/client-app-de\", + \"policy_uri\": \"https://example.com/policy\", + \"policy_uri#sr-Latn\": \"https://example.com/policy-sr-latin\", + \"tos_uri\": \"https://example.com/tos\", + \"tos_uri#sr-Cyrl\": \"https://example.com/tos-sr-cyrl\", + \"jwks_uri\": \"https://example.com/jwks\", + \"jwks\": {{\"keys\": [{}]}}, + \"sector_identifier_uri\": \"https://example.com/sector\", + \"subject_type\": \"pairwise\", + \"id_token_signed_response_alg\": \"HS256\", + \"id_token_encrypted_response_alg\": \"RSA1_5\", + \"id_token_encrypted_response_enc\": \"A128CBC-HS256\", + \"userinfo_signed_response_alg\": \"RS384\", + \"userinfo_encrypted_response_alg\": \"RSA-OAEP\", + \"userinfo_encrypted_response_enc\": \"A256CBC-HS512\", + \"request_object_signing_alg\": \"ES512\", + \"request_object_encryption_alg\": \"ECDH-ES+A128KW\", + \"request_object_encryption_enc\": \"A256GCM\", + \"token_endpoint_auth_method\": \"client_secret_basic\", + \"token_endpoint_auth_signing_alg\": \"PS512\", + \"default_max_age\": 3600, + \"require_auth_time\": true, + \"default_acr_values\": [\"0\", \"urn:mace:incommon:iap:silver\", \ + \"urn:mace:incommon:iap:bronze\"], + \"initiate_login_uri\": \"https://example.com/login\", + \"request_uris\": [\"https://example.com/request-1\", \"https://example.com/request-2\"] + }}", TEST_RSA_PUB_KEY); + + let registration_response: CoreClientRegistrationResponse = + serde_json::from_str(&json_response).unwrap(); + + assert_eq!( + *registration_response.client_id(), + ClientId::new("abcdefgh".to_string()) + ); + assert_eq!( + *registration_response.client_secret().unwrap().secret(), + "shhhh" + ); + assert_eq!( + *registration_response + .registration_access_token() + .unwrap() + .secret(), + "use_me_to_update_registration", + ); + assert_eq!( + *registration_response.registration_client_uri().unwrap(), + ClientConfigUrl::new("https://example-provider.com/registration".to_string()).unwrap() + ); + assert_eq!( + registration_response.client_id_issued_at().unwrap(), + Utc.timestamp_opt(1523953306, 0) + .single() + .expect("valid timestamp") + ); + assert_eq!( + registration_response.client_secret_expires_at().unwrap(), + Utc.timestamp_opt(1526545306, 0) + .single() + .expect("valid timestamp") + ); + assert_eq!( + *registration_response.redirect_uris(), + vec![ + RedirectUrl::new("https://example.com/redirect-1".to_string()).unwrap(), + RedirectUrl::new("https://example.com/redirect-2".to_string()).unwrap(), + ] + ); + assert_eq!( + *registration_response.response_types().unwrap(), + vec![ + ResponseTypes::new(vec![CoreResponseType::Code]), + ResponseTypes::new(vec![ + CoreResponseType::Code, + CoreResponseType::Token, + CoreResponseType::IdToken, + ]), + ] + ); + assert_eq!( + registration_response.grant_types().unwrap(), + &vec![ + CoreGrantType::AuthorizationCode, + CoreGrantType::ClientCredentials, + CoreGrantType::Implicit, + CoreGrantType::Password, + CoreGrantType::RefreshToken, + ] + ); + assert_eq!( + *registration_response.application_type().unwrap(), + CoreApplicationType::Web + ); + assert_eq!( + *registration_response.contacts().unwrap(), + vec![ + ClientContactEmail::new("user@example.com".to_string()), + ClientContactEmail::new("admin@openidconnect.local".to_string()), + ] + ); + assert_eq!( + sorted(registration_response.client_name().unwrap().clone()) + .collect::, ClientName)>>(), + vec![ + (None, ClientName::new("Example".to_string())), + ( + Some(LanguageTag::new("es".to_string())), + ClientName::new("Ejemplo".to_string()), + ), + ] + ); + assert_eq!( + sorted(registration_response.logo_uri().unwrap().clone()) + .collect::, LogoUrl)>>(), + vec![ + ( + None, + LogoUrl::new("https://example.com/logo.png".to_string()).unwrap(), + ), + ( + Some(LanguageTag::new("fr".to_string())), + LogoUrl::new("https://example.com/logo-fr.png".to_string()).unwrap(), + ), + ] + ); + assert_eq!( + sorted(registration_response.client_uri().unwrap().clone()) + .collect::, ClientUrl)>>(), + vec![ + ( + None, + ClientUrl::new("https://example.com/client-app".to_string()).unwrap(), + ), + ( + Some(LanguageTag::new("de".to_string())), + ClientUrl::new("https://example.com/client-app-de".to_string()).unwrap(), + ), + ] + ); + assert_eq!( + sorted(registration_response.policy_uri().unwrap().clone()) + .collect::, PolicyUrl)>>(), + vec![ + ( + None, + PolicyUrl::new("https://example.com/policy".to_string()).unwrap(), + ), + ( + Some(LanguageTag::new("sr-Latn".to_string())), + PolicyUrl::new("https://example.com/policy-sr-latin".to_string()).unwrap(), + ), + ] + ); + assert_eq!( + sorted(registration_response.tos_uri().unwrap().clone()) + .collect::, ToSUrl)>>(), + vec![ + ( + None, + ToSUrl::new("https://example.com/tos".to_string()).unwrap(), + ), + ( + Some(LanguageTag::new("sr-Cyrl".to_string())), + ToSUrl::new("https://example.com/tos-sr-cyrl".to_string()).unwrap(), + ), + ] + ); + assert_eq!( + *registration_response.jwks_uri().unwrap(), + JsonWebKeySetUrl::new("https://example.com/jwks".to_string()).unwrap() + ); + assert_eq!( + registration_response.jwks(), + Some(&JsonWebKeySet::new(vec![serde_json::from_str( + TEST_RSA_PUB_KEY + ) + .unwrap()],)), + ); + assert_eq!( + *registration_response.sector_identifier_uri().unwrap(), + SectorIdentifierUrl::new("https://example.com/sector".to_string()).unwrap() + ); + assert_eq!( + *registration_response.subject_type().unwrap(), + CoreSubjectIdentifierType::Pairwise + ); + assert_eq!( + *registration_response + .id_token_signed_response_alg() + .unwrap(), + CoreJwsSigningAlgorithm::HmacSha256 + ); + assert_eq!( + *registration_response + .id_token_encrypted_response_alg() + .unwrap(), + CoreJweKeyManagementAlgorithm::RsaPkcs1V15 + ); + assert_eq!( + *registration_response + .id_token_encrypted_response_enc() + .unwrap(), + CoreJweContentEncryptionAlgorithm::Aes128CbcHmacSha256 + ); + assert_eq!( + *registration_response + .userinfo_signed_response_alg() + .unwrap(), + CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha384 + ); + assert_eq!( + *registration_response + .userinfo_encrypted_response_alg() + .unwrap(), + CoreJweKeyManagementAlgorithm::RsaOaep + ); + assert_eq!( + *registration_response + .userinfo_encrypted_response_enc() + .unwrap(), + CoreJweContentEncryptionAlgorithm::Aes256CbcHmacSha512 + ); + assert_eq!( + *registration_response.request_object_signing_alg().unwrap(), + CoreJwsSigningAlgorithm::EcdsaP521Sha512 + ); + assert_eq!( + *registration_response + .request_object_encryption_alg() + .unwrap(), + CoreJweKeyManagementAlgorithm::EcdhEsAesKeyWrap128 + ); + assert_eq!( + *registration_response + .request_object_encryption_enc() + .unwrap(), + CoreJweContentEncryptionAlgorithm::Aes256Gcm + ); + assert_eq!( + *registration_response.token_endpoint_auth_method().unwrap(), + CoreClientAuthMethod::ClientSecretBasic + ); + assert_eq!( + *registration_response + .token_endpoint_auth_signing_alg() + .unwrap(), + CoreJwsSigningAlgorithm::RsaSsaPssSha512 + ); + assert_eq!( + *registration_response.default_max_age().unwrap(), + Duration::from_secs(3600) + ); + assert!(registration_response.require_auth_time().unwrap()); + assert_eq!( + *registration_response.default_acr_values().unwrap(), + vec![ + AuthenticationContextClass::new("0".to_string()), + AuthenticationContextClass::new("urn:mace:incommon:iap:silver".to_string()), + AuthenticationContextClass::new("urn:mace:incommon:iap:bronze".to_string()), + ] + ); + assert_eq!( + *registration_response.sector_identifier_uri().unwrap(), + SectorIdentifierUrl::new("https://example.com/sector".to_string()).unwrap() + ); + assert_eq!( + *registration_response.request_uris().unwrap(), + vec![ + RequestUrl::new("https://example.com/request-1".to_string()).unwrap(), + RequestUrl::new("https://example.com/request-2".to_string()).unwrap(), + ] + ); + let serialized_json = serde_json::to_string(®istration_response).unwrap(); + + let deserialized: CoreClientRegistrationResponse = + serde_json::from_str(&serialized_json).unwrap(); + assert_eq!(registration_response.client_id, deserialized.client_id); + assert_eq!( + registration_response.client_secret.unwrap().secret(), + deserialized.client_secret.unwrap().secret(), + ); + assert_eq!( + registration_response + .registration_access_token + .unwrap() + .secret(), + deserialized.registration_access_token.unwrap().secret(), + ); + assert_eq!( + registration_response.registration_client_uri, + deserialized.registration_client_uri, + ); + assert_eq!( + registration_response.client_id_issued_at, + deserialized.client_id_issued_at, + ); + assert_eq!( + registration_response.client_secret_expires_at, + deserialized.client_secret_expires_at, + ); + assert_eq!( + registration_response.client_metadata, + deserialized.client_metadata, + ); + assert_eq!( + registration_response.additional_response, + deserialized.additional_response, + ); +} diff --git a/src/token.rs b/src/token.rs new file mode 100644 index 0000000..62442b4 --- /dev/null +++ b/src/token.rs @@ -0,0 +1,38 @@ +use crate::{ + AdditionalClaims, ExtraTokenFields, GenderClaim, IdToken, IdTokenFields, JsonWebKeyType, + JweContentEncryptionAlgorithm, JwsSigningAlgorithm, OAuth2TokenResponse, StandardTokenResponse, + TokenType, +}; + +/// Extends the base OAuth2 token response with an ID token. +pub trait TokenResponse: OAuth2TokenResponse +where + AC: AdditionalClaims, + GC: GenderClaim, + JE: JweContentEncryptionAlgorithm, + JS: JwsSigningAlgorithm, + JT: JsonWebKeyType, + TT: TokenType, +{ + /// Returns the ID token provided by the token response. + /// + /// OpenID Connect authorization servers should always return this field, but it is optional + /// to allow for interoperability with authorization servers that only support OAuth2. + fn id_token(&self) -> Option<&IdToken>; +} + +impl TokenResponse + for StandardTokenResponse, TT> +where + AC: AdditionalClaims, + EF: ExtraTokenFields, + GC: GenderClaim, + JE: JweContentEncryptionAlgorithm, + JS: JwsSigningAlgorithm, + JT: JsonWebKeyType, + TT: TokenType, +{ + fn id_token(&self) -> Option<&IdToken> { + self.extra_fields().id_token() + } +} diff --git a/src/types.rs b/src/types.rs deleted file mode 100644 index ae923cb..0000000 --- a/src/types.rs +++ /dev/null @@ -1,1307 +0,0 @@ -use crate::http_utils::{check_content_type, MIME_TYPE_JSON, MIME_TYPE_JWKS}; -use crate::{ - AccessToken, AuthorizationCode, DiscoveryError, HttpRequest, HttpResponse, - SignatureVerificationError, -}; - -use http::header::{HeaderValue, ACCEPT}; -use http::method::Method; -use http::status::StatusCode; -use oauth2::helpers::deserialize_space_delimited_vec; -use rand::{thread_rng, Rng}; -use serde::de::DeserializeOwned; -use serde::{Deserialize, Serialize}; -use serde_with::{serde_as, VecSkipError}; -use thiserror::Error; -use url::Url; - -use std::collections::HashMap; -use std::fmt::{Debug, Display, Error as FormatterError, Formatter}; -use std::future::Future; -use std::hash::Hash; -use std::iter::FromIterator; -use std::marker::PhantomData; -use std::ops::Deref; - -/// A [locale-aware](https://openid.net/specs/openid-connect-core-1_0.html#IndividualClaimsLanguages) -/// claim. -/// -/// This structure associates one more `Option` locales with the corresponding -/// claims values. -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct LocalizedClaim(HashMap, Option); -impl LocalizedClaim { - /// Initialize an empty claim. - pub fn new() -> Self { - Self::default() - } - - /// Returns true if the claim contains a value for the specified locale. - pub fn contains_key(&self, locale: Option<&LanguageTag>) -> bool { - if let Some(l) = locale { - self.0.contains_key(l) - } else { - self.1.is_some() - } - } - - /// Returns the entry for the specified locale or `None` if there is no such entry. - pub fn get(&self, locale: Option<&LanguageTag>) -> Option<&T> { - if let Some(l) = locale { - self.0.get(l) - } else { - self.1.as_ref() - } - } - - /// Returns an iterator over the locales and claim value entries. - pub fn iter(&self) -> impl Iterator, &T)> { - self.1 - .iter() - .map(|value| (None, value)) - .chain(self.0.iter().map(|(locale, value)| (Some(locale), value))) - } - - /// Inserts or updates an entry for the specified locale. - /// - /// Returns the current value associated with the given locale, or `None` if there is no - /// such entry. - pub fn insert(&mut self, locale: Option, value: T) -> Option { - if let Some(l) = locale { - self.0.insert(l, value) - } else { - self.1.replace(value) - } - } - - /// Removes an entry for the specified locale. - /// - /// Returns the current value associated with the given locale, or `None` if there is no - /// such entry. - pub fn remove(&mut self, locale: Option<&LanguageTag>) -> Option { - if let Some(l) = locale { - self.0.remove(l) - } else { - self.1.take() - } - } -} -impl Default for LocalizedClaim { - fn default() -> Self { - Self(HashMap::new(), None) - } -} -impl From for LocalizedClaim { - fn from(default: T) -> Self { - Self(HashMap::new(), Some(default)) - } -} -impl FromIterator<(Option, T)> for LocalizedClaim { - fn from_iter, T)>>(iter: I) -> Self { - let mut temp: HashMap, T> = iter.into_iter().collect(); - let default = temp.remove(&None); - Self( - temp.into_iter() - .filter_map(|(locale, value)| locale.map(|l| (l, value))) - .collect(), - default, - ) - } -} -impl IntoIterator for LocalizedClaim -where - T: 'static, -{ - type Item = as Iterator>::Item; - type IntoIter = LocalizedClaimIterator; - - fn into_iter(self) -> Self::IntoIter { - LocalizedClaimIterator { - inner: Box::new( - self.1.into_iter().map(|value| (None, value)).chain( - self.0 - .into_iter() - .map(|(locale, value)| (Some(locale), value)), - ), - ), - } - } -} - -/// Owned iterator over a LocalizedClaim. -pub struct LocalizedClaimIterator { - inner: Box, T)>>, -} -impl Iterator for LocalizedClaimIterator { - type Item = (Option, T); - fn next(&mut self) -> Option { - self.inner.next() - } -} - -/// Client application type. -pub trait ApplicationType: Debug + DeserializeOwned + Serialize + 'static {} - -/// How the Authorization Server displays the authentication and consent user interface pages to -/// the End-User. -pub trait AuthDisplay: AsRef + Debug + DeserializeOwned + Serialize + 'static {} - -/// Whether the Authorization Server should prompt the End-User for reauthentication and consent. -pub trait AuthPrompt: AsRef + 'static {} - -/// Claim name. -pub trait ClaimName: Debug + DeserializeOwned + Serialize + 'static {} - -/// Claim type (e.g., normal, aggregated, or distributed). -pub trait ClaimType: Debug + DeserializeOwned + Serialize + 'static {} - -/// Client authentication method. -pub trait ClientAuthMethod: Debug + DeserializeOwned + Serialize + 'static {} - -/// Grant type. -pub trait GrantType: Debug + DeserializeOwned + Serialize + 'static {} - -/// Error signing a message. -#[derive(Clone, Debug, Error, PartialEq, Eq)] -#[non_exhaustive] -pub enum SigningError { - /// Failed to sign the message using the given key and parameters. - #[error("Crypto error")] - CryptoError, - /// Unsupported signature algorithm. - #[error("Unsupported signature algorithm: {0}")] - UnsupportedAlg(String), - /// An unexpected error occurred. - #[error("Other error: {0}")] - Other(String), -} - -/// JSON Web Key. -pub trait JsonWebKey: Clone + Debug + DeserializeOwned + Serialize + 'static -where - JS: JwsSigningAlgorithm, - JT: JsonWebKeyType, - JU: JsonWebKeyUse, -{ - /// Returns the key ID, or `None` if no key ID is specified. - fn key_id(&self) -> Option<&JsonWebKeyId>; - - /// Returns the key type (e.g., RSA). - fn key_type(&self) -> &JT; - - /// Returns the allowed key usage (e.g., signing or encryption), or `None` if no usage is - /// specified. - fn key_use(&self) -> Option<&JU>; - - /// Returns the algorithm (e.g. ES512) this key must be used with, or `Unspecified` if - /// no algorithm constraint was given, or unsupported if the algorithm is not for signing. - /// - /// It's not sufficient to tell whether a key can be used for signing, as key use also has to be validated. - #[cfg(feature = "jwk-alg")] - fn signing_alg(&self) -> JsonWebKeyAlgorithm<&JS>; - - /// Initializes a new symmetric key or shared signing secret from the specified raw bytes. - fn new_symmetric(key: Vec) -> Self; - - /// Verifies the given `signature` using the given signature algorithm (`signature_alg`) over - /// the given `message`. - /// - /// Returns `Ok` if the signature is valid, or an `Err` otherwise. - fn verify_signature( - &self, - signature_alg: &JS, - message: &[u8], - signature: &[u8], - ) -> Result<(), SignatureVerificationError>; -} - -/// Encodes a JWK key's alg field compatibility with either signing or encryption operations. -#[derive(Debug)] -pub enum JsonWebKeyAlgorithm { - /// the alg field allows this kind of operation to be performed with this algorithm only - Algorithm(A), - /// there is no alg field - Unspecified, - /// the alg field's algorithm is incompatible with this kind of operation - Unsupported, -} - -/// Private or symmetric key for signing. -pub trait PrivateSigningKey -where - JS: JwsSigningAlgorithm, - JT: JsonWebKeyType, - JU: JsonWebKeyUse, - K: JsonWebKey, -{ - /// Signs the given `message` using the given signature algorithm. - fn sign(&self, signature_alg: &JS, message: &[u8]) -> Result, SigningError>; - - /// Converts this key to a JSON Web Key that can be used for verifying signatures. - fn as_verification_key(&self) -> K; -} - -/// Key type (e.g., RSA). -pub trait JsonWebKeyType: - Clone + Debug + DeserializeOwned + PartialEq + Serialize + 'static -{ -} - -/// Curve type (e.g., P256). -pub trait JsonCurveType: - Clone + Debug + DeserializeOwned + PartialEq + Serialize + 'static -{ -} - -/// Allowed key usage. -pub trait JsonWebKeyUse: Debug + DeserializeOwned + Serialize + 'static { - /// Returns true if the associated key may be used for digital signatures, or false otherwise. - fn allows_signature(&self) -> bool; - - /// Returns true if the associated key may be used for encryption, or false otherwise. - fn allows_encryption(&self) -> bool; -} - -/// JSON Web Encryption (JWE) content encryption algorithm. -pub trait JweContentEncryptionAlgorithm: - Clone + Debug + DeserializeOwned + Serialize + 'static -where - JT: JsonWebKeyType, -{ - /// Returns the type of key required to use this encryption algorithm. - fn key_type(&self) -> Result; -} - -/// JSON Web Encryption (JWE) key management algorithm. -pub trait JweKeyManagementAlgorithm: Debug + DeserializeOwned + Serialize + 'static { - // TODO: add a key_type() method -} - -/// JSON Web Signature (JWS) algorithm. -pub trait JwsSigningAlgorithm: - Clone + Debug + DeserializeOwned + Eq + Hash + PartialEq + Serialize + 'static -where - JT: JsonWebKeyType, -{ - /// Returns the type of key required to use this signature algorithm, or `None` if this - /// algorithm does not require a key. - fn key_type(&self) -> Option; - - /// Returns true if the signature algorithm uses a shared secret (symmetric key). - fn uses_shared_secret(&self) -> bool; - - /// Hashes the given `bytes` using the hash algorithm associated with this signing - /// algorithm, and returns the hashed bytes. - /// - /// If hashing fails or this signing algorithm does not have an associated hash function, an - /// `Err` is returned with a string describing the cause of the error. - fn hash_bytes(&self, bytes: &[u8]) -> Result, String>; - - /// Returns the RS256 algorithm. - /// - /// This is the default algorithm for OpenID Connect ID tokens and must be supported by all - /// implementations. - fn rsa_sha_256() -> Self; -} - -/// Response mode indicating how the OpenID Connect Provider should return the Authorization -/// Response to the Relying Party (client). -pub trait ResponseMode: Debug + DeserializeOwned + Serialize + 'static {} - -/// Response type indicating the desired authorization processing flow, including what -/// parameters are returned from the endpoints used. -pub trait ResponseType: AsRef + Debug + DeserializeOwned + Serialize + 'static { - /// Converts this OpenID Connect response type to an [`oauth2::ResponseType`] used by the - /// underlying [`oauth2`] crate. - fn to_oauth2(&self) -> oauth2::ResponseType; -} - -/// Subject identifier type returned by an OpenID Connect Provider to uniquely identify its users. -pub trait SubjectIdentifierType: Debug + DeserializeOwned + Serialize + 'static {} - -new_type![ - /// Set of authentication methods or procedures that are considered to be equivalent to each - /// other in a particular context. - #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] - AuthenticationContextClass(String) -]; -impl AsRef for AuthenticationContextClass { - fn as_ref(&self) -> &str { - self - } -} - -new_type![ - /// Identifier for an authentication method (e.g., `password` or `totp`). - /// - /// Defining specific AMR identifiers is beyond the scope of the OpenID Connect Core spec. - #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] - AuthenticationMethodReference(String) -]; - -new_type![ - /// Access token hash. - #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] - AccessTokenHash(String) - impl { - /// Initialize a new access token hash from an [`AccessToken`] and signature algorithm. - pub fn from_token( - access_token: &AccessToken, - alg: &JS - ) -> Result - where - JS: JwsSigningAlgorithm, - JT: JsonWebKeyType, - { - alg.hash_bytes(access_token.secret().as_bytes()) - .map(|hash| { - Self::new( - base64::encode_config(&hash[0..hash.len() / 2], base64::URL_SAFE_NO_PAD) - ) - }) - .map_err(SigningError::UnsupportedAlg) - } - } -]; - -new_type![ - /// Country portion of address. - #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] - AddressCountry(String) -]; - -new_type![ - /// Locality portion of address. - #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] - AddressLocality(String) -]; - -new_type![ - /// Postal code portion of address. - #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] - AddressPostalCode(String) -]; - -new_type![ - /// Region portion of address. - #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] - AddressRegion(String) -]; - -new_type![ - /// Audience claim value. - #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] - Audience(String) -]; - -new_type![ - /// Authorization code hash. - #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] - AuthorizationCodeHash(String) - impl { - /// Initialize a new authorization code hash from an [`AuthorizationCode`] and signature - /// algorithm. - pub fn from_code( - code: &AuthorizationCode, - alg: &JS - ) -> Result - where - JS: JwsSigningAlgorithm, - JT: JsonWebKeyType, - { - alg.hash_bytes(code.secret().as_bytes()) - .map(|hash| { - Self::new( - base64::encode_config(&hash[0..hash.len() / 2], base64::URL_SAFE_NO_PAD) - ) - }) - .map_err(SigningError::UnsupportedAlg) - } - } -]; - -new_type![ - #[derive(Deserialize, Hash, Serialize)] - pub(crate) Base64UrlEncodedBytes( - #[serde(with = "serde_base64url_byte_array")] - Vec - ) -]; - -new_type![ - /// OpenID Connect client name. - #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] - ClientName(String) -]; - -new_url_type![ - /// Client configuration endpoint URL. - ClientConfigUrl -]; - -new_url_type![ - /// Client homepage URL. - ClientUrl -]; - -new_type![ - /// Client contact e-mail address. - #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] - ClientContactEmail(String) -]; - -new_url_type![ - /// URL for the [OpenID Connect RP-Initiated Logout 1.0]( - /// https://openid.net/specs/openid-connect-rpinitiated-1_0.html) end session endpoint. - EndSessionUrl -]; - -new_type![ - /// End user's birthday, represented as an - /// [ISO 8601:2004](https://www.iso.org/standard/40874.html) `YYYY-MM-DD` format. - /// - /// The year MAY be `0000`, indicating that it is omitted. To represent only the year, `YYYY` - /// format is allowed. Note that depending on the underlying platform's date related function, - /// providing just year can result in varying month and day, so the implementers need to take - /// this factor into account to correctly process the dates. - #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] - EndUserBirthday(String) -]; - -new_type![ - /// End user's e-mail address. - #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] - EndUserEmail(String) -]; - -new_type![ - /// End user's family name. - #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] - EndUserFamilyName(String) -]; - -new_type![ - /// End user's given name. - #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] - EndUserGivenName(String) -]; - -new_type![ - /// End user's middle name. - #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] - EndUserMiddleName(String) -]; - -new_type![ - /// End user's name. - #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] - EndUserName(String) -]; - -new_type![ - /// End user's nickname. - #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] - EndUserNickname(String) -]; - -new_type![ - /// End user's phone number. - #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] - EndUserPhoneNumber(String) -]; - -new_type![ - /// URL of end user's profile picture. - #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] - EndUserPictureUrl(String) -]; - -new_type![ - /// URL of end user's profile page. - #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] - EndUserProfileUrl(String) -]; - -new_type![ - /// End user's time zone as a string from the - /// [time zone database](https://www.iana.org/time-zones). - #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] - EndUserTimezone(String) -]; - -new_type![ - /// URL of end user's website. - #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] - EndUserWebsiteUrl(String) -]; - -new_type![ - /// End user's username. - #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] - EndUserUsername(String) -]; - -new_type![ - /// Full mailing address, formatted for display or use on a mailing label. - /// - /// This field MAY contain multiple lines, separated by newlines. Newlines can be represented - /// either as a carriage return/line feed pair (`"\r\n"`) or as a single line feed character - /// (`"\n"`). - #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] - FormattedAddress(String) -]; - -new_url_type![ - /// URI using the `https` scheme that a third party can use to initiate a login by the Relying - /// Party. - InitiateLoginUrl -]; - -new_url_type![ - /// URL using the `https` scheme with no query or fragment component that the OP asserts as its - /// Issuer Identifier. - IssuerUrl - impl { - /// Parse a string as a URL, with this URL as the base URL. - /// - /// See [`Url::parse`]. - pub fn join(&self, suffix: &str) -> Result { - if let Some('/') = self.1.chars().next_back() { - Url::parse(&(self.1.clone() + suffix)) - } else { - Url::parse(&(self.1.clone() + "/" + suffix)) - } - } - } -]; - -new_type![ - /// ID of a JSON Web Key. - #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] - JsonWebKeyId(String) -]; - -/// JSON Web Key Set. -#[serde_as] -#[derive(Debug, Deserialize, PartialEq, Eq, Serialize)] -pub struct JsonWebKeySet -where - JS: JwsSigningAlgorithm, - JT: JsonWebKeyType, - JU: JsonWebKeyUse, - K: JsonWebKey, -{ - // FIXME: write a test that ensures duplicate object member names cause an error - // (see https://tools.ietf.org/html/rfc7517#section-5) - #[serde(bound = "K: JsonWebKey")] - // Ignores invalid keys rather than failing. That way, clients can function using the keys that - // they do understand, which is fine if they only ever get JWTs signed with those keys. - #[serde_as(as = "VecSkipError<_>")] - keys: Vec, - #[serde(skip)] - _phantom: PhantomData<(JS, JT, JU)>, -} - -/// Checks whether a JWK key can be used with a given signing algorithm. -pub(crate) fn check_key_compatibility( - key: &K, - signing_algorithm: &JS, -) -> Result<(), &'static str> -where - JS: JwsSigningAlgorithm, - JT: JsonWebKeyType, - JU: JsonWebKeyUse, - K: JsonWebKey, -{ - // if this key isn't suitable for signing - if let Some(use_) = key.key_use() { - if !use_.allows_signature() { - return Err("key usage not permitted for digital signatures"); - } - } - - // if this key doesn't have the right key type - if signing_algorithm.key_type().as_ref() != Some(key.key_type()) { - return Err("key type does not match signature algorithm"); - } - - #[cfg(feature = "jwk-alg")] - match key.signing_alg() { - // if no specific algorithm is mandated, any will do - JsonWebKeyAlgorithm::Unspecified => Ok(()), - JsonWebKeyAlgorithm::Unsupported => Err("key algorithm is not a signing algorithm"), - JsonWebKeyAlgorithm::Algorithm(key_alg) if key_alg == signing_algorithm => Ok(()), - JsonWebKeyAlgorithm::Algorithm(_) => Err("incompatible key algorithm"), - } - - #[cfg(not(feature = "jwk-alg"))] - Ok(()) -} - -impl JsonWebKeySet -where - JS: JwsSigningAlgorithm, - JT: JsonWebKeyType, - JU: JsonWebKeyUse, - K: JsonWebKey, -{ - /// Create a new JSON Web Key Set. - pub fn new(keys: Vec) -> Self { - Self { - keys, - _phantom: PhantomData, - } - } - - /// Return a list of suitable keys, given a key id an signature algorithm - pub(crate) fn filter_keys(&self, key_id: &Option, signature_alg: &JS) -> Vec<&K> { - self.keys() - .iter() - .filter(|key| - // Either the JWT doesn't include a 'kid' (in which case any 'kid' - // is acceptable), or the 'kid' matches the key's ID. - if key_id.is_some() && key_id.as_ref() != key.key_id() { - false - } else { - check_key_compatibility(*key, signature_alg).is_ok() - } - ) - .collect() - } - - /// Fetch a remote JSON Web Key Set from the specified `url` using the given `http_client` - /// (e.g., [`crate::reqwest::http_client`] or [`crate::curl::http_client`]). - pub fn fetch( - url: &JsonWebKeySetUrl, - http_client: HC, - ) -> Result> - where - HC: FnOnce(HttpRequest) -> Result, - RE: std::error::Error + 'static, - { - http_client(Self::fetch_request(url)) - .map_err(DiscoveryError::Request) - .and_then(Self::fetch_response) - } - - /// Fetch a remote JSON Web Key Set from the specified `url` using the given async `http_client` - /// (e.g., [`crate::reqwest::async_http_client`]). - pub async fn fetch_async( - url: &JsonWebKeySetUrl, - http_client: HC, - ) -> Result> - where - F: Future>, - HC: FnOnce(HttpRequest) -> F, - RE: std::error::Error + 'static, - { - http_client(Self::fetch_request(url)) - .await - .map_err(DiscoveryError::Request) - .and_then(Self::fetch_response) - } - - fn fetch_request(url: &JsonWebKeySetUrl) -> HttpRequest { - HttpRequest { - url: url.url().clone(), - method: Method::GET, - headers: vec![(ACCEPT, HeaderValue::from_static(MIME_TYPE_JSON))] - .into_iter() - .collect(), - body: Vec::new(), - } - } - - fn fetch_response(http_response: HttpResponse) -> Result> - where - RE: std::error::Error + 'static, - { - if http_response.status_code != StatusCode::OK { - return Err(DiscoveryError::Response( - http_response.status_code, - http_response.body, - format!("HTTP status code {}", http_response.status_code), - )); - } - - check_content_type(&http_response.headers, MIME_TYPE_JSON) - .or_else(|err| { - check_content_type(&http_response.headers, MIME_TYPE_JWKS).map_err(|_| err) - }) - .map_err(|err_msg| { - DiscoveryError::Response( - http_response.status_code, - http_response.body.clone(), - err_msg, - ) - })?; - - serde_path_to_error::deserialize(&mut serde_json::Deserializer::from_slice( - &http_response.body, - )) - .map_err(DiscoveryError::Parse) - } - - /// Return the keys in this JSON Web Key Set. - pub fn keys(&self) -> &Vec { - &self.keys - } -} -impl Clone for JsonWebKeySet -where - JS: JwsSigningAlgorithm, - JT: JsonWebKeyType, - JU: JsonWebKeyUse, - K: JsonWebKey, -{ - fn clone(&self) -> Self { - Self::new(self.keys.clone()) - } -} -impl Default for JsonWebKeySet -where - JS: JwsSigningAlgorithm, - JT: JsonWebKeyType, - JU: JsonWebKeyUse, - K: JsonWebKey, -{ - fn default() -> Self { - Self::new(Vec::new()) - } -} - -new_url_type![ - /// JSON Web Key Set URL. - JsonWebKeySetUrl -]; - -new_type![ - /// Language tag adhering to RFC 5646 (e.g., `fr` or `fr-CA`). - #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] - LanguageTag(String) -]; -impl AsRef for LanguageTag { - fn as_ref(&self) -> &str { - self - } -} - -new_secret_type![ - /// Hint about the login identifier the End-User might use to log in. - /// - /// The use of this parameter is left to the OpenID Connect Provider's discretion. - #[derive(Clone, Deserialize, Serialize)] - LoginHint(String) -]; - -new_secret_type![ - /// Hint about the logout identifier the End-User might use to log out. - /// - /// The use of this parameter is left to the OpenID Connect Provider's discretion. - #[derive(Clone, Deserialize, Serialize)] - LogoutHint(String) -]; - -new_url_type![ - /// URL that references a logo for the Client application. - LogoUrl -]; - -new_secret_type![ - /// String value used to associate a client session with an ID Token, and to mitigate replay - /// attacks. - #[derive(Clone, Deserialize, Serialize)] - Nonce(String) - impl { - /// Generate a new random, base64-encoded 128-bit nonce. - pub fn new_random() -> Self { - Nonce::new_random_len(16) - } - /// Generate a new random, base64-encoded nonce 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(); - Nonce::new(base64::encode_config(random_bytes, base64::URL_SAFE_NO_PAD)) - } - } -]; -impl PartialEq for Nonce { - fn eq(&self, other: &Self) -> bool { - use subtle::ConstantTimeEq; - self.secret() - .as_bytes() - .ct_eq(other.secret().as_bytes()) - .into() - } -} - -new_url_type![ - /// URL providing the OpenID Connect Provider's data usage policies for client applications. - OpPolicyUrl -]; - -new_url_type![ - /// URL providing the OpenID Connect Provider's Terms of Service. - OpTosUrl -]; - -new_url_type![ - /// URL providing a client application's data usage policy. - PolicyUrl -]; - -new_url_type![ - /// The post logout redirect URL, which should be passed to the end session endpoint - /// of providers implementing [OpenID Connect RP-Initiated Logout 1.0]( - /// https://openid.net/specs/openid-connect-rpinitiated-1_0.html). - PostLogoutRedirectUrl -]; - -new_secret_type![ - /// Access token used by a client application to access the Client Registration endpoint. - #[derive(Clone, Deserialize, Serialize)] - RegistrationAccessToken(String) -]; - -new_url_type![ - /// URL of the Client Registration endpoint. - RegistrationUrl -]; - -new_url_type![ - /// URL used to pass request parameters as JWTs by reference. - RequestUrl -]; - -/// Informs the Authorization Server of the desired authorization processing flow, including what -/// parameters are returned from the endpoints used. -/// -/// See [OAuth 2.0 Multiple Response Type Encoding Practices]( -/// http://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#ResponseTypesAndModes) -/// for further details. -#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)] -pub struct ResponseTypes( - #[serde( - deserialize_with = "deserialize_space_delimited_vec", - serialize_with = "helpers::serialize_space_delimited_vec" - )] - Vec, -); -impl ResponseTypes { - /// Create a new [`ResponseTypes`] to wrap the given [`Vec`]. - pub fn new(s: Vec) -> Self { - ResponseTypes::(s) - } -} -impl Deref for ResponseTypes { - type Target = Vec; - fn deref(&self) -> &Vec { - &self.0 - } -} - -/// Timestamp as seconds since the unix epoch, or optionally an ISO 8601 string. -#[derive(Debug, Deserialize, Serialize)] -#[serde(untagged)] -pub(crate) enum Timestamp { - Seconds(serde_json::Number), - #[cfg(feature = "accept-rfc3339-timestamps")] - Rfc3339(String), -} - -impl Display for Timestamp { - fn fmt(&self, f: &mut Formatter) -> Result<(), FormatterError> { - match self { - Timestamp::Seconds(seconds) => Display::fmt(seconds, f), - #[cfg(feature = "accept-rfc3339-timestamps")] - Timestamp::Rfc3339(iso) => Display::fmt(iso, f), - } - } -} - -/// Newtype around a bool, optionally supporting string values. -#[derive(Debug, Deserialize, Serialize)] -#[serde(transparent)] -pub(crate) struct Boolean( - #[cfg_attr( - feature = "accept-string-booleans", - serde(deserialize_with = "helpers::serde_string_bool::deserialize") - )] - pub bool, -); - -impl Display for Boolean { - fn fmt(&self, f: &mut Formatter) -> Result<(), FormatterError> { - Display::fmt(&self.0, f) - } -} - -new_url_type![ - /// URL for retrieving redirect URIs that should receive identical pairwise subject identifiers. - SectorIdentifierUrl -]; - -new_url_type![ - /// URL for developer documentation for an OpenID Connect Provider. - ServiceDocUrl -]; - -new_type![ - /// A user's street address. - /// - /// Full street address component, which MAY include house number, street name, Post Office Box, - /// and multi-line extended street address information. This field MAY contain multiple lines, - /// separated by newlines. Newlines can be represented either as a carriage return/line feed - /// pair (`\r\n`) or as a single line feed character (`\n`). - #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] - StreetAddress(String) -]; - -new_type![ - /// Locally unique and never reassigned identifier within the Issuer for the End-User, which is - /// intended to be consumed by the client application. - #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] - SubjectIdentifier(String) -]; - -new_url_type![ - /// URL for the relying party's Terms of Service. - ToSUrl -]; - -// FIXME: Add tests -pub(crate) mod helpers { - use crate::types::{LanguageTag, Timestamp}; - - use chrono::{DateTime, TimeZone, Utc}; - use serde::de::DeserializeOwned; - use serde::{Deserialize, Deserializer, Serializer}; - use serde_json::{from_value, Value}; - - pub fn deserialize_string_or_vec<'de, T, D>(deserializer: D) -> Result, D::Error> - where - T: DeserializeOwned, - D: Deserializer<'de>, - { - use serde::de::Error; - - let value: Value = Deserialize::deserialize(deserializer)?; - match from_value::>(value.clone()) { - Ok(val) => Ok(val), - Err(_) => { - let single_val: T = from_value(value).map_err(Error::custom)?; - Ok(vec![single_val]) - } - } - } - - pub fn deserialize_string_or_vec_opt<'de, T, D>( - deserializer: D, - ) -> Result>, D::Error> - where - T: DeserializeOwned, - D: Deserializer<'de>, - { - use serde::de::Error; - - let value: Value = Deserialize::deserialize(deserializer)?; - match from_value::>>(value.clone()) { - Ok(val) => Ok(val), - Err(_) => { - let single_val: T = from_value(value).map_err(Error::custom)?; - Ok(Some(vec![single_val])) - } - } - } - - // Attempt to deserialize the value; if the value is null or an error occurs, return None. - // This is useful when deserializing fields that may mean different things in different - // contexts, and where we would rather ignore the result than fail to deserialize. For example, - // the fields in JWKs are not well defined; extensions could theoretically define their own - // field names that overload field names used by other JWK types. - pub fn deserialize_option_or_none<'de, T, D>(deserializer: D) -> Result, D::Error> - where - T: DeserializeOwned, - D: Deserializer<'de>, - { - let value: Value = Deserialize::deserialize(deserializer)?; - match from_value::>(value) { - Ok(val) => Ok(val), - Err(_) => Ok(None), - } - } - - /// Serde space-delimited string serializer for an `Option>`. - /// - /// This function serializes a string vector into a single space-delimited string. - /// If `string_vec_opt` is `None`, the function serializes it as `None` (e.g., `null` - /// in the case of JSON serialization). - pub fn serialize_space_delimited_vec(vec: &[T], serializer: S) -> Result - where - T: AsRef, - S: Serializer, - { - let space_delimited = vec - .iter() - .map(AsRef::::as_ref) - .collect::>() - .join(" "); - - serializer.serialize_str(&space_delimited) - } - - pub fn split_language_tag_key(key: &str) -> (&str, Option) { - let mut lang_tag_sep = key.splitn(2, '#'); - - // String::splitn(2) always returns at least one element. - let field_name = lang_tag_sep.next().unwrap(); - - let language_tag = lang_tag_sep - .next() - .filter(|language_tag| !language_tag.is_empty()) - .map(|language_tag| LanguageTag::new(language_tag.to_string())); - - (field_name, language_tag) - } - - pub(crate) fn timestamp_to_utc(timestamp: &Timestamp) -> Result, ()> { - match timestamp { - Timestamp::Seconds(seconds) => { - let (secs, nsecs) = if seconds.is_i64() { - (seconds.as_i64().ok_or(())?, 0u32) - } else { - let secs_f64 = seconds.as_f64().ok_or(())?; - let secs = secs_f64.floor(); - ( - secs as i64, - ((secs_f64 - secs) * 1_000_000_000.).floor() as u32, - ) - }; - Utc.timestamp_opt(secs, nsecs).single().ok_or(()) - } - #[cfg(feature = "accept-rfc3339-timestamps")] - Timestamp::Rfc3339(iso) => { - let datetime = DateTime::parse_from_rfc3339(iso).map_err(|_| ())?; - Ok(datetime.into()) - } - } - } - - // The spec is ambiguous about whether seconds should be expressed as integers, or - // whether floating-point values are allowed. For compatibility with a wide range of - // clients, we round down to the nearest second. - pub(crate) fn utc_to_seconds(utc: &DateTime) -> Timestamp { - Timestamp::Seconds(utc.timestamp().into()) - } - - // Some providers return boolean values as strings. Provide support for - // parsing using stdlib. - #[cfg(feature = "accept-string-booleans")] - pub mod serde_string_bool { - use serde::{de, Deserializer}; - - use std::fmt; - - pub fn deserialize<'de, D>(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - struct BooleanLikeVisitor; - - impl<'de> de::Visitor<'de> for BooleanLikeVisitor { - type Value = bool; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("A boolean-like value") - } - - fn visit_bool(self, v: bool) -> Result - where - E: de::Error, - { - Ok(v) - } - - fn visit_str(self, v: &str) -> Result - where - E: de::Error, - { - v.parse().map_err(E::custom) - } - } - deserializer.deserialize_any(BooleanLikeVisitor) - } - } - - pub mod serde_utc_seconds { - use crate::types::Timestamp; - - use chrono::{DateTime, Utc}; - use serde::{Deserialize, Deserializer, Serialize, Serializer}; - - pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> - where - D: Deserializer<'de>, - { - let seconds: Timestamp = Deserialize::deserialize(deserializer)?; - crate::types::helpers::timestamp_to_utc(&seconds).map_err(|_| { - serde::de::Error::custom(format!( - "failed to parse `{}` as UTC datetime (in seconds)", - seconds - )) - }) - } - - pub fn serialize(v: &DateTime, serializer: S) -> Result - where - S: Serializer, - { - crate::types::helpers::utc_to_seconds(v).serialize(serializer) - } - } - - pub mod serde_utc_seconds_opt { - use crate::types::Timestamp; - - use chrono::{DateTime, Utc}; - use serde::{Deserialize, Deserializer, Serialize, Serializer}; - - pub fn deserialize<'de, D>(deserializer: D) -> Result>, D::Error> - where - D: Deserializer<'de>, - { - let seconds: Option = Deserialize::deserialize(deserializer)?; - seconds - .map(|sec| { - crate::types::helpers::timestamp_to_utc(&sec).map_err(|_| { - serde::de::Error::custom(format!( - "failed to parse `{}` as UTC datetime (in seconds)", - sec - )) - }) - }) - .transpose() - } - - pub fn serialize(v: &Option>, serializer: S) -> Result - where - S: Serializer, - { - v.map(|sec| crate::types::helpers::utc_to_seconds(&sec)) - .serialize(serializer) - } - } -} - -mod serde_base64url_byte_array { - use serde::de::Error; - use serde::{Deserialize, Deserializer, Serializer}; - use serde_json::{from_value, Value}; - - pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> - where - D: Deserializer<'de>, - { - let value: Value = Deserialize::deserialize(deserializer)?; - let base64_encoded: String = from_value(value).map_err(D::Error::custom)?; - - base64::decode_config(&base64_encoded, crate::core::base64_url_safe_no_pad()).map_err( - |err| { - D::Error::custom(format!( - "invalid base64url encoding `{}`: {:?}", - base64_encoded, err - )) - }, - ) - } - - pub fn serialize(v: &[u8], serializer: S) -> Result - where - S: Serializer, - { - let base64_encoded = base64::encode_config(v, base64::URL_SAFE_NO_PAD); - serializer.serialize_str(&base64_encoded) - } -} - -#[cfg(test)] -mod tests { - use crate::IssuerUrl; - - #[test] - fn test_issuer_url_append() { - assert_eq!( - "http://example.com/.well-known/openid-configuration", - IssuerUrl::new("http://example.com".to_string()) - .unwrap() - .join(".well-known/openid-configuration") - .unwrap() - .to_string() - ); - assert_eq!( - "http://example.com/.well-known/openid-configuration", - IssuerUrl::new("http://example.com/".to_string()) - .unwrap() - .join(".well-known/openid-configuration") - .unwrap() - .to_string() - ); - assert_eq!( - "http://example.com/x/.well-known/openid-configuration", - IssuerUrl::new("http://example.com/x".to_string()) - .unwrap() - .join(".well-known/openid-configuration") - .unwrap() - .to_string() - ); - assert_eq!( - "http://example.com/x/.well-known/openid-configuration", - IssuerUrl::new("http://example.com/x/".to_string()) - .unwrap() - .join(".well-known/openid-configuration") - .unwrap() - .to_string() - ); - } - - #[test] - fn test_url_serialize() { - let issuer_url = - IssuerUrl::new("http://example.com/.well-known/openid-configuration".to_string()) - .unwrap(); - let serialized_url = serde_json::to_string(&issuer_url).unwrap(); - - assert_eq!( - "\"http://example.com/.well-known/openid-configuration\"", - serialized_url - ); - - let deserialized_url = serde_json::from_str(&serialized_url).unwrap(); - assert_eq!(issuer_url, deserialized_url); - - assert_eq!( - serde_json::to_string(&IssuerUrl::new("http://example.com".to_string()).unwrap()) - .unwrap(), - "\"http://example.com\"", - ); - } - - #[cfg(feature = "accept-string-booleans")] - #[test] - fn test_string_bool_parse() { - use crate::types::Boolean; - - fn test_case(input: &str, expect: bool) { - let value: Boolean = serde_json::from_str(input).unwrap(); - assert_eq!(value.0, expect); - } - test_case("true", true); - test_case("false", false); - test_case("\"true\"", true); - test_case("\"false\"", false); - assert!(serde_json::from_str::("\"maybe\"").is_err()); - } -} diff --git a/src/types/jwk.rs b/src/types/jwk.rs new file mode 100644 index 0000000..4fedb28 --- /dev/null +++ b/src/types/jwk.rs @@ -0,0 +1,141 @@ +use crate::{SignatureVerificationError, SigningError}; + +use serde::de::DeserializeOwned; +use serde::{Deserialize, Serialize}; + +use std::fmt::Debug; +use std::hash::Hash; + +new_type![ + /// ID of a JSON Web Key. + #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] + JsonWebKeyId(String) +]; + +/// JSON Web Key. +pub trait JsonWebKey: Clone + Debug + DeserializeOwned + Serialize + 'static +where + JS: JwsSigningAlgorithm, + JT: JsonWebKeyType, + JU: JsonWebKeyUse, +{ + /// Returns the key ID, or `None` if no key ID is specified. + fn key_id(&self) -> Option<&JsonWebKeyId>; + + /// Returns the key type (e.g., RSA). + fn key_type(&self) -> &JT; + + /// Returns the allowed key usage (e.g., signing or encryption), or `None` if no usage is + /// specified. + fn key_use(&self) -> Option<&JU>; + + /// Returns the algorithm (e.g. ES512) this key must be used with, or `Unspecified` if + /// no algorithm constraint was given, or unsupported if the algorithm is not for signing. + /// + /// It's not sufficient to tell whether a key can be used for signing, as key use also has to be validated. + #[cfg(feature = "jwk-alg")] + fn signing_alg(&self) -> JsonWebKeyAlgorithm<&JS>; + + /// Initializes a new symmetric key or shared signing secret from the specified raw bytes. + fn new_symmetric(key: Vec) -> Self; + + /// Verifies the given `signature` using the given signature algorithm (`signature_alg`) over + /// the given `message`. + /// + /// Returns `Ok` if the signature is valid, or an `Err` otherwise. + fn verify_signature( + &self, + signature_alg: &JS, + message: &[u8], + signature: &[u8], + ) -> Result<(), SignatureVerificationError>; +} + +/// Encodes a JWK key's alg field compatibility with either signing or encryption operations. +#[derive(Debug)] +pub enum JsonWebKeyAlgorithm { + /// the alg field allows this kind of operation to be performed with this algorithm only + Algorithm(A), + /// there is no alg field + Unspecified, + /// the alg field's algorithm is incompatible with this kind of operation + Unsupported, +} + +/// Private or symmetric key for signing. +pub trait PrivateSigningKey +where + JS: JwsSigningAlgorithm, + JT: JsonWebKeyType, + JU: JsonWebKeyUse, + K: JsonWebKey, +{ + /// Signs the given `message` using the given signature algorithm. + fn sign(&self, signature_alg: &JS, message: &[u8]) -> Result, SigningError>; + + /// Converts this key to a JSON Web Key that can be used for verifying signatures. + fn as_verification_key(&self) -> K; +} + +/// Key type (e.g., RSA). +pub trait JsonWebKeyType: + Clone + Debug + DeserializeOwned + PartialEq + Serialize + 'static +{ +} + +/// Curve type (e.g., P256). +pub trait JsonCurveType: + Clone + Debug + DeserializeOwned + PartialEq + Serialize + 'static +{ +} + +/// Allowed key usage. +pub trait JsonWebKeyUse: Debug + DeserializeOwned + Serialize + 'static { + /// Returns true if the associated key may be used for digital signatures, or false otherwise. + fn allows_signature(&self) -> bool; + + /// Returns true if the associated key may be used for encryption, or false otherwise. + fn allows_encryption(&self) -> bool; +} + +/// JSON Web Encryption (JWE) content encryption algorithm. +pub trait JweContentEncryptionAlgorithm: + Clone + Debug + DeserializeOwned + Serialize + 'static +where + JT: JsonWebKeyType, +{ + /// Returns the type of key required to use this encryption algorithm. + fn key_type(&self) -> Result; +} + +/// JSON Web Encryption (JWE) key management algorithm. +pub trait JweKeyManagementAlgorithm: Debug + DeserializeOwned + Serialize + 'static { + // TODO: add a key_type() method +} + +/// JSON Web Signature (JWS) algorithm. +pub trait JwsSigningAlgorithm: + Clone + Debug + DeserializeOwned + Eq + Hash + PartialEq + Serialize + 'static +where + JT: JsonWebKeyType, +{ + /// Returns the type of key required to use this signature algorithm, or `None` if this + /// algorithm does not require a key. + fn key_type(&self) -> Option; + + /// Returns true if the signature algorithm uses a shared secret (symmetric key). + fn uses_shared_secret(&self) -> bool; + + /// Hashes the given `bytes` using the hash algorithm associated with this signing + /// algorithm, and returns the hashed bytes. + /// + /// If hashing fails or this signing algorithm does not have an associated hash function, an + /// `Err` is returned with a string describing the cause of the error. + fn hash_bytes(&self, bytes: &[u8]) -> Result, String>; + + /// Returns the RS256 algorithm. + /// + /// This is the default algorithm for OpenID Connect ID tokens and must be supported by all + /// implementations. + fn rsa_sha_256() -> Self; +} diff --git a/src/types/jwks.rs b/src/types/jwks.rs new file mode 100644 index 0000000..8647f19 --- /dev/null +++ b/src/types/jwks.rs @@ -0,0 +1,207 @@ +use crate::http_utils::{check_content_type, MIME_TYPE_JSON, MIME_TYPE_JWKS}; +use crate::types::jwk::{ + JsonWebKey, JsonWebKeyId, JsonWebKeyType, JsonWebKeyUse, JwsSigningAlgorithm, +}; +use crate::{DiscoveryError, HttpRequest, HttpResponse}; + +use http::header::ACCEPT; +use http::{HeaderValue, Method, StatusCode}; +use serde::{Deserialize, Serialize}; +use serde_with::{serde_as, VecSkipError}; + +use std::future::Future; +use std::marker::PhantomData; + +new_url_type![ + /// JSON Web Key Set URL. + JsonWebKeySetUrl +]; + +/// JSON Web Key Set. +#[serde_as] +#[derive(Debug, Deserialize, PartialEq, Eq, Serialize)] +pub struct JsonWebKeySet +where + JS: JwsSigningAlgorithm, + JT: JsonWebKeyType, + JU: JsonWebKeyUse, + K: JsonWebKey, +{ + // FIXME: write a test that ensures duplicate object member names cause an error + // (see https://tools.ietf.org/html/rfc7517#section-5) + #[serde(bound = "K: JsonWebKey")] + // Ignores invalid keys rather than failing. That way, clients can function using the keys that + // they do understand, which is fine if they only ever get JWTs signed with those keys. + #[serde_as(as = "VecSkipError<_>")] + keys: Vec, + #[serde(skip)] + _phantom: PhantomData<(JS, JT, JU)>, +} + +/// Checks whether a JWK key can be used with a given signing algorithm. +pub(crate) fn check_key_compatibility( + key: &K, + signing_algorithm: &JS, +) -> Result<(), &'static str> +where + JS: JwsSigningAlgorithm, + JT: JsonWebKeyType, + JU: JsonWebKeyUse, + K: JsonWebKey, +{ + // if this key isn't suitable for signing + if let Some(use_) = key.key_use() { + if !use_.allows_signature() { + return Err("key usage not permitted for digital signatures"); + } + } + + // if this key doesn't have the right key type + if signing_algorithm.key_type().as_ref() != Some(key.key_type()) { + return Err("key type does not match signature algorithm"); + } + + #[cfg(feature = "jwk-alg")] + match key.signing_alg() { + // if no specific algorithm is mandated, any will do + crate::JsonWebKeyAlgorithm::Unspecified => Ok(()), + crate::JsonWebKeyAlgorithm::Unsupported => Err("key algorithm is not a signing algorithm"), + crate::JsonWebKeyAlgorithm::Algorithm(key_alg) if key_alg == signing_algorithm => Ok(()), + crate::JsonWebKeyAlgorithm::Algorithm(_) => Err("incompatible key algorithm"), + } + + #[cfg(not(feature = "jwk-alg"))] + Ok(()) +} + +impl JsonWebKeySet +where + JS: JwsSigningAlgorithm, + JT: JsonWebKeyType, + JU: JsonWebKeyUse, + K: JsonWebKey, +{ + /// Create a new JSON Web Key Set. + pub fn new(keys: Vec) -> Self { + Self { + keys, + _phantom: PhantomData, + } + } + + /// Return a list of suitable keys, given a key id an signature algorithm + pub(crate) fn filter_keys(&self, key_id: &Option, signature_alg: &JS) -> Vec<&K> { + self.keys() + .iter() + .filter(|key| + // Either the JWT doesn't include a 'kid' (in which case any 'kid' + // is acceptable), or the 'kid' matches the key's ID. + if key_id.is_some() && key_id.as_ref() != key.key_id() { + false + } else { + check_key_compatibility(*key, signature_alg).is_ok() + } + ) + .collect() + } + + /// Fetch a remote JSON Web Key Set from the specified `url` using the given `http_client` + /// (e.g., [`crate::reqwest::http_client`] or [`crate::curl::http_client`]). + pub fn fetch( + url: &JsonWebKeySetUrl, + http_client: HC, + ) -> Result> + where + HC: FnOnce(HttpRequest) -> Result, + RE: std::error::Error + 'static, + { + http_client(Self::fetch_request(url)) + .map_err(DiscoveryError::Request) + .and_then(Self::fetch_response) + } + + /// Fetch a remote JSON Web Key Set from the specified `url` using the given async `http_client` + /// (e.g., [`crate::reqwest::async_http_client`]). + pub async fn fetch_async( + url: &JsonWebKeySetUrl, + http_client: HC, + ) -> Result> + where + F: Future>, + HC: FnOnce(HttpRequest) -> F, + RE: std::error::Error + 'static, + { + http_client(Self::fetch_request(url)) + .await + .map_err(DiscoveryError::Request) + .and_then(Self::fetch_response) + } + + fn fetch_request(url: &JsonWebKeySetUrl) -> HttpRequest { + HttpRequest { + url: url.url().clone(), + method: Method::GET, + headers: vec![(ACCEPT, HeaderValue::from_static(MIME_TYPE_JSON))] + .into_iter() + .collect(), + body: Vec::new(), + } + } + + fn fetch_response(http_response: HttpResponse) -> Result> + where + RE: std::error::Error + 'static, + { + if http_response.status_code != StatusCode::OK { + return Err(DiscoveryError::Response( + http_response.status_code, + http_response.body, + format!("HTTP status code {}", http_response.status_code), + )); + } + + check_content_type(&http_response.headers, MIME_TYPE_JSON) + .or_else(|err| { + check_content_type(&http_response.headers, MIME_TYPE_JWKS).map_err(|_| err) + }) + .map_err(|err_msg| { + DiscoveryError::Response( + http_response.status_code, + http_response.body.clone(), + err_msg, + ) + })?; + + serde_path_to_error::deserialize(&mut serde_json::Deserializer::from_slice( + &http_response.body, + )) + .map_err(DiscoveryError::Parse) + } + + /// Return the keys in this JSON Web Key Set. + pub fn keys(&self) -> &Vec { + &self.keys + } +} +impl Clone for JsonWebKeySet +where + JS: JwsSigningAlgorithm, + JT: JsonWebKeyType, + JU: JsonWebKeyUse, + K: JsonWebKey, +{ + fn clone(&self) -> Self { + Self::new(self.keys.clone()) + } +} +impl Default for JsonWebKeySet +where + JS: JwsSigningAlgorithm, + JT: JsonWebKeyType, + JU: JsonWebKeyUse, + K: JsonWebKey, +{ + fn default() -> Self { + Self::new(Vec::new()) + } +} diff --git a/src/types/localized.rs b/src/types/localized.rs new file mode 100644 index 0000000..ae51daa --- /dev/null +++ b/src/types/localized.rs @@ -0,0 +1,144 @@ +use serde::{Deserialize, Serialize}; + +use std::collections::HashMap; + +new_type![ + /// Language tag adhering to RFC 5646 (e.g., `fr` or `fr-CA`). + #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] + LanguageTag(String) +]; +impl AsRef for LanguageTag { + fn as_ref(&self) -> &str { + self + } +} + +pub(crate) fn split_language_tag_key(key: &str) -> (&str, Option) { + let mut lang_tag_sep = key.splitn(2, '#'); + + // String::splitn(2) always returns at least one element. + let field_name = lang_tag_sep.next().unwrap(); + + let language_tag = lang_tag_sep + .next() + .filter(|language_tag| !language_tag.is_empty()) + .map(|language_tag| LanguageTag::new(language_tag.to_string())); + + (field_name, language_tag) +} + +/// A [locale-aware](https://openid.net/specs/openid-connect-core-1_0.html#IndividualClaimsLanguages) +/// claim. +/// +/// This structure associates one more `Option` locales with the corresponding +/// claims values. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct LocalizedClaim(HashMap, Option); +impl LocalizedClaim { + /// Initialize an empty claim. + pub fn new() -> Self { + Self::default() + } + + /// Returns true if the claim contains a value for the specified locale. + pub fn contains_key(&self, locale: Option<&LanguageTag>) -> bool { + if let Some(l) = locale { + self.0.contains_key(l) + } else { + self.1.is_some() + } + } + + /// Returns the entry for the specified locale or `None` if there is no such entry. + pub fn get(&self, locale: Option<&LanguageTag>) -> Option<&T> { + if let Some(l) = locale { + self.0.get(l) + } else { + self.1.as_ref() + } + } + + /// Returns an iterator over the locales and claim value entries. + pub fn iter(&self) -> impl Iterator, &T)> { + self.1 + .iter() + .map(|value| (None, value)) + .chain(self.0.iter().map(|(locale, value)| (Some(locale), value))) + } + + /// Inserts or updates an entry for the specified locale. + /// + /// Returns the current value associated with the given locale, or `None` if there is no + /// such entry. + pub fn insert(&mut self, locale: Option, value: T) -> Option { + if let Some(l) = locale { + self.0.insert(l, value) + } else { + self.1.replace(value) + } + } + + /// Removes an entry for the specified locale. + /// + /// Returns the current value associated with the given locale, or `None` if there is no + /// such entry. + pub fn remove(&mut self, locale: Option<&LanguageTag>) -> Option { + if let Some(l) = locale { + self.0.remove(l) + } else { + self.1.take() + } + } +} +impl Default for LocalizedClaim { + fn default() -> Self { + Self(HashMap::new(), None) + } +} +impl From for LocalizedClaim { + fn from(default: T) -> Self { + Self(HashMap::new(), Some(default)) + } +} +impl FromIterator<(Option, T)> for LocalizedClaim { + fn from_iter, T)>>(iter: I) -> Self { + let mut temp: HashMap, T> = iter.into_iter().collect(); + let default = temp.remove(&None); + Self( + temp.into_iter() + .filter_map(|(locale, value)| locale.map(|l| (l, value))) + .collect(), + default, + ) + } +} +impl IntoIterator for LocalizedClaim +where + T: 'static, +{ + type Item = as Iterator>::Item; + type IntoIter = LocalizedClaimIterator; + + fn into_iter(self) -> Self::IntoIter { + LocalizedClaimIterator { + inner: Box::new( + self.1.into_iter().map(|value| (None, value)).chain( + self.0 + .into_iter() + .map(|(locale, value)| (Some(locale), value)), + ), + ), + } + } +} + +/// Owned iterator over a LocalizedClaim. +pub struct LocalizedClaimIterator { + inner: Box, T)>>, +} +impl Iterator for LocalizedClaimIterator { + type Item = (Option, T); + fn next(&mut self) -> Option { + self.inner.next() + } +} diff --git a/src/types/mod.rs b/src/types/mod.rs new file mode 100644 index 0000000..ef829cb --- /dev/null +++ b/src/types/mod.rs @@ -0,0 +1,470 @@ +use crate::types::jwk::{JsonWebKeyType, JwsSigningAlgorithm}; +use crate::{AccessToken, AuthorizationCode}; + +use oauth2::helpers::deserialize_space_delimited_vec; +use rand::{thread_rng, Rng}; +use serde::de::DeserializeOwned; +use serde::{Deserialize, Serialize}; +use thiserror::Error; +use url::Url; + +use std::fmt::Debug; +use std::hash::Hash; +use std::ops::Deref; + +pub(crate) mod jwk; +pub(crate) mod jwks; +pub(crate) mod localized; + +#[cfg(test)] +mod tests; + +/// Client application type. +pub trait ApplicationType: Debug + DeserializeOwned + Serialize + 'static {} + +/// How the Authorization Server displays the authentication and consent user interface pages to +/// the End-User. +pub trait AuthDisplay: AsRef + Debug + DeserializeOwned + Serialize + 'static {} + +/// Whether the Authorization Server should prompt the End-User for reauthentication and consent. +pub trait AuthPrompt: AsRef + 'static {} + +/// Claim name. +pub trait ClaimName: Debug + DeserializeOwned + Serialize + 'static {} + +/// Claim type (e.g., normal, aggregated, or distributed). +pub trait ClaimType: Debug + DeserializeOwned + Serialize + 'static {} + +/// Client authentication method. +pub trait ClientAuthMethod: Debug + DeserializeOwned + Serialize + 'static {} + +/// Grant type. +pub trait GrantType: Debug + DeserializeOwned + Serialize + 'static {} + +/// Error signing a message. +#[derive(Clone, Debug, Error, PartialEq, Eq)] +#[non_exhaustive] +pub enum SigningError { + /// Failed to sign the message using the given key and parameters. + #[error("Crypto error")] + CryptoError, + /// Unsupported signature algorithm. + #[error("Unsupported signature algorithm: {0}")] + UnsupportedAlg(String), + /// An unexpected error occurred. + #[error("Other error: {0}")] + Other(String), +} + +/// Response mode indicating how the OpenID Connect Provider should return the Authorization +/// Response to the Relying Party (client). +pub trait ResponseMode: Debug + DeserializeOwned + Serialize + 'static {} + +/// Response type indicating the desired authorization processing flow, including what +/// parameters are returned from the endpoints used. +pub trait ResponseType: AsRef + Debug + DeserializeOwned + Serialize + 'static { + /// Converts this OpenID Connect response type to an [`oauth2::ResponseType`] used by the + /// underlying [`oauth2`] crate. + fn to_oauth2(&self) -> oauth2::ResponseType; +} + +/// Subject identifier type returned by an OpenID Connect Provider to uniquely identify its users. +pub trait SubjectIdentifierType: Debug + DeserializeOwned + Serialize + 'static {} + +new_type![ + /// Set of authentication methods or procedures that are considered to be equivalent to each + /// other in a particular context. + #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] + AuthenticationContextClass(String) +]; +impl AsRef for AuthenticationContextClass { + fn as_ref(&self) -> &str { + self + } +} + +new_type![ + /// Identifier for an authentication method (e.g., `password` or `totp`). + /// + /// Defining specific AMR identifiers is beyond the scope of the OpenID Connect Core spec. + #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] + AuthenticationMethodReference(String) +]; + +new_type![ + /// Access token hash. + #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] + AccessTokenHash(String) + impl { + /// Initialize a new access token hash from an [`AccessToken`] and signature algorithm. + pub fn from_token( + access_token: &AccessToken, + alg: &JS + ) -> Result + where + JS: JwsSigningAlgorithm, + JT: JsonWebKeyType, + { + alg.hash_bytes(access_token.secret().as_bytes()) + .map(|hash| { + Self::new( + base64::encode_config(&hash[0..hash.len() / 2], base64::URL_SAFE_NO_PAD) + ) + }) + .map_err(SigningError::UnsupportedAlg) + } + } +]; + +new_type![ + /// Country portion of address. + #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] + AddressCountry(String) +]; + +new_type![ + /// Locality portion of address. + #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] + AddressLocality(String) +]; + +new_type![ + /// Postal code portion of address. + #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] + AddressPostalCode(String) +]; + +new_type![ + /// Region portion of address. + #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] + AddressRegion(String) +]; + +new_type![ + /// Audience claim value. + #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] + Audience(String) +]; + +new_type![ + /// Authorization code hash. + #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] + AuthorizationCodeHash(String) + impl { + /// Initialize a new authorization code hash from an [`AuthorizationCode`] and signature + /// algorithm. + pub fn from_code( + code: &AuthorizationCode, + alg: &JS + ) -> Result + where + JS: JwsSigningAlgorithm, + JT: JsonWebKeyType, + { + alg.hash_bytes(code.secret().as_bytes()) + .map(|hash| { + Self::new( + base64::encode_config(&hash[0..hash.len() / 2], base64::URL_SAFE_NO_PAD) + ) + }) + .map_err(SigningError::UnsupportedAlg) + } + } +]; + +new_type![ + /// OpenID Connect client name. + #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] + ClientName(String) +]; + +new_url_type![ + /// Client configuration endpoint URL. + ClientConfigUrl +]; + +new_url_type![ + /// Client homepage URL. + ClientUrl +]; + +new_type![ + /// Client contact e-mail address. + #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] + ClientContactEmail(String) +]; + +new_url_type![ + /// URL for the [OpenID Connect RP-Initiated Logout 1.0]( + /// https://openid.net/specs/openid-connect-rpinitiated-1_0.html) end session endpoint. + EndSessionUrl +]; + +new_type![ + /// End user's birthday, represented as an + /// [ISO 8601:2004](https://www.iso.org/standard/40874.html) `YYYY-MM-DD` format. + /// + /// The year MAY be `0000`, indicating that it is omitted. To represent only the year, `YYYY` + /// format is allowed. Note that depending on the underlying platform's date related function, + /// providing just year can result in varying month and day, so the implementers need to take + /// this factor into account to correctly process the dates. + #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] + EndUserBirthday(String) +]; + +new_type![ + /// End user's e-mail address. + #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] + EndUserEmail(String) +]; + +new_type![ + /// End user's family name. + #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] + EndUserFamilyName(String) +]; + +new_type![ + /// End user's given name. + #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] + EndUserGivenName(String) +]; + +new_type![ + /// End user's middle name. + #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] + EndUserMiddleName(String) +]; + +new_type![ + /// End user's name. + #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] + EndUserName(String) +]; + +new_type![ + /// End user's nickname. + #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] + EndUserNickname(String) +]; + +new_type![ + /// End user's phone number. + #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] + EndUserPhoneNumber(String) +]; + +new_type![ + /// URL of end user's profile picture. + #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] + EndUserPictureUrl(String) +]; + +new_type![ + /// URL of end user's profile page. + #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] + EndUserProfileUrl(String) +]; + +new_type![ + /// End user's time zone as a string from the + /// [time zone database](https://www.iana.org/time-zones). + #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] + EndUserTimezone(String) +]; + +new_type![ + /// URL of end user's website. + #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] + EndUserWebsiteUrl(String) +]; + +new_type![ + /// End user's username. + #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] + EndUserUsername(String) +]; + +new_type![ + /// Full mailing address, formatted for display or use on a mailing label. + /// + /// This field MAY contain multiple lines, separated by newlines. Newlines can be represented + /// either as a carriage return/line feed pair (`"\r\n"`) or as a single line feed character + /// (`"\n"`). + #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] + FormattedAddress(String) +]; + +new_url_type![ + /// URI using the `https` scheme that a third party can use to initiate a login by the Relying + /// Party. + InitiateLoginUrl +]; + +new_url_type![ + /// URL using the `https` scheme with no query or fragment component that the OP asserts as its + /// Issuer Identifier. + IssuerUrl + impl { + /// Parse a string as a URL, with this URL as the base URL. + /// + /// See [`Url::parse`]. + pub fn join(&self, suffix: &str) -> Result { + if let Some('/') = self.1.chars().next_back() { + Url::parse(&(self.1.clone() + suffix)) + } else { + Url::parse(&(self.1.clone() + "/" + suffix)) + } + } + } +]; + +new_secret_type![ + /// Hint about the login identifier the End-User might use to log in. + /// + /// The use of this parameter is left to the OpenID Connect Provider's discretion. + #[derive(Clone, Deserialize, Serialize)] + LoginHint(String) +]; + +new_secret_type![ + /// Hint about the logout identifier the End-User might use to log out. + /// + /// The use of this parameter is left to the OpenID Connect Provider's discretion. + #[derive(Clone, Deserialize, Serialize)] + LogoutHint(String) +]; + +new_url_type![ + /// URL that references a logo for the Client application. + LogoUrl +]; + +new_secret_type![ + /// String value used to associate a client session with an ID Token, and to mitigate replay + /// attacks. + #[derive(Clone, Deserialize, Serialize)] + Nonce(String) + impl { + /// Generate a new random, base64-encoded 128-bit nonce. + pub fn new_random() -> Self { + Nonce::new_random_len(16) + } + /// Generate a new random, base64-encoded nonce 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(); + Nonce::new(base64::encode_config(random_bytes, base64::URL_SAFE_NO_PAD)) + } + } +]; +impl PartialEq for Nonce { + fn eq(&self, other: &Self) -> bool { + use subtle::ConstantTimeEq; + self.secret() + .as_bytes() + .ct_eq(other.secret().as_bytes()) + .into() + } +} + +new_url_type![ + /// URL providing the OpenID Connect Provider's data usage policies for client applications. + OpPolicyUrl +]; + +new_url_type![ + /// URL providing the OpenID Connect Provider's Terms of Service. + OpTosUrl +]; + +new_url_type![ + /// URL providing a client application's data usage policy. + PolicyUrl +]; + +new_url_type![ + /// The post logout redirect URL, which should be passed to the end session endpoint + /// of providers implementing [OpenID Connect RP-Initiated Logout 1.0]( + /// https://openid.net/specs/openid-connect-rpinitiated-1_0.html). + PostLogoutRedirectUrl +]; + +new_secret_type![ + /// Access token used by a client application to access the Client Registration endpoint. + #[derive(Clone, Deserialize, Serialize)] + RegistrationAccessToken(String) +]; + +new_url_type![ + /// URL of the Client Registration endpoint. + RegistrationUrl +]; + +new_url_type![ + /// URL used to pass request parameters as JWTs by reference. + RequestUrl +]; + +/// Informs the Authorization Server of the desired authorization processing flow, including what +/// parameters are returned from the endpoints used. +/// +/// See [OAuth 2.0 Multiple Response Type Encoding Practices]( +/// http://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#ResponseTypesAndModes) +/// for further details. +#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)] +pub struct ResponseTypes( + #[serde( + deserialize_with = "deserialize_space_delimited_vec", + serialize_with = "crate::helpers::serialize_space_delimited_vec" + )] + Vec, +); +impl ResponseTypes { + /// Create a new [`ResponseTypes`] to wrap the given [`Vec`]. + pub fn new(s: Vec) -> Self { + ResponseTypes::(s) + } +} +impl Deref for ResponseTypes { + type Target = Vec; + fn deref(&self) -> &Vec { + &self.0 + } +} + +new_url_type![ + /// URL for retrieving redirect URIs that should receive identical pairwise subject identifiers. + SectorIdentifierUrl +]; + +new_url_type![ + /// URL for developer documentation for an OpenID Connect Provider. + ServiceDocUrl +]; + +new_type![ + /// A user's street address. + /// + /// Full street address component, which MAY include house number, street name, Post Office Box, + /// and multi-line extended street address information. This field MAY contain multiple lines, + /// separated by newlines. Newlines can be represented either as a carriage return/line feed + /// pair (`\r\n`) or as a single line feed character (`\n`). + #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] + StreetAddress(String) +]; + +new_type![ + /// Locally unique and never reassigned identifier within the Issuer for the End-User, which is + /// intended to be consumed by the client application. + #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] + SubjectIdentifier(String) +]; + +new_url_type![ + /// URL for the relying party's Terms of Service. + ToSUrl +]; diff --git a/src/types/tests.rs b/src/types/tests.rs new file mode 100644 index 0000000..6e93b63 --- /dev/null +++ b/src/types/tests.rs @@ -0,0 +1,73 @@ +use crate::IssuerUrl; + +#[test] +fn test_issuer_url_append() { + assert_eq!( + "http://example.com/.well-known/openid-configuration", + IssuerUrl::new("http://example.com".to_string()) + .unwrap() + .join(".well-known/openid-configuration") + .unwrap() + .to_string() + ); + assert_eq!( + "http://example.com/.well-known/openid-configuration", + IssuerUrl::new("http://example.com/".to_string()) + .unwrap() + .join(".well-known/openid-configuration") + .unwrap() + .to_string() + ); + assert_eq!( + "http://example.com/x/.well-known/openid-configuration", + IssuerUrl::new("http://example.com/x".to_string()) + .unwrap() + .join(".well-known/openid-configuration") + .unwrap() + .to_string() + ); + assert_eq!( + "http://example.com/x/.well-known/openid-configuration", + IssuerUrl::new("http://example.com/x/".to_string()) + .unwrap() + .join(".well-known/openid-configuration") + .unwrap() + .to_string() + ); +} + +#[test] +fn test_url_serialize() { + let issuer_url = + IssuerUrl::new("http://example.com/.well-known/openid-configuration".to_string()).unwrap(); + let serialized_url = serde_json::to_string(&issuer_url).unwrap(); + + assert_eq!( + "\"http://example.com/.well-known/openid-configuration\"", + serialized_url + ); + + let deserialized_url = serde_json::from_str(&serialized_url).unwrap(); + assert_eq!(issuer_url, deserialized_url); + + assert_eq!( + serde_json::to_string(&IssuerUrl::new("http://example.com".to_string()).unwrap()).unwrap(), + "\"http://example.com\"", + ); +} + +#[cfg(feature = "accept-string-booleans")] +#[test] +fn test_string_bool_parse() { + use crate::helpers::Boolean; + + fn test_case(input: &str, expect: bool) { + let value: Boolean = serde_json::from_str(input).unwrap(); + assert_eq!(value.0, expect); + } + test_case("true", true); + test_case("false", false); + test_case("\"true\"", true); + test_case("\"false\"", false); + assert!(serde_json::from_str::("\"maybe\"").is_err()); +} diff --git a/src/user_info.rs b/src/user_info.rs index ca577d5..bd8925a 100644 --- a/src/user_info.rs +++ b/src/user_info.rs @@ -1,8 +1,6 @@ -use crate::helpers::FilteredFlatten; +use crate::helpers::{deserialize_string_or_vec_opt, FilteredFlatten}; use crate::http_utils::{auth_bearer, content_type_has_essence, MIME_TYPE_JSON, MIME_TYPE_JWT}; use crate::jwt::{JsonWebTokenError, JsonWebTokenJsonPayloadSerde}; -use crate::types::helpers::deserialize_string_or_vec_opt; -use crate::types::LocalizedClaim; use crate::verification::UserInfoVerifier; use crate::{ AccessToken, AdditionalClaims, AddressClaim, Audience, AudiencesClaim, ClaimsVerificationError, @@ -10,8 +8,8 @@ use crate::{ EndUserName, EndUserNickname, EndUserPhoneNumber, EndUserPictureUrl, EndUserProfileUrl, EndUserTimezone, EndUserUsername, EndUserWebsiteUrl, GenderClaim, HttpRequest, HttpResponse, IssuerClaim, IssuerUrl, JsonWebKey, JsonWebKeyType, JsonWebKeyUse, JsonWebToken, - JweContentEncryptionAlgorithm, JwsSigningAlgorithm, LanguageTag, PrivateSigningKey, - StandardClaims, SubjectIdentifier, + JweContentEncryptionAlgorithm, JwsSigningAlgorithm, LanguageTag, LocalizedClaim, + PrivateSigningKey, StandardClaims, SubjectIdentifier, }; use chrono::{DateTime, Utc}; @@ -20,10 +18,8 @@ use http::method::Method; use http::status::StatusCode; use serde::{Deserialize, Serialize}; use thiserror::Error; -use url::Url; use std::future::Future; -use std::ops::Deref; use std::str; /// User info request. @@ -35,11 +31,11 @@ where JU: JsonWebKeyUse, K: JsonWebKey, { - pub(super) url: &'a UserInfoUrl, - pub(super) access_token: AccessToken, - pub(super) require_signed_response: bool, - pub(super) signed_response_verifier: UserInfoVerifier<'static, JE, JS, JT, JU, K>, - pub(super) response_type: UserInfoResponseType, + pub(crate) url: &'a UserInfoUrl, + pub(crate) access_token: AccessToken, + pub(crate) require_signed_response: bool, + pub(crate) signed_response_verifier: UserInfoVerifier<'static, JE, JS, JT, JU, K>, + pub(crate) response_type: UserInfoResponseType, } impl<'a, JE, JS, JT, JU, K> UserInfoRequest<'a, JE, JS, JT, JU, K> where diff --git a/src/verification.rs b/src/verification.rs deleted file mode 100644 index cf030c3..0000000 --- a/src/verification.rs +++ /dev/null @@ -1,2059 +0,0 @@ -use crate::jwt::{JsonWebToken, JsonWebTokenJsonPayloadSerde}; -use crate::user_info::UserInfoClaimsImpl; -use crate::{ - AdditionalClaims, Audience, AuthenticationContextClass, ClientId, ClientSecret, GenderClaim, - IdTokenClaims, IssuerUrl, JsonWebKey, JsonWebKeySet, JsonWebKeyType, JsonWebKeyUse, - JsonWebTokenAccess, JsonWebTokenAlgorithm, JsonWebTokenHeader, JweContentEncryptionAlgorithm, - JwsSigningAlgorithm, Nonce, SubjectIdentifier, -}; - -use chrono::{DateTime, Utc}; -use serde::de::DeserializeOwned; -use serde::Serialize; -use thiserror::Error; - -use std::collections::HashSet; -use std::fmt::Debug; -use std::marker::PhantomData; -use std::ops::Deref; -use std::sync::Arc; - -pub(crate) trait AudiencesClaim { - fn audiences(&self) -> Option<&Vec>; -} - -pub(crate) trait IssuerClaim { - fn issuer(&self) -> Option<&IssuerUrl>; -} - -/// Error verifying claims. -#[derive(Clone, Debug, Error, PartialEq, Eq)] -#[non_exhaustive] -pub enum ClaimsVerificationError { - /// Claims have expired. - #[error("Expired: {0}")] - Expired(String), - /// Audience claim is invalid. - #[error("Invalid audiences: {0}")] - InvalidAudience(String), - /// Authorization context class reference (`acr`) claim is invalid. - #[error("Invalid authorization context class reference: {0}")] - InvalidAuthContext(String), - /// User authenticated too long ago. - #[error("Invalid authentication time: {0}")] - InvalidAuthTime(String), - /// Issuer claim is invalid. - #[error("Invalid issuer: {0}")] - InvalidIssuer(String), - /// Nonce is invalid. - #[error("Invalid nonce: {0}")] - InvalidNonce(String), - /// Subject claim is invalid. - #[error("Invalid subject: {0}")] - InvalidSubject(String), - /// No signature present but claims must be signed. - #[error("Claims must be signed")] - NoSignature, - /// An unexpected error occurred. - #[error("{0}")] - Other(String), - /// Failed to verify the claims signature. - #[error("Signature verification failed")] - SignatureVerification(#[source] SignatureVerificationError), - /// Unsupported argument or value. - #[error("Unsupported: {0}")] - Unsupported(String), -} - -/// Error verifying claims signature. -#[derive(Clone, Debug, Error, PartialEq, Eq)] -#[non_exhaustive] -pub enum SignatureVerificationError { - /// More than one key matches the supplied key constraints (e.g., key ID). - #[error("Ambiguous key identification: {0}")] - AmbiguousKeyId(String), - /// Invalid signature for the supplied claims and signing key. - #[error("Crypto error: {0}")] - CryptoError(String), - /// The supplied signature algorithm is disallowed by the verifier. - #[error("Disallowed signature algorithm: {0}")] - DisallowedAlg(String), - /// The supplied key cannot be used in this context. This may occur if the key type does not - /// match the signature type (e.g., an RSA key used to validate an HMAC) or the JWK usage - /// disallows signatures. - #[error("Invalid cryptographic key: {0}")] - InvalidKey(String), - /// The signing key needed for verifying the - /// [JSON Web Token](https://tools.ietf.org/html/rfc7519)'s signature/MAC could not be found. - /// This error can occur if the key ID (`kid`) specified in the JWT's - /// [JOSE header](https://tools.ietf.org/html/rfc7519#section-5) does not match the ID of any - /// key in the OpenID Connect provider's JSON Web Key Set (JWKS), typically retrieved from - /// the provider's [JWKS document]( - /// http://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata). To support - /// [rotation of asymmetric signing keys]( - /// http://openid.net/specs/openid-connect-core-1_0.html#RotateSigKeys), client applications - /// should consider refreshing the JWKS document (via - /// [`JsonWebKeySet::fetch`][crate::JsonWebKeySet::fetch]). - /// - /// This error can also occur if the identified - /// [JSON Web Key](https://tools.ietf.org/html/rfc7517) is of the wrong type (e.g., an RSA key - /// when the JOSE header specifies an ECDSA algorithm) or does not support signing. - #[error("No matching key found")] - NoMatchingKey, - /// Unsupported signature algorithm. - #[error("Unsupported signature algorithm: {0}")] - UnsupportedAlg(String), - /// An unexpected error occurred. - #[error("Other error: {0}")] - Other(String), -} - -// This struct is intentionally private. -#[derive(Clone)] -struct JwtClaimsVerifier<'a, JS, JT, JU, K> -where - JS: JwsSigningAlgorithm, - JT: JsonWebKeyType, - JU: JsonWebKeyUse, - K: JsonWebKey, -{ - allowed_algs: Option>, - aud_match_required: bool, - client_id: ClientId, - client_secret: Option, - iss_required: bool, - issuer: IssuerUrl, - is_signature_check_enabled: bool, - other_aud_verifier_fn: Arc bool + 'a + Send + Sync>, - signature_keys: JsonWebKeySet, -} -impl<'a, JS, JT, JU, K> JwtClaimsVerifier<'a, JS, JT, JU, K> -where - JS: JwsSigningAlgorithm, - JT: JsonWebKeyType, - JU: JsonWebKeyUse, - K: JsonWebKey, -{ - pub fn new( - client_id: ClientId, - issuer: IssuerUrl, - signature_keys: JsonWebKeySet, - ) -> Self { - JwtClaimsVerifier { - allowed_algs: Some([JS::rsa_sha_256()].iter().cloned().collect()), - aud_match_required: true, - client_id, - client_secret: None, - iss_required: true, - issuer, - is_signature_check_enabled: true, - // Secure default: reject all other audiences as untrusted, since any other audience - // can potentially impersonate the user when by sending its copy of these claims - // to this relying party. - other_aud_verifier_fn: Arc::new(|_| false), - signature_keys, - } - } - - pub fn require_audience_match(mut self, aud_required: bool) -> Self { - self.aud_match_required = aud_required; - self - } - - pub fn require_issuer_match(mut self, iss_required: bool) -> Self { - self.iss_required = iss_required; - self - } - - pub fn require_signature_check(mut self, sig_required: bool) -> Self { - self.is_signature_check_enabled = sig_required; - self - } - - pub fn set_allowed_algs(mut self, algs: I) -> Self - where - I: IntoIterator, - { - self.allowed_algs = Some(algs.into_iter().collect()); - self - } - pub fn allow_any_alg(mut self) -> Self { - self.allowed_algs = None; - self - } - - pub fn set_client_secret(mut self, client_secret: ClientSecret) -> Self { - self.client_secret = Some(client_secret); - self - } - - pub fn set_other_audience_verifier_fn(mut self, other_aud_verifier_fn: T) -> Self - where - T: Fn(&Audience) -> bool + 'a + Send + Sync, - { - self.other_aud_verifier_fn = Arc::new(other_aud_verifier_fn); - self - } - - fn validate_jose_header( - jose_header: &JsonWebTokenHeader, - ) -> Result<(), ClaimsVerificationError> - where - JE: JweContentEncryptionAlgorithm, - { - // The 'typ' header field must either be omitted or have the canonicalized value JWT. - if let Some(ref jwt_type) = jose_header.typ { - if jwt_type.to_uppercase() != "JWT" { - return Err(ClaimsVerificationError::Unsupported(format!( - "unexpected or unsupported JWT type `{}`", - **jwt_type - ))); - } - } - // The 'cty' header field must be omitted, since it's only used for JWTs that contain - // content types other than JSON-encoded claims. This may include nested JWTs, such as if - // JWE encryption is used. This is currently unsupported. - if let Some(ref content_type) = jose_header.cty { - if content_type.to_uppercase() == "JWT" { - return Err(ClaimsVerificationError::Unsupported( - "nested JWT's are not currently supported".to_string(), - )); - } else { - return Err(ClaimsVerificationError::Unsupported(format!( - "unexpected or unsupported JWT content type `{}`", - **content_type - ))); - } - } - - // If 'crit' fields are specified, we must reject any we do not understand. Since this - // implementation doesn't understand any of them, unconditionally reject the JWT. Note that - // the spec prohibits this field from containing any of the standard headers or being empty. - if jose_header.crit.is_some() { - // https://tools.ietf.org/html/rfc7515#appendix-E - return Err(ClaimsVerificationError::Unsupported( - "critical JWT header fields are unsupported".to_string(), - )); - } - Ok(()) - } - - pub fn verified_claims(&self, jwt: A) -> Result - where - A: JsonWebTokenAccess, - C: AudiencesClaim + Debug + DeserializeOwned + IssuerClaim + Serialize, - JE: JweContentEncryptionAlgorithm, - T: AudiencesClaim + IssuerClaim, - { - { - let jose_header = jwt.unverified_header(); - Self::validate_jose_header(jose_header)?; - - // The code below roughly follows the validation steps described in - // https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation - - // 1. If the ID Token is encrypted, decrypt it using the keys and algorithms that the Client - // specified during Registration that the OP was to use to encrypt the ID Token. If - // encryption was negotiated with the OP at Registration time and the ID Token is not - // encrypted, the RP SHOULD reject it. - - if let JsonWebTokenAlgorithm::Encryption(ref encryption_alg) = jose_header.alg { - return Err(ClaimsVerificationError::Unsupported(format!( - "JWE encryption is not currently supported (found algorithm `{}`)", - serde_plain::to_string(encryption_alg).unwrap_or_else(|err| panic!( - "encryption alg {:?} failed to serialize to a string: {}", - encryption_alg, err - )), - ))); - } - } - - // TODO: Add encryption (JWE) support - { - // 2. The Issuer Identifier for the OpenID Provider (which is typically obtained during - // Discovery) MUST exactly match the value of the iss (issuer) Claim. - let unverified_claims = jwt.unverified_payload_ref(); - if self.iss_required { - if let Some(issuer) = unverified_claims.issuer() { - if *issuer != self.issuer { - return Err(ClaimsVerificationError::InvalidIssuer(format!( - "expected `{}` (found `{}`)", - *self.issuer, **issuer - ))); - } - } else { - return Err(ClaimsVerificationError::InvalidIssuer( - "missing issuer claim".to_string(), - )); - } - } - - // 3. The Client MUST validate that the aud (audience) Claim contains its client_id value - // registered at the Issuer identified by the iss (issuer) Claim as an audience. The aud - // (audience) Claim MAY contain an array with more than one element. The ID Token MUST be - // rejected if the ID Token does not list the Client as a valid audience, or if it - // contains additional audiences not trusted by the Client. - if self.aud_match_required { - if let Some(audiences) = unverified_claims.audiences() { - if !audiences - .iter() - .any(|aud| (**aud).deref() == self.client_id.deref()) - { - return Err(ClaimsVerificationError::InvalidAudience(format!( - "must contain `{}` (found audiences: {})", - *self.client_id, - audiences - .iter() - .map(|aud| format!("`{}`", Deref::deref(aud))) - .collect::>() - .join(", ") - ))); - } else if audiences.len() > 1 { - audiences - .iter() - .filter(|aud| (**aud).deref() != self.client_id.deref()) - .find(|aud| !(self.other_aud_verifier_fn)(aud)) - .map(|aud| { - Err(ClaimsVerificationError::InvalidAudience(format!( - "`{}` is not a trusted audience", - **aud, - ))) - }) - .unwrap_or(Ok(()))?; - } - } else { - return Err(ClaimsVerificationError::InvalidAudience( - "missing audiences claim".to_string(), - )); - } - } - } - // Steps 4--5 (azp claim validation) are specific to the ID token. - - // 6. If the ID Token is received via direct communication between the Client and the Token - // Endpoint (which it is in this flow), the TLS server validation MAY be used to validate - // the issuer in place of checking the token signature. The Client MUST validate the - // signature of all other ID Tokens according to JWS [JWS] using the algorithm specified - // in the JWT alg Header Parameter. The Client MUST use the keys provided by the Issuer. - if !self.is_signature_check_enabled { - return Ok(jwt.unverified_payload()); - } - - // Borrow the header again. We had to drop the reference above to allow for the - // early exit calling jwt.unverified_claims(), which takes ownership of the JWT. - let signature_alg = match jwt.unverified_header().alg { - // Encryption is handled above. - JsonWebTokenAlgorithm::Encryption(_) => unreachable!(), - JsonWebTokenAlgorithm::Signature(ref signature_alg, _) => signature_alg, - // Section 2 of OpenID Connect Core 1.0 specifies that "ID Tokens MUST NOT use - // none as the alg value unless the Response Type used returns no ID Token from - // the Authorization Endpoint (such as when using the Authorization Code Flow) - // and the Client explicitly requested the use of none at Registration time." - // - // While there's technically a use case where this is ok, we choose not to - // support it for now to protect against accidental misuse. If demand arises, - // we can figure out a API that mitigates the risk. - JsonWebTokenAlgorithm::None => return Err(ClaimsVerificationError::NoSignature), - } - .clone(); - - // 7. The alg value SHOULD be the default of RS256 or the algorithm sent by the Client - // in the id_token_signed_response_alg parameter during Registration. - if let Some(ref allowed_algs) = self.allowed_algs { - if !allowed_algs.contains(&signature_alg) { - return Err(ClaimsVerificationError::SignatureVerification( - SignatureVerificationError::DisallowedAlg(format!( - "algorithm `{}` is not one of: {}", - serde_plain::to_string(&signature_alg).unwrap_or_else(|err| panic!( - "signature alg {:?} failed to serialize to a string: {}", - signature_alg, err, - )), - allowed_algs - .iter() - .map( - |alg| serde_plain::to_string(alg).unwrap_or_else(|err| panic!( - "signature alg {:?} failed to serialize to a string: {}", - alg, err, - )) - ) - .collect::>() - .join(", "), - )), - )); - } - } - - // NB: We must *not* trust the 'kid' (key ID) or 'alg' (algorithm) fields present in the - // JOSE header, as an attacker could manipulate these while forging the JWT. The code - // below must be secure regardless of how these fields are manipulated. - - if signature_alg.uses_shared_secret() { - // 8. If the JWT alg Header Parameter uses a MAC based algorithm such as HS256, - // HS384, or HS512, the octets of the UTF-8 representation of the client_secret - // corresponding to the client_id contained in the aud (audience) Claim are used - // as the key to validate the signature. For MAC based algorithms, the behavior - // is unspecified if the aud is multi-valued or if an azp value is present that - // is different than the aud value. - if let Some(ref client_secret) = self.client_secret { - let key = K::new_symmetric(client_secret.secret().clone().into_bytes()); - return jwt - .payload(&signature_alg, &key) - .map_err(ClaimsVerificationError::SignatureVerification); - } else { - // The client secret isn't confidential for public clients, so anyone can forge a - // JWT with a valid signature. - return Err(ClaimsVerificationError::SignatureVerification( - SignatureVerificationError::DisallowedAlg( - "symmetric signatures are disallowed for public clients".to_string(), - ), - )); - } - } - - // Section 10.1 of OpenID Connect Core 1.0 states that the JWT must include a key ID - // if the JWK set contains more than one public key. - - // See if any key has a matching key ID (if supplied) and compatible type. - let public_keys = { - let key_id = &jwt.unverified_header().kid; - self.signature_keys.filter_keys(key_id, &signature_alg) - }; - if public_keys.is_empty() { - return Err(ClaimsVerificationError::SignatureVerification( - SignatureVerificationError::NoMatchingKey, - )); - } else if public_keys.len() != 1 { - return Err(ClaimsVerificationError::SignatureVerification( - SignatureVerificationError::AmbiguousKeyId(format!( - "JWK set must only contain one eligible public key \ - ({} eligible keys: {})", - public_keys.len(), - public_keys - .iter() - .map(|key| format!( - "{} ({})", - key.key_id() - .map(|kid| format!("`{}`", **kid)) - .unwrap_or_else(|| "null ID".to_string()), - serde_plain::to_string(key.key_type()).unwrap_or_else(|err| panic!( - "key type {:?} failed to serialize to a string: {}", - key.key_type(), - err, - )) - )) - .collect::>() - .join(", ") - )), - )); - } - - jwt.payload( - &signature_alg.clone(), - *public_keys.first().expect("unreachable"), - ) - .map_err(ClaimsVerificationError::SignatureVerification) - - // Steps 9--13 are specific to the ID token. - } -} - -/// Trait for verifying ID token nonces. -pub trait NonceVerifier { - /// Verifies the nonce. - /// - /// Returns `Ok(())` if the nonce is valid, or a string describing the error otherwise. - fn verify(self, nonce: Option<&Nonce>) -> Result<(), String>; -} - -impl NonceVerifier for &Nonce { - fn verify(self, nonce: Option<&Nonce>) -> Result<(), String> { - if let Some(claims_nonce) = nonce { - // Nonce::eq is already implemented with a constant time comparison - if claims_nonce != self { - return Err("nonce mismatch".to_string()); - } - } else { - return Err("missing nonce claim".to_string()); - } - Ok(()) - } -} - -impl NonceVerifier for F -where - F: FnOnce(Option<&Nonce>) -> Result<(), String>, -{ - fn verify(self, nonce: Option<&Nonce>) -> Result<(), String> { - self(nonce) - } -} - -/// ID token verifier. -#[derive(Clone)] -pub struct IdTokenVerifier<'a, JS, JT, JU, K> -where - JS: JwsSigningAlgorithm, - JT: JsonWebKeyType, - JU: JsonWebKeyUse, - K: JsonWebKey, -{ - acr_verifier_fn: - Arc) -> Result<(), String> + 'a + Send + Sync>, - #[allow(clippy::type_complexity)] - auth_time_verifier_fn: - Arc>) -> Result<(), String> + 'a + Send + Sync>, - iat_verifier_fn: Arc) -> Result<(), String> + 'a + Send + Sync>, - jwt_verifier: JwtClaimsVerifier<'a, JS, JT, JU, K>, - time_fn: Arc DateTime + 'a + Send + Sync>, -} -impl<'a, JS, JT, JU, K> IdTokenVerifier<'a, JS, JT, JU, K> -where - JS: JwsSigningAlgorithm, - JT: JsonWebKeyType, - JU: JsonWebKeyUse, - K: JsonWebKey, -{ - fn new(jwt_verifier: JwtClaimsVerifier<'a, JS, JT, JU, K>) -> Self { - IdTokenVerifier { - // By default, accept authorization context reference (acr claim). - acr_verifier_fn: Arc::new(|_| Ok(())), - auth_time_verifier_fn: Arc::new(|_| Ok(())), - // By default, accept any issued time (iat claim). - iat_verifier_fn: Arc::new(|_| Ok(())), - jwt_verifier, - // By default, use the current system time. - time_fn: Arc::new(Utc::now), - } - } - - /// Initializes a new verifier for a public client (i.e., one without a client secret). - pub fn new_public_client( - client_id: ClientId, - issuer: IssuerUrl, - signature_keys: JsonWebKeySet, - ) -> Self { - Self::new(JwtClaimsVerifier::new(client_id, issuer, signature_keys)) - } - - /// Initializes a no-op verifier that performs no signature, audience, or issuer verification. - /// The token's expiration time is still checked, and the token is otherwise required to conform to the expected format. - pub fn new_insecure_without_verification() -> Self { - let empty_issuer = IssuerUrl::new("https://0.0.0.0".to_owned()) - .expect("Creating empty issuer url mustn't fail"); - Self::new_public_client( - ClientId::new(String::new()), - empty_issuer, - JsonWebKeySet::new(vec![]), - ) - .insecure_disable_signature_check() - .require_audience_match(false) - .require_issuer_match(false) - } - - /// Initializes a new verifier for a confidential client (i.e., one with a client secret). - /// - /// A confidential client verifier is required in order to verify ID tokens signed using a - /// shared secret algorithm such as `HS256`, `HS384`, or `HS512`. For these algorithms, the - /// client secret is the shared secret. - pub fn new_confidential_client( - client_id: ClientId, - client_secret: ClientSecret, - issuer: IssuerUrl, - signature_keys: JsonWebKeySet, - ) -> Self { - Self::new( - JwtClaimsVerifier::new(client_id, issuer, signature_keys) - .set_client_secret(client_secret), - ) - } - - /// Specifies which JSON Web Signature algorithms are supported. - pub fn set_allowed_algs(mut self, algs: I) -> Self - where - I: IntoIterator, - { - self.jwt_verifier = self.jwt_verifier.set_allowed_algs(algs); - self - } - - /// Specifies that any signature algorithm is supported. - pub fn allow_any_alg(mut self) -> Self { - self.jwt_verifier = self.jwt_verifier.allow_any_alg(); - self - } - - /// Specifies a function for verifying the `acr` claim. - /// - /// The function should return `Ok(())` if the claim is valid, or a string describing the error - /// otherwise. - pub fn set_auth_context_verifier_fn(mut self, acr_verifier_fn: T) -> Self - where - T: Fn(Option<&AuthenticationContextClass>) -> Result<(), String> + 'a + Send + Sync, - { - self.acr_verifier_fn = Arc::new(acr_verifier_fn); - self - } - - /// Specifies a function for verifying the `auth_time` claim. - /// - /// The function should return `Ok(())` if the claim is valid, or a string describing the error - /// otherwise. - pub fn set_auth_time_verifier_fn(mut self, auth_time_verifier_fn: T) -> Self - where - T: Fn(Option>) -> Result<(), String> + 'a + Send + Sync, - { - self.auth_time_verifier_fn = Arc::new(auth_time_verifier_fn); - self - } - - /// Enables signature verification. - /// - /// Signature verification is enabled by default, so this function is only useful if - /// [`IdTokenVerifier::insecure_disable_signature_check`] was previously invoked. - pub fn enable_signature_check(mut self) -> Self { - self.jwt_verifier = self.jwt_verifier.require_signature_check(true); - self - } - - /// Disables signature verification. - /// - /// # Security Warning - /// - /// Unverified ID tokens may be subject to forgery. See [Section 16.3]( - /// https://openid.net/specs/openid-connect-core-1_0.html#TokenManufacture) for more - /// information. - pub fn insecure_disable_signature_check(mut self) -> Self { - self.jwt_verifier = self.jwt_verifier.require_signature_check(false); - self - } - - /// Specifies whether the issuer claim must match the expected issuer URL for the provider. - pub fn require_issuer_match(mut self, iss_required: bool) -> Self { - self.jwt_verifier = self.jwt_verifier.require_issuer_match(iss_required); - self - } - - /// Specifies whether the audience claim must match this client's client ID. - pub fn require_audience_match(mut self, aud_required: bool) -> Self { - self.jwt_verifier = self.jwt_verifier.require_audience_match(aud_required); - self - } - - /// Specifies a function for returning the current time. - /// - /// This function is used for verifying the ID token expiration time. - pub fn set_time_fn(mut self, time_fn: T) -> Self - where - T: Fn() -> DateTime + 'a + Send + Sync, - { - self.time_fn = Arc::new(time_fn); - self - } - - /// Specifies a function for verifying the ID token issue time. - /// - /// The function should return `Ok(())` if the claim is valid, or a string describing the error - /// otherwise. - pub fn set_issue_time_verifier_fn(mut self, iat_verifier_fn: T) -> Self - where - T: Fn(DateTime) -> Result<(), String> + 'a + Send + Sync, - { - self.iat_verifier_fn = Arc::new(iat_verifier_fn); - self - } - - /// Specifies a function for verifying audiences included in the `aud` claim that differ from - /// this client's client ID. - /// - /// The function should return `true` if the audience is trusted, or `false` otherwise. - /// - /// [Section 3.1.3.7](https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation) - /// states that *"The ID Token MUST be rejected if the ID Token does not list the Client as a - /// valid audience, or if it contains additional audiences not trusted by the Client."* - pub fn set_other_audience_verifier_fn(mut self, other_aud_verifier_fn: T) -> Self - where - T: Fn(&Audience) -> bool + 'a + Send + Sync, - { - self.jwt_verifier = self - .jwt_verifier - .set_other_audience_verifier_fn(other_aud_verifier_fn); - self - } - - pub(super) fn verified_claims<'b, AC, GC, JE, N>( - &self, - jwt: &'b JsonWebToken, JsonWebTokenJsonPayloadSerde>, - nonce_verifier: N, - ) -> Result<&'b IdTokenClaims, ClaimsVerificationError> - where - AC: AdditionalClaims, - GC: GenderClaim, - JE: JweContentEncryptionAlgorithm, - N: NonceVerifier, - { - // The code below roughly follows the validation steps described in - // https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation - - // Steps 1--3 are handled by the generic JwtClaimsVerifier. - let partially_verified_claims = self.jwt_verifier.verified_claims(jwt)?; - - self.verify_claims(partially_verified_claims, nonce_verifier)?; - Ok(partially_verified_claims) - } - - pub(super) fn verified_claims_owned( - &self, - jwt: JsonWebToken, JsonWebTokenJsonPayloadSerde>, - nonce_verifier: N, - ) -> Result, ClaimsVerificationError> - where - AC: AdditionalClaims, - GC: GenderClaim, - JE: JweContentEncryptionAlgorithm, - N: NonceVerifier, - { - // The code below roughly follows the validation steps described in - // https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation - - // Steps 1--3 are handled by the generic JwtClaimsVerifier. - let partially_verified_claims = self.jwt_verifier.verified_claims(jwt)?; - - self.verify_claims(&partially_verified_claims, nonce_verifier)?; - Ok(partially_verified_claims) - } - - fn verify_claims( - &self, - partially_verified_claims: &'_ IdTokenClaims, - nonce_verifier: N, - ) -> Result<(), ClaimsVerificationError> - where - AC: AdditionalClaims, - GC: GenderClaim, - N: NonceVerifier, - { - // 4. If the ID Token contains multiple audiences, the Client SHOULD verify that an azp - // Claim is present. - - // There is significant confusion and contradiction in the OpenID Connect Core spec around - // the azp claim. See https://bitbucket.org/openid/connect/issues/973/ for a detailed - // discussion. Given the lack of clarity around how this claim should be used, we defer - // any verification of it here until a use case becomes apparent. If such a use case does - // arise, we most likely want to allow clients to pass in a function for validating the - // azp claim rather than introducing logic that affects all clients of this library. - - // This naive implementation of the spec would almost certainly not be useful in practice: - /* - let azp_required = partially_verified_claims.audiences().len() > 1; - - // 5. If an azp (authorized party) Claim is present, the Client SHOULD verify that its - // client_id is the Claim Value. - if let Some(authorized_party) = partially_verified_claims.authorized_party() { - if *authorized_party != self.client_id { - return Err(ClaimsVerificationError::InvalidAudience(format!( - "authorized party must match client ID `{}` (found `{}`", - *self.client_id, **authorized_party - ))); - } - } else if azp_required { - return Err(ClaimsVerificationError::InvalidAudience(format!( - "missing authorized party claim but multiple audiences found" - ))); - } - */ - - // Steps 6--8 are handled by the generic JwtClaimsVerifier. - - // 9. The current time MUST be before the time represented by the exp Claim. - let cur_time = (*self.time_fn)(); - if cur_time >= partially_verified_claims.expiration() { - return Err(ClaimsVerificationError::Expired(format!( - "ID token expired at {} (current time is {})", - partially_verified_claims.expiration(), - cur_time - ))); - } - - // 10. The iat Claim can be used to reject tokens that were issued too far away from the - // current time, limiting the amount of time that nonces need to be stored to prevent - // attacks. The acceptable range is Client specific. - (*self.iat_verifier_fn)(partially_verified_claims.issue_time()) - .map_err(ClaimsVerificationError::Expired)?; - - // 11. If a nonce value was sent in the Authentication Request, a nonce Claim MUST be - // present and its value checked to verify that it is the same value as the one that was - // sent in the Authentication Request. The Client SHOULD check the nonce value for - // replay attacks. The precise method for detecting replay attacks is Client specific. - nonce_verifier - .verify(partially_verified_claims.nonce()) - .map_err(ClaimsVerificationError::InvalidNonce)?; - - // 12. If the acr Claim was requested, the Client SHOULD check that the asserted Claim Value - // is appropriate. The meaning and processing of acr Claim Values is out of scope for - // this specification. - (*self.acr_verifier_fn)(partially_verified_claims.auth_context_ref()) - .map_err(ClaimsVerificationError::InvalidAuthContext)?; - - // 13. If the auth_time Claim was requested, either through a specific request for this - // Claim or by using the max_age parameter, the Client SHOULD check the auth_time Claim - // value and request re-authentication if it determines too much time has elapsed since - // the last End-User authentication. - (*self.auth_time_verifier_fn)(partially_verified_claims.auth_time()) - .map_err(ClaimsVerificationError::InvalidAuthTime)?; - - Ok(()) - } -} - -/// User info verifier. -#[derive(Clone)] -pub struct UserInfoVerifier<'a, JE, JS, JT, JU, K> -where - JE: JweContentEncryptionAlgorithm, - JS: JwsSigningAlgorithm, - JT: JsonWebKeyType, - JU: JsonWebKeyUse, - K: JsonWebKey, -{ - jwt_verifier: JwtClaimsVerifier<'a, JS, JT, JU, K>, - expected_subject: Option, - _phantom: PhantomData, -} -impl<'a, JE, JS, JT, JU, K> UserInfoVerifier<'a, JE, JS, JT, JU, K> -where - JE: JweContentEncryptionAlgorithm, - JS: JwsSigningAlgorithm, - JT: JsonWebKeyType, - JU: JsonWebKeyUse, - K: JsonWebKey, -{ - /// Instantiates a user info verifier. - pub fn new( - client_id: ClientId, - issuer: IssuerUrl, - signature_keys: JsonWebKeySet, - expected_subject: Option, - ) -> Self { - UserInfoVerifier { - jwt_verifier: JwtClaimsVerifier::new(client_id, issuer, signature_keys), - expected_subject, - _phantom: PhantomData, - } - } - - pub(crate) fn expected_subject(&self) -> Option<&SubjectIdentifier> { - self.expected_subject.as_ref() - } - - /// Specifies whether the issuer claim must match the expected issuer URL for the provider. - pub fn require_issuer_match(mut self, iss_required: bool) -> Self { - self.jwt_verifier = self.jwt_verifier.require_issuer_match(iss_required); - self - } - - /// Specifies whether the audience claim must match this client's client ID. - pub fn require_audience_match(mut self, aud_required: bool) -> Self { - self.jwt_verifier = self.jwt_verifier.require_audience_match(aud_required); - self - } - - pub(crate) fn verified_claims( - &self, - user_info_jwt: JsonWebToken< - JE, - JS, - JT, - UserInfoClaimsImpl, - JsonWebTokenJsonPayloadSerde, - >, - ) -> Result, ClaimsVerificationError> - where - AC: AdditionalClaims, - GC: GenderClaim, - { - let user_info = self.jwt_verifier.verified_claims(user_info_jwt)?; - if self - .expected_subject - .iter() - .all(|expected_subject| user_info.standard_claims.sub == *expected_subject) - { - Ok(user_info) - } else { - Err(ClaimsVerificationError::InvalidSubject(format!( - "expected `{}` (found `{}`)", - // This can only happen when self.expected_subject is not None. - self.expected_subject.as_ref().unwrap().as_str(), - user_info.standard_claims.sub.as_str() - ))) - } - } -} - -#[cfg(test)] -mod tests { - use crate::core::{ - CoreIdToken, CoreIdTokenClaims, CoreIdTokenVerifier, CoreJsonWebKey, CoreJsonWebKeySet, - CoreJsonWebKeyType, CoreJsonWebKeyUse, CoreJweContentEncryptionAlgorithm, - CoreJwsSigningAlgorithm, CoreRsaPrivateSigningKey, CoreUserInfoClaims, - CoreUserInfoJsonWebToken, CoreUserInfoVerifier, - }; - use crate::jwt::tests::{TEST_RSA_PRIV_KEY, TEST_RSA_PUB_KEY}; - use crate::jwt::{JsonWebToken, JsonWebTokenHeader, JsonWebTokenJsonPayloadSerde}; - use crate::types::helpers::timestamp_to_utc; - use crate::types::{Base64UrlEncodedBytes, Timestamp}; - use crate::verification::{AudiencesClaim, IssuerClaim, JwtClaimsVerifier}; - use crate::{ - AccessToken, Audience, AuthenticationContextClass, AuthorizationCode, - ClaimsVerificationError, ClientId, ClientSecret, EndUserName, IssuerUrl, JsonWebKeyId, - Nonce, SignatureVerificationError, StandardClaims, SubjectIdentifier, UserInfoError, - }; - - use chrono::{TimeZone, Utc}; - use serde::{Deserialize, Serialize}; - - use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; - - type CoreJsonWebTokenHeader = JsonWebTokenHeader< - CoreJweContentEncryptionAlgorithm, - CoreJwsSigningAlgorithm, - CoreJsonWebKeyType, - >; - - type CoreJwtClaimsVerifier<'a> = JwtClaimsVerifier< - 'a, - CoreJwsSigningAlgorithm, - CoreJsonWebKeyType, - CoreJsonWebKeyUse, - CoreJsonWebKey, - >; - - fn assert_unsupported(result: Result, expected_substr: &str) { - match result { - Err(ClaimsVerificationError::Unsupported(msg)) => { - assert!(msg.contains(expected_substr)) - } - Err(err) => panic!("unexpected error: {:?}", err), - Ok(_) => panic!("validation should fail"), - } - } - - #[test] - fn test_jose_header() { - // Unexpected JWT type. - assert_unsupported( - CoreJwtClaimsVerifier::validate_jose_header( - &serde_json::from_str::( - "{\"alg\":\"RS256\",\"typ\":\"NOT_A_JWT\"}", - ) - .expect("failed to deserialize"), - ), - "unsupported JWT type", - ); - - // Nested JWTs. - assert_unsupported( - CoreJwtClaimsVerifier::validate_jose_header( - &serde_json::from_str::( - "{\"alg\":\"RS256\",\"cty\":\"JWT\"}", - ) - .expect("failed to deserialize"), - ), - "nested JWT", - ); - assert_unsupported( - CoreJwtClaimsVerifier::validate_jose_header( - &serde_json::from_str::( - "{\"alg\":\"RS256\",\"cty\":\"NOT_A_JWT\"}", - ) - .expect("failed to deserialize"), - ), - "unsupported JWT content type", - ); - - // Critical fields. Adapted from https://tools.ietf.org/html/rfc7515#appendix-E - assert_unsupported( - CoreJwtClaimsVerifier::validate_jose_header( - &serde_json::from_str::( - "{\ - \"alg\":\"RS256\",\ - \"crit\":[\"http://example.invalid/UNDEFINED\"],\ - \"http://example.invalid/UNDEFINED\":true\ - }", - ) - .expect("failed to deserialize"), - ), - "critical JWT header fields are unsupported", - ); - } - - #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] - struct TestClaims { - aud: Option>, - iss: Option, - payload: String, - } - impl AudiencesClaim for TestClaims { - fn audiences(&self) -> Option<&Vec> { - self.aud.as_ref() - } - } - impl IssuerClaim for TestClaims { - fn issuer(&self) -> Option<&IssuerUrl> { - self.iss.as_ref() - } - } - type TestClaimsJsonWebToken = JsonWebToken< - CoreJweContentEncryptionAlgorithm, - CoreJwsSigningAlgorithm, - CoreJsonWebKeyType, - TestClaims, - JsonWebTokenJsonPayloadSerde, - >; - - #[test] - fn test_jwt_verified_claims() { - let rsa_key = serde_json::from_str::(TEST_RSA_PUB_KEY) - .expect("deserialization failed"); - - let client_id = ClientId::new("my_client".to_string()); - let issuer = IssuerUrl::new("https://example.com".to_string()).unwrap(); - let verifier = CoreJwtClaimsVerifier::new( - client_id.clone(), - issuer.clone(), - CoreJsonWebKeySet::new(vec![rsa_key.clone()]), - ); - - // Invalid JOSE header. - assert_unsupported( - verifier.verified_claims( - serde_json::from_value::(serde_json::Value::String( - "eyJhbGciOiJBMjU2R0NNIiwiY3R5IjoiSldUIn0.eyJhdWQiOlsibXlfY2xpZW50Il0sImlzcyI6Im\ - h0dHBzOi8vZXhhbXBsZS5jb20iLCJwYXlsb2FkIjoiaGVsbG8gd29ybGQifQ.YmFkX2hhc2g" - .to_string(), - )).expect("failed to deserialize"), - ), - "nested JWT", - ); - - // JWE-encrypted JWT. - assert_unsupported( - verifier.verified_claims( - serde_json::from_value::(serde_json::Value::String( - "eyJhbGciOiJBMjU2R0NNIn0.eyJhdWQiOlsibXlfY2xpZW50Il0sImlzcyI6Imh0dHBzOi8vZXhhbX\ - BsZS5jb20iLCJwYXlsb2FkIjoiaGVsbG8gd29ybGQifQ.YmFkX2hhc2g" - .to_string(), - )).expect("failed to deserialize"), - ), - "JWE encryption", - ); - - // Wrong issuer. - match verifier.verified_claims( - serde_json::from_value::(serde_json::Value::String( - "eyJhbGciOiJSUzI1NiJ9.eyJhdWQiOlsibXlfY2xpZW50Il0sImlzcyI6Imh0dHBzOi8vYXR0YWNrZXIuY\ - 29tIiwicGF5bG9hZCI6ImhlbGxvIHdvcmxkIn0.YmFkX2hhc2g" - .to_string(), - )).expect("failed to deserialize"), - ) { - Err(ClaimsVerificationError::InvalidIssuer(_)) => {}, - other => panic!("unexpected result: {:?}", other), - } - - // Missing issuer. - match verifier.verified_claims( - serde_json::from_value::(serde_json::Value::String( - "eyJhbGciOiJSUzI1NiJ9.eyJhdWQiOlsibXlfY2xpZW50Il0sInBheWxvYWQiOiJoZWxsbyB3b3JsZCJ9.\ - YmFkX2hhc2g" - .to_string(), - )).expect("failed to deserialize"), - ) { - Err(ClaimsVerificationError::InvalidIssuer(_)) => {}, - other => panic!("unexpected result: {:?}", other), - } - - // Ignore missing issuer. - verifier - .clone() - .require_issuer_match(false) - .verified_claims( - serde_json::from_value::(serde_json::Value::String( - "eyJhbGciOiJSUzI1NiJ9.eyJhdWQiOlsibXlfY2xpZW50Il0sInBheWxvYWQiOiJoZWxsbyB3b3JsZCJ9.\ - nv09al63NNDfb8cF3IozegXKbPaUC08zknRPKmQ5qKgXv80hjVxknkpRz7BxocB3JYTBjhYd0gyN9wAuJj\ - byZ1QaUC14HOB83awAGbehy5yFLkLadTfPT7-siBCvE2V7AF73a_21YvwdkKmJ-RaKWHzFnG8CDmioma3X\ - cWyrsdRLgvUkrWllajLRo8DCIXQ8OuZo1_o4n17PSlPxSkhKIrgaWCvG6tan40Y_1DZOFv47bx4hQUGd-J\ - h2aEjiwn65WV3M_Xb2vQMP7VgYNVaNlfxzpL4yDASItbPMWaXBt3ZUa_IOGoSx2GMnPkrQ4xp56qUth6U7\ - esWPqRSqqolnHg" - .to_string(), - )).expect("failed to deserialize"), - ).expect("verification should succeed"); - - // Wrong audience. - match verifier.verified_claims( - serde_json::from_value::(serde_json::Value::String( - "eyJhbGciOiJSUzI1NiJ9.eyJhdWQiOlsib3RoZXJfY2xpZW50Il0sImlzcyI6Imh0dHBzOi8vZXhhbXBsZ\ - S5jb20iLCJwYXlsb2FkIjoiaGVsbG8gd29ybGQifQ.YmFkX2hhc2g" - .to_string(), - )).expect("failed to deserialize"), - ) { - Err(ClaimsVerificationError::InvalidAudience(_)) => {}, - other => panic!("unexpected result: {:?}", other), - } - - // Missing audience. - match verifier.verified_claims( - serde_json::from_value::(serde_json::Value::String( - "eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJodHRwczovL2V4YW1wbGUuY29tIiwicGF5bG9hZCI6ImhlbGxvI\ - HdvcmxkIn0.YmFkX2hhc2g" - .to_string(), - )).expect("failed to deserialize"), - ) { - Err(ClaimsVerificationError::InvalidAudience(_)) => {}, - other => panic!("unexpected result: {:?}", other), - } - - // Ignore missing audience. - verifier - .clone() - .require_audience_match(false) - .verified_claims( - serde_json::from_value::(serde_json::Value::String( - "eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJodHRwczovL2V4YW1wbGUuY29tIiwicGF5bG9hZCI6Imhlb\ - GxvIHdvcmxkIn0.lP-Z_zGPNoKIbLQsnrZc2LAc5qJrKyb7t07ZtJUKVhcwHiCUou4bBhq5RHlElCh\ - 0ElRRP6I25lp6UszkRvIC46UV3GVze0x73kVkHSvCVI7MO75LbL9BRqrm5b4CN2zCiFBY8-EwTXnJd\ - Ri0d_U8K29TV24L2I-Z5ZILebwUue1N59AGDjx2yYLFx5NOw3TUsPyscG62aZAT321pL_jcYwTWTWw\ - 2FYm07zguwx-PUTZwGXlJiOgXQqRIbY_1bS3I_D8UWsmEB3DmV0f9z-iklgIPFawa4wHaE-hpzBAEx\ - pSieyOavA5pl0Se3XRYA-CkdDVgzG0Pt4IdnxFanfUXTw" - .to_string(), - )).expect("failed to deserialize"), - ).expect("verification should succeed"); - - // Multiple audiences, where one is a match (default = reject) - match verifier.verified_claims( - serde_json::from_value::(serde_json::Value::String( - "eyJhbGciOiJSUzI1NiJ9.eyJhdWQiOlsiYXVkMSIsIm15X2NsaWVudCIsImF1ZDIiXSwiaXNzIjoia\ - HR0cHM6Ly9leGFtcGxlLmNvbSIsInBheWxvYWQiOiJoZWxsbyB3b3JsZCJ9.N9ibisEe0kKLe1GDWM\ - ON3PmYqbL73dag-loM8pjKJNinF9SB7n4JuSu4FrNkeW4F1Cz8MIbLuWfKvDa_4v_3FstMA3GODZWH\ - BVIiuNFay2ovCfGFyykwe47dF_47g_OM5AkJc_teE5MN8lPh9V5zYCy3ON3zZ3acFPJMOPTdbU56xD\ - eFe7lil6DmV4JU9A52t5ZkJILFaIuxxXJUIDmqpPTvHkggh_QOj9C2US9bgg5b543JwT4j-HbDp51L\ - dDB4k3azOssT1ddtoAuuDOctnraMKUtqffJXexxfwA1uM6EIofSrK5v11xwgTciL9xDXAvav_G2buP\ - ol1bjGLa2t0Q" - .to_string(), - )) - .expect("failed to deserialize"), - ) { - Err(ClaimsVerificationError::InvalidAudience(_)) => {} - other => panic!("unexpected result: {:?}", other), - } - - // Multiple audiences, where one is a match (allowed) - verifier - .clone() - .set_other_audience_verifier_fn(|aud| **aud == "aud1" || **aud == "aud2") - .verified_claims( - serde_json::from_value::(serde_json::Value::String( - "eyJhbGciOiJSUzI1NiJ9.eyJhdWQiOlsiYXVkMSIsIm15X2NsaWVudCIsImF1ZDIiXSwiaXNzIjoia\ - HR0cHM6Ly9leGFtcGxlLmNvbSIsInBheWxvYWQiOiJoZWxsbyB3b3JsZCJ9.N9ibisEe0kKLe1GDWM\ - ON3PmYqbL73dag-loM8pjKJNinF9SB7n4JuSu4FrNkeW4F1Cz8MIbLuWfKvDa_4v_3FstMA3GODZWH\ - BVIiuNFay2ovCfGFyykwe47dF_47g_OM5AkJc_teE5MN8lPh9V5zYCy3ON3zZ3acFPJMOPTdbU56xD\ - eFe7lil6DmV4JU9A52t5ZkJILFaIuxxXJUIDmqpPTvHkggh_QOj9C2US9bgg5b543JwT4j-HbDp51L\ - dDB4k3azOssT1ddtoAuuDOctnraMKUtqffJXexxfwA1uM6EIofSrK5v11xwgTciL9xDXAvav_G2buP\ - ol1bjGLa2t0Q" - .to_string(), - )).expect("failed to deserialize"), - ).expect("verification should succeed"); - - // Multiple audiences, where none is a match - match verifier.verified_claims( - serde_json::from_value::(serde_json::Value::String( - "eyJhbGciOiJSUzI1NiJ9.eyJhdWQiOlsiYXVkMSIsImF1ZDIiXSwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlL\ - mNvbSIsInBheWxvYWQiOiJoZWxsbyB3b3JsZCJ9.YmFkX2hhc2g" - .to_string(), - )).expect("failed to deserialize"), - ) { - Err(ClaimsVerificationError::InvalidAudience(_)) => {}, - other => panic!("unexpected result: {:?}", other), - } - - // Disable signature check. - verifier - .clone() - .require_signature_check(false) - .verified_claims( - serde_json::from_value::(serde_json::Value::String( - "eyJhbGciOiJSUzI1NiJ9.eyJhdWQiOlsibXlfY2xpZW50Il0sImlzcyI6Imh0dHBzOi8vZXhhbXBsZ\ - S5jb20iLCJwYXlsb2FkIjoiaGVsbG8gd29ybGQifQ.YmFkX2hhc2g" - .to_string(), - )).expect("failed to deserialize"), - ).expect("verification should succeed"); - - // "none" algorithm (unsigned JWT). - match verifier.verified_claims( - serde_json::from_value::(serde_json::Value::String( - "eyJhbGciOiJub25lIn0.eyJhdWQiOlsibXlfY2xpZW50Il0sImlzcyI6Imh0dHBzOi8vZXhhbXBsZ\ - S5jb20iLCJwYXlsb2FkIjoiaGVsbG8gd29ybGQifQ." - .to_string(), - )) - .expect("failed to deserialize"), - ) { - Err(ClaimsVerificationError::NoSignature) => {} - other => panic!("unexpected result: {:?}", other), - } - - let valid_rs256_jwt = - serde_json::from_value::(serde_json::Value::String( - "eyJhbGciOiJSUzI1NiJ9.eyJhdWQiOlsibXlfY2xpZW50Il0sImlzcyI6Imh0dHBzOi8vZXhhbXBsZ\ - S5jb20iLCJwYXlsb2FkIjoiaGVsbG8gd29ybGQifQ.UZ7vmAsDmOBzeB6e2_0POUfyhMRZKM6WSKz3\ - jB2QdmO-eZ9605EzhkJufJQ8515ryWnHv-gUHtZHQi3zilrzhBwvE2cVP83Gv2XIL1EKaMMmfISeEB\ - ShWez_FvqxN_bamh5yTROhWmoZTmof-MweBCHgINcsEd7K4e_BHHgq3aaRBpvSFlL_z4l_1NwNcTBo\ - kqjNScKZITk42AbsSuGR39L94BWLhz6WXQZ_Sn6R1Ro6roOm1b7E82jJiQEtlseQiCCvPR2JJ6LgW6\ - XTMzQ0vCqSh1A7U_IBDsjY_yag8_X3xxFh2URCtHJ47ZSjqfv6hq7OAq8tmVecOVgfIvABOg" - .to_string(), - )) - .expect("failed to deserialize"); - // Default algs + RS256 -> allowed - verifier - .verified_claims(valid_rs256_jwt.clone()) - .expect("verification should succeed"); - - let verifier_with_client_secret = CoreJwtClaimsVerifier::new( - client_id.clone(), - issuer.clone(), - CoreJsonWebKeySet::new(vec![]), - ) - .set_client_secret(ClientSecret::new("my_secret".to_string())); - let valid_hs256_jwt = - serde_json::from_value::(serde_json::Value::String( - "eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOlsibXlfY2xpZW50Il0sImlzcyI6Imh0dHBzOi8vZXhhbXBsZ\ - S5jb20iLCJwYXlsb2FkIjoiaGVsbG8gd29ybGQifQ.dTXvSWen74_rC4oiWw0ziLZNe4KZk8Jw2VZe\ - N6vLCDo" - .to_string(), - )) - .expect("failed to deserialize"); - - // Default algs + HS256 -> disallowed - match verifier_with_client_secret.verified_claims(valid_hs256_jwt.clone()) { - Err(ClaimsVerificationError::SignatureVerification( - SignatureVerificationError::DisallowedAlg(_), - )) => {} - other => panic!("unexpected result: {:?}", other), - } - - // none algs + RS256 -> allowed - verifier - .clone() - .allow_any_alg() - .verified_claims(valid_rs256_jwt.clone()) - .expect("verification should succeed"); - - // none algs + HS256 -> allowed - verifier_with_client_secret - .clone() - .allow_any_alg() - .verified_claims(valid_hs256_jwt.clone()) - .expect("verification should succeed"); - - // none algs + none -> disallowed - match verifier.clone().allow_any_alg().verified_claims( - serde_json::from_value::(serde_json::Value::String( - "eyJhbGciOiJub25lIn0.eyJhdWQiOlsibXlfY2xpZW50Il0sImlzcyI6Imh0dHBzOi8vZXhhbXBsZ\ - S5jb20iLCJwYXlsb2FkIjoiaGVsbG8gd29ybGQifQ." - .to_string(), - )) - .expect("failed to deserialize"), - ) { - Err(ClaimsVerificationError::NoSignature) => {} - other => panic!("unexpected result: {:?}", other), - } - - // HS256 + no client secret -> disallowed - match verifier - .clone() - .allow_any_alg() - .verified_claims(valid_hs256_jwt.clone()) - { - Err(ClaimsVerificationError::SignatureVerification( - SignatureVerificationError::DisallowedAlg(_), - )) => {} - other => panic!("unexpected result: {:?}", other), - } - - // HS256 + valid signature - verifier_with_client_secret - .clone() - .set_allowed_algs(vec![CoreJwsSigningAlgorithm::HmacSha256]) - .verified_claims(valid_hs256_jwt) - .expect("verification should succeed"); - - // HS256 + invalid signature - match verifier_with_client_secret - .clone() - .set_allowed_algs(vec![CoreJwsSigningAlgorithm::HmacSha256]) - .verified_claims( - serde_json::from_value::(serde_json::Value::String( - "eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOlsibXlfY2xpZW50Il0sImlzcyI6Imh0dHBzOi8vZXhhbXBsZ\ - S5jb20iLCJwYXlsb2FkIjoiaGVsbG8gd29ybGQifQ.dTXvSWen74_rC4oiWw0ziLZNe4KZk8Jw2VZe\ - N6vLCEo" - .to_string(), - )).expect("failed to deserialize") - ) - { - Err(ClaimsVerificationError::SignatureVerification( - SignatureVerificationError::CryptoError(_), - )) => {} - other => panic!("unexpected result: {:?}", other), - } - - // No public keys - match CoreJwtClaimsVerifier::new( - client_id.clone(), - issuer.clone(), - CoreJsonWebKeySet::new(vec![]), - ) - .verified_claims(valid_rs256_jwt.clone()) - { - Err(ClaimsVerificationError::SignatureVerification( - SignatureVerificationError::NoMatchingKey, - )) => {} - other => panic!("unexpected result: {:?}", other), - } - - let kid = JsonWebKeyId::new("bilbo.baggins@hobbiton.example".to_string()); - let n = Base64UrlEncodedBytes::new(vec![ - 159, 129, 15, 180, 3, 130, 115, 208, 37, 145, 228, 7, 63, 49, 210, 182, 0, 27, 130, - 206, 219, 77, 146, 240, 80, 22, 93, 71, 207, 202, 184, 163, 196, 28, 183, 120, 172, - 117, 83, 121, 63, 142, 249, 117, 118, 141, 26, 35, 116, 216, 113, 37, 100, 195, 188, - 215, 123, 158, 164, 52, 84, 72, 153, 64, 124, 255, 0, 153, 146, 10, 147, 26, 36, 196, - 65, 72, 82, 171, 41, 189, 176, 169, 92, 6, 83, 243, 108, 96, 230, 11, 249, 11, 98, 88, - 221, 165, 111, 55, 4, 123, 165, 194, 209, 208, 41, 175, 156, 157, 64, 186, 199, 170, - 65, 199, 138, 13, 209, 6, 138, 221, 105, 158, 128, 143, 234, 1, 30, 161, 68, 29, 138, - 79, 123, 180, 233, 123, 227, 159, 85, 241, 221, 212, 78, 156, 75, 163, 53, 21, 151, 3, - 212, 211, 75, 96, 62, 101, 20, 122, 79, 35, 214, 211, 192, 153, 108, 117, 237, 238, - 132, 106, 130, 209, 144, 174, 16, 120, 60, 150, 28, 240, 56, 122, 237, 33, 6, 210, 208, - 85, 91, 111, 217, 55, 250, 213, 83, 83, 135, 224, 255, 114, 255, 190, 120, 148, 20, 2, - 176, 184, 34, 234, 42, 116, 182, 5, 140, 29, 171, 249, 179, 74, 118, 203, 99, 184, 127, - 170, 44, 104, 71, 184, 226, 131, 127, 255, 145, 24, 110, 107, 28, 20, 145, 28, 249, - 137, 168, 144, 146, 168, 28, 230, 1, 221, 172, 211, 249, 207, - ]); - let e = Base64UrlEncodedBytes::new(vec![1, 0, 1]); - - // Wrong key type (symmetric key) - match CoreJwtClaimsVerifier::new( - client_id.clone(), - issuer.clone(), - CoreJsonWebKeySet::new(vec![CoreJsonWebKey { - kty: CoreJsonWebKeyType::Symmetric, - use_: Some(CoreJsonWebKeyUse::Signature), - kid: Some(kid.clone()), - n: None, - e: None, - k: Some(Base64UrlEncodedBytes::new(vec![1, 2, 3, 4])), - crv: None, - x: None, - y: None, - d: None, - #[cfg(feature = "jwk-alg")] - alg: None, - }]), - ) - .verified_claims(valid_rs256_jwt.clone()) - { - Err(ClaimsVerificationError::SignatureVerification( - SignatureVerificationError::NoMatchingKey, - )) => {} - other => panic!("unexpected result: {:?}", other), - } - - // Correct public key, but with signing disallowed - match CoreJwtClaimsVerifier::new( - client_id.clone(), - issuer.clone(), - CoreJsonWebKeySet::new(vec![CoreJsonWebKey { - kty: CoreJsonWebKeyType::RSA, - use_: Some(CoreJsonWebKeyUse::Encryption), - kid: Some(kid), - n: Some(n), - e: Some(e), - k: None, - crv: None, - x: None, - y: None, - d: None, - #[cfg(feature = "jwk-alg")] - alg: None, - }]), - ) - .verified_claims(valid_rs256_jwt.clone()) - { - Err(ClaimsVerificationError::SignatureVerification( - SignatureVerificationError::NoMatchingKey, - )) => {} - other => panic!("unexpected result: {:?}", other), - } - - // Wrong key ID - match verifier.verified_claims( - serde_json::from_value::(serde_json::Value::String( - "eyJhbGciOiJSUzI1NiIsImtpZCI6Indyb25nX2tleSJ9.eyJhdWQiOlsibXlfY2xpZW50Il0sImlzcyI6I\ - mh0dHBzOi8vZXhhbXBsZS5jb20iLCJwYXlsb2FkIjoiaGVsbG8gd29ybGQifQ.lVLomyIyO8WmyS1VZWPu\ - cGhRTUyK9RCw90fJC5CfDWUCgt1CBn-aP_ieWWBGfjb4ccR4dl57OYxdLl0Day8QN5pTCBud9QKpQ0rKQX\ - K8eBlOW8uSosx8q5pwU_bRyy-XuKJiPlDCOwTEHOp_hOgZFGjoN27MH3Xm8kc0iT3PgyqQ46-wsqHY9S02\ - hdJORX7vqYwQLZF8_k_L8K0IG_dC-1Co0g5oAf37oVSdl8hE-ScQ9K-AiSpS-cGYyldbMhyKNDL3ry2cuI\ - EUgYSIznkVFuM7RrEdNK222z5PF11ijYx-TM7BIDggbcIyJm-UqpmvVaJImmj5FNkMzuHYznLtdg" - .to_string(), - )).expect("failed to deserialize") - ) { - Err(ClaimsVerificationError::SignatureVerification( - SignatureVerificationError::NoMatchingKey, - )) => {} - other => panic!("unexpected result: {:?}", other), - } - - // Client secret + public key - verifier - .clone() - .set_client_secret(ClientSecret::new("my_secret".to_string())) - .verified_claims(valid_rs256_jwt.clone()) - .expect("verification should succeed"); - - // Multiple matching public keys: no KID specified - match CoreJwtClaimsVerifier::new( - client_id.clone(), - issuer.clone(), - CoreJsonWebKeySet::new(vec![rsa_key.clone(), rsa_key.clone()]), - ) - .verified_claims(valid_rs256_jwt.clone()) - { - Err(ClaimsVerificationError::SignatureVerification( - SignatureVerificationError::AmbiguousKeyId(_), - )) => {} - other => panic!("unexpected result: {:?}", other), - } - - // Multiple matching public keys: KID specified - match CoreJwtClaimsVerifier::new( - client_id, - issuer, - CoreJsonWebKeySet::new(vec![rsa_key.clone(), rsa_key]), - ).verified_claims( - serde_json::from_value::(serde_json::Value::String( - "eyJhbGciOiJSUzI1NiIsImtpZCI6ImJpbGJvLmJhZ2dpbnNAaG9iYml0b24uZXhhbXBsZSJ9.eyJhdWQiO\ - lsibXlfY2xpZW50Il0sImlzcyI6Imh0dHBzOi8vZXhhbXBsZS5jb20iLCJwYXlsb2FkIjoiaGVsbG8gd29\ - ybGQifQ.jH0v2fQGvH2MD0jn5pQP6W6AF5rJlizyofdyRUIt7E3GraGA1LYDiLAVIfhST3uwJopP-TgtBk\ - zc-zyJSvgTR63S8iI1YlHypItpx7r4I9ydzo8GSN5RrZudcU2esY4uEnLbVl17ZVNu4IyTExeKJ0sPM0Hj\ - qkOA4XaP2cJwsK-bookNHSA8NRE6adRMrHAKJbor5jrGjpkZAKHbnQFK-wu-nEV_OjS9jpN_FboRZVcDTZ\ - GFzeFbqFqHdRn6UWPFnVpVnUhih16UjNH1om6gwc0uFoPWTDxJlXQCFbHMhZtgCbUkXQBH7twPMc4YUziw\ - S8GIRKCcXjdrP5oyxmcitQ" - .to_string(), - )).expect("failed to deserialize") - ) { - Err(ClaimsVerificationError::SignatureVerification( - SignatureVerificationError::AmbiguousKeyId(_), - )) => {} - other => panic!("unexpected result: {:?}", other), - } - - // RS256 + valid signature - verifier - .verified_claims(valid_rs256_jwt) - .expect("verification should succeed"); - - // RS256 + invalid signature - match verifier.verified_claims( - serde_json::from_value::(serde_json::Value::String( - "eyJhbGciOiJSUzI1NiJ9.eyJhdWQiOlsibXlfY2xpZW50Il0sImlzcyI6Imh0dHBzOi8vZXhhbXBsZS5jb\ - 20iLCJwYXlsb2FkIjoiaGVsbG8gd29ybGQifQ.YmFkX2hhc2g" - .to_string(), - )).expect("failed to deserialize"), - ) { - Err(ClaimsVerificationError::SignatureVerification( - SignatureVerificationError::CryptoError(_), - )) => {} - other => panic!("unexpected result: {:?}", other), - } - } - - type CoreIdTokenJwt = JsonWebToken< - CoreJweContentEncryptionAlgorithm, - CoreJwsSigningAlgorithm, - CoreJsonWebKeyType, - CoreIdTokenClaims, - JsonWebTokenJsonPayloadSerde, - >; - - #[test] - fn test_id_token_verified_claims() { - let rsa_key = serde_json::from_str::(TEST_RSA_PUB_KEY) - .expect("deserialization failed"); - - let client_id = ClientId::new("my_client".to_string()); - let issuer = IssuerUrl::new("https://example.com".to_string()).unwrap(); - let mock_current_time = AtomicUsize::new(1544932149); - let mock_is_valid_issue_time = AtomicBool::new(true); - // Extra scope needed to ensure closures are destroyed before the values they borrow. - { - let public_client_verifier = CoreIdTokenVerifier::new_public_client( - client_id.clone(), - issuer.clone(), - CoreJsonWebKeySet::new(vec![rsa_key.clone()]), - ) - .set_time_fn(|| { - timestamp_to_utc(&Timestamp::Seconds( - mock_current_time.load(Ordering::Relaxed).into(), - )) - .unwrap() - }) - .set_issue_time_verifier_fn(|_| { - if mock_is_valid_issue_time.load(Ordering::Relaxed) { - Ok(()) - } else { - Err("Invalid iat claim".to_string()) - } - }); - - let insecure_verifier = CoreIdTokenVerifier::new_insecure_without_verification() - .set_time_fn(|| { - timestamp_to_utc(&Timestamp::Seconds( - mock_current_time.load(Ordering::Relaxed).into(), - )) - .unwrap() - }); - - // This JWTs below have an issue time of 1544928549 and an expiration time of 1544932149. - - let test_jwt_without_nonce = - serde_json::from_value::(serde_json::Value::String( - "eyJhbGciOiJSUzI1NiJ9.eyJhdWQiOlsibXlfY2xpZW50Il0sImlzcyI6Imh0dHBzOi8vZXhhbXBsZ\ - S5jb20iLCJzdWIiOiJzdWJqZWN0IiwiZXhwIjoxNTQ0OTMyMTQ5LCJpYXQiOjE1NDQ5Mjg1NDl9.nN\ - aTxNwclnTHd1Q9POkddm5wB1w3wJ-gwQWHomhimttk3SWQTLhxI0SSjWrHahGxlfkjufJlSyt-t_VO\ - SdcROvIYZTDznDfFZz3oSOev-p9XiZ-EZTS-U6N11Y923sDQjbTMeukz1F3ZFEfn5Mv2xjdEoJccCe\ - 7SaGuDmVqMqTLXMtsw9NCE_KDd0oKSwDzbJIBBPEfG3JjbKg0Dln7ENHg9wzoNFQzPXrkKzjneBgD3\ - vuwFCV5y-e8xUBdLaLZF1kdkDZJIA48uRROLlWjsM8pEptosA5QK07luQCZNqcaZWEczoGXeQs8PyA\ - zkNV7JEmti3bJnWSN-ud4cFU0LiQ" - .to_string(), - )) - .expect("failed to deserialize"); - - // Invalid JWT claims - match public_client_verifier.verified_claims( - &serde_json::from_value::(serde_json::Value::String( - "eyJhbGciOiJSUzI1NiJ9.eyJhdWQiOlsibXlfY2xpZW50Il0sImlzcyI6Imh0dHBzOi8vYXR0YWNrZ\ - XIuY29tIiwic3ViIjoic3ViamVjdCIsImV4cCI6MTU0NDkzMjE0OSwiaWF0IjoxNTQ0OTI4NTQ5LCJ\ - ub25jZSI6InRoZV9ub25jZSIsImFjciI6InRoZV9hY3IifQ.Pkicxk0dTU5BkSxgqTON6lE7A7ir3l\ - aADRyoeRoCNDX3AOx7BXCbfzbda6HJiPskN2nu56w0q-0OdkDSIHls-2xTUlLEJv2Bv0BLYwV5ZVJ8\ - hoc-rTd0_oLUb5NzyD80RyVByjVMK8bh6cwysTnr8QDxsEiFZbFo3mVJob2yjPZnNOdcNJWPcVVueP\ - 8vqMJnx5kHih1gKZpWj_dMN9b2AW6zVLOInW3Ox__gx6fsFFz7rjxItG-PTY_OQMzthqeHUyq4o9y7\ - Jv8mB_jFkTZGVKHTPpObHV-qptJ_rnlwvF_mP5GARBLng-4Yd7nmSr31onYL48QDjGOrwPqQ-IyaCQ" - .to_string(), - )) - .expect("failed to deserialize"), |_: Option<&Nonce>| Ok(())) { - Err(ClaimsVerificationError::InvalidIssuer(_)) => {} - other => panic!("unexpected result: {:?}", other), - } - - // TODO: disallowed algs - - // Expired token - mock_current_time.store(1544928549 + 3600, Ordering::Relaxed); - match public_client_verifier - .verified_claims(&test_jwt_without_nonce, |_: Option<&Nonce>| Ok(())) - { - Err(ClaimsVerificationError::Expired(_)) => {} - other => panic!("unexpected result: {:?}", other), - } - mock_current_time.store(1544928549 + 1, Ordering::Relaxed); - - // Invalid issue time - mock_is_valid_issue_time.store(false, Ordering::Relaxed); - match public_client_verifier - .verified_claims(&test_jwt_without_nonce, |_: Option<&Nonce>| Ok(())) - { - Err(ClaimsVerificationError::Expired(_)) => {} - other => panic!("unexpected result: {:?}", other), - } - mock_is_valid_issue_time.store(true, Ordering::Relaxed); - - let valid_nonce = Nonce::new("the_nonce".to_string()); - - // Successful verification w/o checking nonce - public_client_verifier - .verified_claims(&test_jwt_without_nonce, |_: Option<&Nonce>| Ok(())) - .expect("verification should succeed"); - - // Missing nonce - match public_client_verifier.verified_claims(&test_jwt_without_nonce, &valid_nonce) { - Err(ClaimsVerificationError::InvalidNonce(_)) => {} - other => panic!("unexpected result: {:?}", other), - } - - // Missing nonce w/ closure - match public_client_verifier.verified_claims( - &test_jwt_without_nonce, - |nonce: Option<&Nonce>| { - if nonce.iter().any(|n| n.secret() == valid_nonce.secret()) { - Ok(()) - } else { - Err("invalid nonce".to_string()) - } - }, - ) { - Err(ClaimsVerificationError::InvalidNonce(_)) => {} - other => panic!("unexpected result: {:?}", other), - } - - let test_jwt_with_nonce = - serde_json::from_value::(serde_json::Value::String( - "eyJhbGciOiJSUzI1NiJ9.eyJhdWQiOlsibXlfY2xpZW50Il0sImlzcyI6Imh0dHBzOi8vZXhhbXBsZ\ - S5jb20iLCJzdWIiOiJzdWJqZWN0IiwiZXhwIjoxNTQ0OTMyMTQ5LCJpYXQiOjE1NDQ5Mjg1NDksIm5\ - vbmNlIjoidGhlX25vbmNlIiwiYWNyIjoidGhlX2FjciIsImF1dGhfdGltZSI6MTU0NDkyODU0OH0.W\ - XA7SS9aMh_6rvBEgQce5D2J84OqphmmnCLGgEKRTN5G-UuQTNOBp8VS5_4f3xgzMEEMvGJJauJoALk\ - muUeHB-N_ESrkmB3tgDzBSYBa7kuYPHUPYpdjZM2UVolqI9RYyHaWwKjL_Io5YyAazB5lH5ibPaiBl\ - UNKGs3cmVsEB22UGMFKM6cek7GinrHQe_aJQsMU839-c2zzlEyFSeI8QBphQtG6AN82IPkNRv8QWmw\ - ZjUiB5a-W73Z3gURYMNs7f32BjAUNoJzW0Qj34vzD2djoSHhltE0wHKBzPqGhUM1Y3A-a3q-LS2g1h\ - 6qgXb_KQ_Mmok8v8ld0cW_aYRLfNg" - .to_string(), - )) - .expect("failed to deserialize"); - - // Invalid nonce - match public_client_verifier.verified_claims( - &test_jwt_with_nonce, - &Nonce::new("different_nonce".to_string()), - ) { - Err(ClaimsVerificationError::InvalidNonce(_)) => {} - other => panic!("unexpected result: {:?}", other), - } - - // Invalid AuthenticationContextClass reference - match public_client_verifier - .clone() - .set_auth_context_verifier_fn(|acr| { - assert_eq!(**acr.unwrap(), "the_acr"); - Err("Invalid acr claim".to_string()) - }) - .verified_claims(&test_jwt_with_nonce, &valid_nonce) - { - Err(ClaimsVerificationError::InvalidAuthContext(_)) => {} - other => panic!("unexpected result: {:?}", other), - } - - let test_jwt_without_auth_time = - serde_json::from_value::(serde_json::Value::String( - "eyJhbGciOiJSUzI1NiJ9.eyJhdWQiOlsibXlfY2xpZW50Il0sImlzcyI6Imh0dHBzOi8vZXhhbXBsZ\ - S5jb20iLCJzdWIiOiJzdWJqZWN0IiwiZXhwIjoxNTQ0OTMyMTQ5LCJpYXQiOjE1NDQ5Mjg1NDksIm5\ - vbmNlIjoidGhlX25vbmNlIiwiYWNyIjoidGhlX2FjciJ9.c_lU1VRasTg0mB4lwdOzbzvFS_XShMLN\ - lAPUpHBaMtCSPtI71L2x3hIByfkqIrAED-Qc_am2gNJ20bifidlkTOO6nyaBrJuaSjwT8aqajEbXon\ - 5JFswwPvqCIWjd0eV5dXC1MZunpd7ANXSC7Qw16v3m_crc9wcI_fLFCzuAKrWYokGvNy0gr1CxcgVg\ - aE9qR0eqaatetzCuaOJhYOq4njrRlGZWtbj5Q56q3zhxJ_yS8K8gv1QcB4sHjUyXIj21jzjUD87zVG\ - dJsn8E-nFJSltBdQhEaLksTBH6ZZhkeGicQ8cEPnNeS4L1vfVyAd_cjl64JHLmzw8RUp8XuoF9nA" - .to_string(), - )) - .expect("failed to deserialize"); - - // Missing auth_time (ok) - public_client_verifier - .verified_claims(&test_jwt_without_auth_time, |_: Option<&Nonce>| Ok(())) - .expect("verification should succeed"); - - // Missing auth_time (error) - match public_client_verifier - .clone() - .set_auth_time_verifier_fn(|auth_time| { - assert!(auth_time.is_none()); - Err("Invalid auth_time claim".to_string()) - }) - .verified_claims(&test_jwt_without_auth_time, |_: Option<&Nonce>| Ok(())) - { - Err(ClaimsVerificationError::InvalidAuthTime(_)) => {} - other => panic!("unexpected result: {:?}", other), - } - - // Invalid auth_time - match public_client_verifier - .clone() - .set_auth_time_verifier_fn(|auth_time| { - assert_eq!( - auth_time.unwrap(), - timestamp_to_utc(&Timestamp::Seconds(1544928548.into())).unwrap(), - ); - Err("Invalid auth_time claim".to_string()) - }) - .verified_claims(&test_jwt_with_nonce, &valid_nonce) - { - Err(ClaimsVerificationError::InvalidAuthTime(_)) => {} - other => panic!("unexpected result: {:?}", other), - } - - // Successful verification with nonce, acr, and auth_time specified (no expected Nonce) - public_client_verifier - .verified_claims(&test_jwt_with_nonce, |_: Option<&Nonce>| Ok(())) - .expect("verification should succeed"); - insecure_verifier - .verified_claims(&test_jwt_with_nonce, |_: Option<&Nonce>| Ok(())) - .expect("verification should succeed"); - - // Successful verification with nonce, acr, and auth_time specified (w/ expected Nonce) - public_client_verifier - .verified_claims(&test_jwt_with_nonce, &valid_nonce) - .expect("verification should succeed"); - insecure_verifier - .verified_claims(&test_jwt_with_nonce, &valid_nonce) - .expect("verification should succeed"); - - // Successful verification with nonce, acr, and auth_time specified (w/ closure) - public_client_verifier - .verified_claims(&test_jwt_with_nonce, |nonce: Option<&Nonce>| { - if nonce.iter().any(|n| n.secret() == valid_nonce.secret()) { - Ok(()) - } else { - Err("invalid nonce".to_string()) - } - }) - .expect("verification should succeed"); - insecure_verifier - .verified_claims(&test_jwt_with_nonce, |nonce: Option<&Nonce>| { - if nonce.iter().any(|n| n.secret() == valid_nonce.secret()) { - Ok(()) - } else { - Err("invalid nonce".to_string()) - } - }) - .expect("verification should succeed"); - - // HS256 w/ default algs - let test_jwt_hs256 = - serde_json::from_value::(serde_json::Value::String( - "eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOlsibXlfY2xpZW50Il0sImlzcyI6Imh0dHBzOi8vZXhhbXBsZ\ - S5jb20iLCJzdWIiOiJzdWJqZWN0IiwiZXhwIjoxNTQ0OTMyMTQ5LCJpYXQiOjE1NDQ5Mjg1NDksIm5\ - vbmNlIjoidGhlX25vbmNlIn0.xUnSwSbcHsHWyJxwKGg69BIo_CktcyN5BVulGDb_QzE" - .to_string(), - )) - .expect("failed to deserialize"); - let private_client_verifier = CoreIdTokenVerifier::new_confidential_client( - client_id.clone(), - ClientSecret::new("my_secret".to_string()), - issuer.clone(), - CoreJsonWebKeySet::new(vec![rsa_key.clone()]), - ) - .set_time_fn(|| { - timestamp_to_utc(&Timestamp::Seconds( - mock_current_time.load(Ordering::Relaxed).into(), - )) - .unwrap() - }); - match private_client_verifier.verified_claims(&test_jwt_hs256, &valid_nonce) { - Err(ClaimsVerificationError::SignatureVerification(_)) => {} - other => panic!("unexpected result: {:?}", other), - } - insecure_verifier - .clone() - .verified_claims(&test_jwt_hs256, &valid_nonce) - .expect("verification should succeed"); - - // HS256 w/ set_allowed_algs - private_client_verifier - .clone() - .set_allowed_algs(vec![CoreJwsSigningAlgorithm::HmacSha256]) - .verified_claims(&test_jwt_hs256, &valid_nonce) - .expect("verification should succeed"); - - // HS256 w/ allow_any_alg - private_client_verifier - .clone() - .allow_any_alg() - .verified_claims(&test_jwt_hs256, &valid_nonce) - .expect("verification should succeed"); - - // Invalid signature - let private_client_verifier_with_other_secret = - CoreIdTokenVerifier::new_confidential_client( - client_id, - ClientSecret::new("other_secret".to_string()), - issuer, - CoreJsonWebKeySet::new(vec![rsa_key]), - ) - .allow_any_alg() - .set_time_fn(|| { - timestamp_to_utc(&Timestamp::Seconds( - mock_current_time.load(Ordering::Relaxed).into(), - )) - .unwrap() - }); - match private_client_verifier_with_other_secret - .verified_claims(&test_jwt_hs256, &valid_nonce) - { - Err(ClaimsVerificationError::SignatureVerification(_)) => {} - other => panic!("unexpected result: {:?}", other), - } - - // Invalid signature w/ signature check disabled - private_client_verifier_with_other_secret - .clone() - .insecure_disable_signature_check() - .verified_claims(&test_jwt_hs256, &valid_nonce) - .expect("verification should succeed"); - }; - } - - #[test] - fn test_new_id_token() { - let client_id = ClientId::new("my_client".to_string()); - let issuer = IssuerUrl::new("https://example.com".to_string()).unwrap(); - let nonce = Nonce::new("the_nonce".to_string()); - let rsa_priv_key = CoreRsaPrivateSigningKey::from_pem(TEST_RSA_PRIV_KEY, None).unwrap(); - - let id_token = CoreIdToken::new( - CoreIdTokenClaims::new( - issuer.clone(), - vec![Audience::new((*client_id).clone())], - Utc.timestamp_opt(1544932149, 0) - .single() - .expect("valid timestamp"), - Utc.timestamp_opt(1544928549, 0) - .single() - .expect("valid timestamp"), - StandardClaims::new(SubjectIdentifier::new("subject".to_string())), - Default::default(), - ) - .set_nonce(Some(nonce.clone())) - .set_auth_context_ref(Some(AuthenticationContextClass::new("the_acr".to_string()))) - .set_auth_time(Some( - Utc.timestamp_opt(1544928548, 0) - .single() - .expect("valid timestamp"), - )), - &rsa_priv_key, - CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256, - Some(&AccessToken::new("the_access_token".to_string())), - Some(&AuthorizationCode::new( - "the_authorization_code".to_string(), - )), - ) - .unwrap(); - - let serialized_jwt: serde_json::Value = serde_json::to_value(&id_token).unwrap(); - let expected_serialized_jwt = - "eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJodHRwczovL2V4YW1wbGUuY29tIiwiYXVkIjpbIm15X2NsaWVudCJdL\ - CJleHAiOjE1NDQ5MzIxNDksImlhdCI6MTU0NDkyODU0OSwiYXV0aF90aW1lIjoxNTQ0OTI4NTQ4LCJub25jZSI\ - 6InRoZV9ub25jZSIsImFjciI6InRoZV9hY3IiLCJhdF9oYXNoIjoiWjNJQUNVR00tbXhIV3lZUXZpSzhFUSIsI\ - mNfaGFzaCI6Imo2OW1CZmFIbmRMM1Y1RmNoak9LVXciLCJzdWIiOiJzdWJqZWN0In0.CHCWFcIqbCZhZwZH4oY\ - _mlcRy5aUQQtlNI0VHNYxiILn9ppRHLL4Bn_LMn9VP8tGXkfZWxCgP25ZTyBXXKfk0fQvnukVdyM0bCOpQbiBg\ - 5gB9c46l_f-ZznDoHWonpnKky2Gmzk3ocb3TCUQ9GSeRXAzRdRNWTT0ElWNBsLWU4j2IIdnghM78gkXwOC76Rk\ - pshgB73ubtuHGdIf5L9Ec3hifHlVjzKuvedAM4SIOjdBOelgtBlF3463ufX_Ut91CjP5TzLMsuK3Lh_vyo8ttn\ - S41rBDuetR2ENvR0yj5RjkX_SPY3V0yCW8_NPPu1CHu_1oL0Nma0ohCbF3vnUJcwg"; - assert_eq!(expected_serialized_jwt, serialized_jwt.as_str().unwrap()); - - let rsa_pub_key = serde_json::from_str::(TEST_RSA_PUB_KEY) - .expect("deserialization failed"); - - let mock_current_time = AtomicUsize::new(1544932148); - let time_fn = || { - timestamp_to_utc(&Timestamp::Seconds( - mock_current_time.load(Ordering::Relaxed).into(), - )) - .unwrap() - }; - let verifier = CoreIdTokenVerifier::new_public_client( - client_id, - issuer, - CoreJsonWebKeySet::new(vec![rsa_pub_key]), - ) - .set_time_fn(time_fn); - let claims = id_token.claims(&verifier, &nonce).unwrap(); - let unverified = id_token - .claims( - &CoreIdTokenVerifier::new_insecure_without_verification().set_time_fn(time_fn), - &nonce, - ) - .unwrap(); - assert_eq!(claims, unverified); - } - - #[test] - fn test_user_info_verified_claims() { - let rsa_key = serde_json::from_str::(TEST_RSA_PUB_KEY) - .expect("deserialization failed"); - - let client_id = ClientId::new("my_client".to_string()); - let issuer = IssuerUrl::new("https://example.com".to_string()).unwrap(); - let sub = SubjectIdentifier::new("the_subject".to_string()); - - let verifier = CoreUserInfoVerifier::new( - client_id.clone(), - issuer.clone(), - CoreJsonWebKeySet::new(vec![rsa_key.clone()]), - Some(sub.clone()), - ); - - let json_claims = "{\ - \"sub\": \"the_subject\",\ - \"name\": \"Jane Doe\"\ - }"; - - // JSON response (default args) - assert_eq!( - CoreUserInfoClaims::from_json::( - json_claims.as_bytes(), - Some(&sub) - ) - .expect("verification should succeed") - .name() - .unwrap() - .iter() - .collect::>(), - vec![(None, &EndUserName::new("Jane Doe".to_string()))], - ); - - // Invalid subject - match CoreUserInfoClaims::from_json::( - json_claims.as_bytes(), - Some(&SubjectIdentifier::new("wrong_subject".to_string())), - ) { - Err(UserInfoError::ClaimsVerification(ClaimsVerificationError::InvalidSubject(_))) => {} - other => panic!("unexpected result: {:?}", other), - } - - let jwt_claims = - serde_json::from_value::(serde_json::Value::String( - "eyJhbGciOiJSUzI1NiJ9.eyJhdWQiOlsibXlfY2xpZW50Il0sImlzcyI6Imh0dHBzOi8vZXhhb\ - XBsZS5jb20iLCJzdWIiOiJ0aGVfc3ViamVjdCIsIm5hbWUiOiJKYW5lIERvZSJ9.aX7VpexLAd\ - 43HtC1cFTot3jmqsr105rB50mzTcS1TXzWcxLbqYf1K7Kf-S1oP-ZCL_dnL9-nu3iDK_vRa6xT\ - nGGt3I1JwhoIv6znSS3JOPT1wtekyD-sLcUwqsJHWBBiTSBwlmGG_kVRuGkBtXgVZ9aGlqg9u1\ - FlxvyGUJ5q1o9gdb8mKql5ojgsThTNo9qdW3lPIVsiDO-n4mMp4HuOp1re4ZDDkHxiExjtLQAV\ - kR4q3SlhJC2mkr4mw3_0a2AW52ocWDiwY_lPcdmohmwFaB8aHlivYLFnmKGQIatEW-KDaW5fFo\ - JYreNkplo4FvzXYyxgxAsqHjHMI8MZVEa1IA" - .to_string(), - )) - .expect("failed to deserialize"); - - // Valid JWT response (default args) - jwt_claims - .clone() - .claims(&verifier) - .expect("verification should succeed"); - - // JWT response with invalid signature - match serde_json::from_value::(serde_json::Value::String( - "eyJhbGciOiJSUzI1NiJ9.eyJhdWQiOlsibXlfY2xpZW50Il0sImlzcyI6Imh0dHBzOi8vZXhhb\ - XBsZS5jb20iLCJzdWIiOiJ0aGVfc3ViamVjdCIsIm5hbWUiOiJKYW5lIERvZSJ9.bX7VpexLAd\ - 43HtC1cFTot3jmqsr105rB50mzTcS1TXzWcxLbqYf1K7Kf-S1oP-ZCL_dnL9-nu3iDK_vRa6xT\ - nGGt3I1JwhoIv6znSS3JOPT1wtekyD-sLcUwqsJHWBBiTSBwlmGG_kVRuGkBtXgVZ9aGlqg9u1\ - FlxvyGUJ5q1o9gdb8mKql5ojgsThTNo9qdW3lPIVsiDO-n4mMp4HuOp1re4ZDDkHxiExjtLQAV\ - kR4q3SlhJC2mkr4mw3_0a2AW52ocWDiwY_lPcdmohmwFaB8aHlivYLFnmKGQIatEW-KDaW5fFo\ - JYreNkplo4FvzXYyxgxAsqHjHMI8MZVEa1IA" - .to_string(), - )) - .expect("failed to deserialize") - .claims(&verifier) - { - Err(ClaimsVerificationError::SignatureVerification( - SignatureVerificationError::CryptoError(_), - )) => {} - other => panic!("unexpected result: {:?}", other), - } - - // JWT response with invalid issuer claim (error) - match jwt_claims.clone().claims(&CoreUserInfoVerifier::new( - client_id.clone(), - IssuerUrl::new("https://attacker.com".to_string()).unwrap(), - CoreJsonWebKeySet::new(vec![rsa_key.clone()]), - Some(sub.clone()), - )) { - Err(ClaimsVerificationError::InvalidIssuer(_)) => {} - other => panic!("unexpected result: {:?}", other), - } - - // JWT response with invalid issuer claim (allowed) - jwt_claims - .clone() - .claims( - &CoreUserInfoVerifier::new( - client_id, - IssuerUrl::new("https://attacker.com".to_string()).unwrap(), - CoreJsonWebKeySet::new(vec![rsa_key.clone()]), - Some(sub.clone()), - ) - .require_issuer_match(false), - ) - .expect("verification should succeed"); - - // JWT response with invalid audience claim (error) - match jwt_claims.clone().claims(&CoreUserInfoVerifier::new( - ClientId::new("wrong_client".to_string()), - issuer.clone(), - CoreJsonWebKeySet::new(vec![rsa_key.clone()]), - Some(sub.clone()), - )) { - Err(ClaimsVerificationError::InvalidAudience(_)) => {} - other => panic!("unexpected result: {:?}", other), - } - - // JWT response with invalid audience claim (allowed) - jwt_claims - .claims( - &CoreUserInfoVerifier::new( - ClientId::new("wrong_client".to_string()), - issuer, - CoreJsonWebKeySet::new(vec![rsa_key]), - Some(sub), - ) - .require_audience_match(false), - ) - .expect("verification should succeed"); - } - - #[test] - fn test_new_user_info_claims() { - let claims = CoreUserInfoClaims::new( - StandardClaims { - sub: SubjectIdentifier::new("the_subject".to_string()), - name: Some(EndUserName::new("John Doe".to_string()).into()), - given_name: None, - family_name: None, - middle_name: None, - nickname: None, - preferred_username: None, - profile: None, - picture: None, - website: None, - email: None, - email_verified: None, - gender: None, - birthday: None, - birthdate: None, - zoneinfo: None, - locale: None, - phone_number: None, - phone_number_verified: None, - address: None, - updated_at: Some( - Utc.timestamp_opt(1544928548, 0) - .single() - .expect("valid timestamp"), - ), - }, - Default::default(), - ); - - assert_eq!( - "{\"sub\":\"the_subject\",\"name\":\"John Doe\",\"updated_at\":1544928548}", - serde_json::to_string(&claims).unwrap() - ); - - let rsa_priv_key = CoreRsaPrivateSigningKey::from_pem(TEST_RSA_PRIV_KEY, None).unwrap(); - let claims_jwt = CoreUserInfoJsonWebToken::new( - claims, - &rsa_priv_key, - CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256, - ) - .unwrap(); - assert_eq!( - "eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ0aGVfc3ViamVjdCIsIm5hbWUiOiJKb2huIERvZSIsInVwZGF0ZWRfY\ - XQiOjE1NDQ5Mjg1NDh9.nJ7Buckt_p_ACXkyVRCQLqyaW8KhDsk5H9Nu7PdNf4daEcEWm-lGjoSTAfAbDPgHAZ\ - 78knomgLgDxiGWrj1qdFTIEFep32I3q18VBP_DcMdyuQafipK6T98RgZFWP8YnxlxLPHeJQlRsdMpemHK4vxas\ - ZD4A4aIn0K7z5J9RvrR3L7DWnc3fJQ0VU2v5QLePyqNWnFxks5eyl8Ios8JrZhwr4Q8GES8Q4Iw8Sz6W9vYpHK\ - 2r1YdaACMM4g_TTtV91lpjn-Li2-HxW9NERdLvYvF6HwGIwbss26trp2yjNTARlxBUT6LR7y82oPIJKXIKL1GD\ - YeSLeErhb6oTQ0a5gQ", - serde_json::to_value(claims_jwt).unwrap().as_str().unwrap() - ); - } -} diff --git a/src/verification/mod.rs b/src/verification/mod.rs new file mode 100644 index 0000000..23f3a51 --- /dev/null +++ b/src/verification/mod.rs @@ -0,0 +1,893 @@ +use crate::jwt::{JsonWebToken, JsonWebTokenJsonPayloadSerde}; +use crate::user_info::UserInfoClaimsImpl; +use crate::{ + AdditionalClaims, Audience, AuthenticationContextClass, ClientId, ClientSecret, GenderClaim, + IdTokenClaims, IssuerUrl, JsonWebKey, JsonWebKeySet, JsonWebKeyType, JsonWebKeyUse, + JsonWebTokenAccess, JsonWebTokenAlgorithm, JsonWebTokenHeader, JweContentEncryptionAlgorithm, + JwsSigningAlgorithm, Nonce, SubjectIdentifier, +}; + +use chrono::{DateTime, Utc}; +use serde::de::DeserializeOwned; +use serde::Serialize; +use thiserror::Error; + +use std::collections::HashSet; +use std::fmt::Debug; +use std::marker::PhantomData; +use std::ops::Deref; +use std::sync::Arc; + +#[cfg(test)] +mod tests; + +pub(crate) trait AudiencesClaim { + fn audiences(&self) -> Option<&Vec>; +} + +pub(crate) trait IssuerClaim { + fn issuer(&self) -> Option<&IssuerUrl>; +} + +/// Error verifying claims. +#[derive(Clone, Debug, Error, PartialEq, Eq)] +#[non_exhaustive] +pub enum ClaimsVerificationError { + /// Claims have expired. + #[error("Expired: {0}")] + Expired(String), + /// Audience claim is invalid. + #[error("Invalid audiences: {0}")] + InvalidAudience(String), + /// Authorization context class reference (`acr`) claim is invalid. + #[error("Invalid authorization context class reference: {0}")] + InvalidAuthContext(String), + /// User authenticated too long ago. + #[error("Invalid authentication time: {0}")] + InvalidAuthTime(String), + /// Issuer claim is invalid. + #[error("Invalid issuer: {0}")] + InvalidIssuer(String), + /// Nonce is invalid. + #[error("Invalid nonce: {0}")] + InvalidNonce(String), + /// Subject claim is invalid. + #[error("Invalid subject: {0}")] + InvalidSubject(String), + /// No signature present but claims must be signed. + #[error("Claims must be signed")] + NoSignature, + /// An unexpected error occurred. + #[error("{0}")] + Other(String), + /// Failed to verify the claims signature. + #[error("Signature verification failed")] + SignatureVerification(#[source] SignatureVerificationError), + /// Unsupported argument or value. + #[error("Unsupported: {0}")] + Unsupported(String), +} + +/// Error verifying claims signature. +#[derive(Clone, Debug, Error, PartialEq, Eq)] +#[non_exhaustive] +pub enum SignatureVerificationError { + /// More than one key matches the supplied key constraints (e.g., key ID). + #[error("Ambiguous key identification: {0}")] + AmbiguousKeyId(String), + /// Invalid signature for the supplied claims and signing key. + #[error("Crypto error: {0}")] + CryptoError(String), + /// The supplied signature algorithm is disallowed by the verifier. + #[error("Disallowed signature algorithm: {0}")] + DisallowedAlg(String), + /// The supplied key cannot be used in this context. This may occur if the key type does not + /// match the signature type (e.g., an RSA key used to validate an HMAC) or the JWK usage + /// disallows signatures. + #[error("Invalid cryptographic key: {0}")] + InvalidKey(String), + /// The signing key needed for verifying the + /// [JSON Web Token](https://tools.ietf.org/html/rfc7519)'s signature/MAC could not be found. + /// This error can occur if the key ID (`kid`) specified in the JWT's + /// [JOSE header](https://tools.ietf.org/html/rfc7519#section-5) does not match the ID of any + /// key in the OpenID Connect provider's JSON Web Key Set (JWKS), typically retrieved from + /// the provider's [JWKS document]( + /// http://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata). To support + /// [rotation of asymmetric signing keys]( + /// http://openid.net/specs/openid-connect-core-1_0.html#RotateSigKeys), client applications + /// should consider refreshing the JWKS document (via + /// [`JsonWebKeySet::fetch`][crate::JsonWebKeySet::fetch]). + /// + /// This error can also occur if the identified + /// [JSON Web Key](https://tools.ietf.org/html/rfc7517) is of the wrong type (e.g., an RSA key + /// when the JOSE header specifies an ECDSA algorithm) or does not support signing. + #[error("No matching key found")] + NoMatchingKey, + /// Unsupported signature algorithm. + #[error("Unsupported signature algorithm: {0}")] + UnsupportedAlg(String), + /// An unexpected error occurred. + #[error("Other error: {0}")] + Other(String), +} + +// This struct is intentionally private. +#[derive(Clone)] +struct JwtClaimsVerifier<'a, JS, JT, JU, K> +where + JS: JwsSigningAlgorithm, + JT: JsonWebKeyType, + JU: JsonWebKeyUse, + K: JsonWebKey, +{ + allowed_algs: Option>, + aud_match_required: bool, + client_id: ClientId, + client_secret: Option, + iss_required: bool, + issuer: IssuerUrl, + is_signature_check_enabled: bool, + other_aud_verifier_fn: Arc bool + 'a + Send + Sync>, + signature_keys: JsonWebKeySet, +} +impl<'a, JS, JT, JU, K> JwtClaimsVerifier<'a, JS, JT, JU, K> +where + JS: JwsSigningAlgorithm, + JT: JsonWebKeyType, + JU: JsonWebKeyUse, + K: JsonWebKey, +{ + pub fn new( + client_id: ClientId, + issuer: IssuerUrl, + signature_keys: JsonWebKeySet, + ) -> Self { + JwtClaimsVerifier { + allowed_algs: Some([JS::rsa_sha_256()].iter().cloned().collect()), + aud_match_required: true, + client_id, + client_secret: None, + iss_required: true, + issuer, + is_signature_check_enabled: true, + // Secure default: reject all other audiences as untrusted, since any other audience + // can potentially impersonate the user when by sending its copy of these claims + // to this relying party. + other_aud_verifier_fn: Arc::new(|_| false), + signature_keys, + } + } + + pub fn require_audience_match(mut self, aud_required: bool) -> Self { + self.aud_match_required = aud_required; + self + } + + pub fn require_issuer_match(mut self, iss_required: bool) -> Self { + self.iss_required = iss_required; + self + } + + pub fn require_signature_check(mut self, sig_required: bool) -> Self { + self.is_signature_check_enabled = sig_required; + self + } + + pub fn set_allowed_algs(mut self, algs: I) -> Self + where + I: IntoIterator, + { + self.allowed_algs = Some(algs.into_iter().collect()); + self + } + pub fn allow_any_alg(mut self) -> Self { + self.allowed_algs = None; + self + } + + pub fn set_client_secret(mut self, client_secret: ClientSecret) -> Self { + self.client_secret = Some(client_secret); + self + } + + pub fn set_other_audience_verifier_fn(mut self, other_aud_verifier_fn: T) -> Self + where + T: Fn(&Audience) -> bool + 'a + Send + Sync, + { + self.other_aud_verifier_fn = Arc::new(other_aud_verifier_fn); + self + } + + fn validate_jose_header( + jose_header: &JsonWebTokenHeader, + ) -> Result<(), ClaimsVerificationError> + where + JE: JweContentEncryptionAlgorithm, + { + // The 'typ' header field must either be omitted or have the canonicalized value JWT. + if let Some(ref jwt_type) = jose_header.typ { + if jwt_type.to_uppercase() != "JWT" { + return Err(ClaimsVerificationError::Unsupported(format!( + "unexpected or unsupported JWT type `{}`", + **jwt_type + ))); + } + } + // The 'cty' header field must be omitted, since it's only used for JWTs that contain + // content types other than JSON-encoded claims. This may include nested JWTs, such as if + // JWE encryption is used. This is currently unsupported. + if let Some(ref content_type) = jose_header.cty { + if content_type.to_uppercase() == "JWT" { + return Err(ClaimsVerificationError::Unsupported( + "nested JWT's are not currently supported".to_string(), + )); + } else { + return Err(ClaimsVerificationError::Unsupported(format!( + "unexpected or unsupported JWT content type `{}`", + **content_type + ))); + } + } + + // If 'crit' fields are specified, we must reject any we do not understand. Since this + // implementation doesn't understand any of them, unconditionally reject the JWT. Note that + // the spec prohibits this field from containing any of the standard headers or being empty. + if jose_header.crit.is_some() { + // https://tools.ietf.org/html/rfc7515#appendix-E + return Err(ClaimsVerificationError::Unsupported( + "critical JWT header fields are unsupported".to_string(), + )); + } + Ok(()) + } + + pub fn verified_claims(&self, jwt: A) -> Result + where + A: JsonWebTokenAccess, + C: AudiencesClaim + Debug + DeserializeOwned + IssuerClaim + Serialize, + JE: JweContentEncryptionAlgorithm, + T: AudiencesClaim + IssuerClaim, + { + { + let jose_header = jwt.unverified_header(); + Self::validate_jose_header(jose_header)?; + + // The code below roughly follows the validation steps described in + // https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation + + // 1. If the ID Token is encrypted, decrypt it using the keys and algorithms that the Client + // specified during Registration that the OP was to use to encrypt the ID Token. If + // encryption was negotiated with the OP at Registration time and the ID Token is not + // encrypted, the RP SHOULD reject it. + + if let JsonWebTokenAlgorithm::Encryption(ref encryption_alg) = jose_header.alg { + return Err(ClaimsVerificationError::Unsupported(format!( + "JWE encryption is not currently supported (found algorithm `{}`)", + serde_plain::to_string(encryption_alg).unwrap_or_else(|err| panic!( + "encryption alg {:?} failed to serialize to a string: {}", + encryption_alg, err + )), + ))); + } + } + + // TODO: Add encryption (JWE) support + { + // 2. The Issuer Identifier for the OpenID Provider (which is typically obtained during + // Discovery) MUST exactly match the value of the iss (issuer) Claim. + let unverified_claims = jwt.unverified_payload_ref(); + if self.iss_required { + if let Some(issuer) = unverified_claims.issuer() { + if *issuer != self.issuer { + return Err(ClaimsVerificationError::InvalidIssuer(format!( + "expected `{}` (found `{}`)", + *self.issuer, **issuer + ))); + } + } else { + return Err(ClaimsVerificationError::InvalidIssuer( + "missing issuer claim".to_string(), + )); + } + } + + // 3. The Client MUST validate that the aud (audience) Claim contains its client_id value + // registered at the Issuer identified by the iss (issuer) Claim as an audience. The aud + // (audience) Claim MAY contain an array with more than one element. The ID Token MUST be + // rejected if the ID Token does not list the Client as a valid audience, or if it + // contains additional audiences not trusted by the Client. + if self.aud_match_required { + if let Some(audiences) = unverified_claims.audiences() { + if !audiences + .iter() + .any(|aud| (**aud).deref() == self.client_id.deref()) + { + return Err(ClaimsVerificationError::InvalidAudience(format!( + "must contain `{}` (found audiences: {})", + *self.client_id, + audiences + .iter() + .map(|aud| format!("`{}`", Deref::deref(aud))) + .collect::>() + .join(", ") + ))); + } else if audiences.len() > 1 { + audiences + .iter() + .filter(|aud| (**aud).deref() != self.client_id.deref()) + .find(|aud| !(self.other_aud_verifier_fn)(aud)) + .map(|aud| { + Err(ClaimsVerificationError::InvalidAudience(format!( + "`{}` is not a trusted audience", + **aud, + ))) + }) + .unwrap_or(Ok(()))?; + } + } else { + return Err(ClaimsVerificationError::InvalidAudience( + "missing audiences claim".to_string(), + )); + } + } + } + // Steps 4--5 (azp claim validation) are specific to the ID token. + + // 6. If the ID Token is received via direct communication between the Client and the Token + // Endpoint (which it is in this flow), the TLS server validation MAY be used to validate + // the issuer in place of checking the token signature. The Client MUST validate the + // signature of all other ID Tokens according to JWS [JWS] using the algorithm specified + // in the JWT alg Header Parameter. The Client MUST use the keys provided by the Issuer. + if !self.is_signature_check_enabled { + return Ok(jwt.unverified_payload()); + } + + // Borrow the header again. We had to drop the reference above to allow for the + // early exit calling jwt.unverified_claims(), which takes ownership of the JWT. + let signature_alg = match jwt.unverified_header().alg { + // Encryption is handled above. + JsonWebTokenAlgorithm::Encryption(_) => unreachable!(), + JsonWebTokenAlgorithm::Signature(ref signature_alg, _) => signature_alg, + // Section 2 of OpenID Connect Core 1.0 specifies that "ID Tokens MUST NOT use + // none as the alg value unless the Response Type used returns no ID Token from + // the Authorization Endpoint (such as when using the Authorization Code Flow) + // and the Client explicitly requested the use of none at Registration time." + // + // While there's technically a use case where this is ok, we choose not to + // support it for now to protect against accidental misuse. If demand arises, + // we can figure out a API that mitigates the risk. + JsonWebTokenAlgorithm::None => return Err(ClaimsVerificationError::NoSignature), + } + .clone(); + + // 7. The alg value SHOULD be the default of RS256 or the algorithm sent by the Client + // in the id_token_signed_response_alg parameter during Registration. + if let Some(ref allowed_algs) = self.allowed_algs { + if !allowed_algs.contains(&signature_alg) { + return Err(ClaimsVerificationError::SignatureVerification( + SignatureVerificationError::DisallowedAlg(format!( + "algorithm `{}` is not one of: {}", + serde_plain::to_string(&signature_alg).unwrap_or_else(|err| panic!( + "signature alg {:?} failed to serialize to a string: {}", + signature_alg, err, + )), + allowed_algs + .iter() + .map( + |alg| serde_plain::to_string(alg).unwrap_or_else(|err| panic!( + "signature alg {:?} failed to serialize to a string: {}", + alg, err, + )) + ) + .collect::>() + .join(", "), + )), + )); + } + } + + // NB: We must *not* trust the 'kid' (key ID) or 'alg' (algorithm) fields present in the + // JOSE header, as an attacker could manipulate these while forging the JWT. The code + // below must be secure regardless of how these fields are manipulated. + + if signature_alg.uses_shared_secret() { + // 8. If the JWT alg Header Parameter uses a MAC based algorithm such as HS256, + // HS384, or HS512, the octets of the UTF-8 representation of the client_secret + // corresponding to the client_id contained in the aud (audience) Claim are used + // as the key to validate the signature. For MAC based algorithms, the behavior + // is unspecified if the aud is multi-valued or if an azp value is present that + // is different than the aud value. + if let Some(ref client_secret) = self.client_secret { + let key = K::new_symmetric(client_secret.secret().clone().into_bytes()); + return jwt + .payload(&signature_alg, &key) + .map_err(ClaimsVerificationError::SignatureVerification); + } else { + // The client secret isn't confidential for public clients, so anyone can forge a + // JWT with a valid signature. + return Err(ClaimsVerificationError::SignatureVerification( + SignatureVerificationError::DisallowedAlg( + "symmetric signatures are disallowed for public clients".to_string(), + ), + )); + } + } + + // Section 10.1 of OpenID Connect Core 1.0 states that the JWT must include a key ID + // if the JWK set contains more than one public key. + + // See if any key has a matching key ID (if supplied) and compatible type. + let public_keys = { + let key_id = &jwt.unverified_header().kid; + self.signature_keys.filter_keys(key_id, &signature_alg) + }; + if public_keys.is_empty() { + return Err(ClaimsVerificationError::SignatureVerification( + SignatureVerificationError::NoMatchingKey, + )); + } else if public_keys.len() != 1 { + return Err(ClaimsVerificationError::SignatureVerification( + SignatureVerificationError::AmbiguousKeyId(format!( + "JWK set must only contain one eligible public key \ + ({} eligible keys: {})", + public_keys.len(), + public_keys + .iter() + .map(|key| format!( + "{} ({})", + key.key_id() + .map(|kid| format!("`{}`", **kid)) + .unwrap_or_else(|| "null ID".to_string()), + serde_plain::to_string(key.key_type()).unwrap_or_else(|err| panic!( + "key type {:?} failed to serialize to a string: {}", + key.key_type(), + err, + )) + )) + .collect::>() + .join(", ") + )), + )); + } + + jwt.payload( + &signature_alg.clone(), + *public_keys.first().expect("unreachable"), + ) + .map_err(ClaimsVerificationError::SignatureVerification) + + // Steps 9--13 are specific to the ID token. + } +} + +/// Trait for verifying ID token nonces. +pub trait NonceVerifier { + /// Verifies the nonce. + /// + /// Returns `Ok(())` if the nonce is valid, or a string describing the error otherwise. + fn verify(self, nonce: Option<&Nonce>) -> Result<(), String>; +} + +impl NonceVerifier for &Nonce { + fn verify(self, nonce: Option<&Nonce>) -> Result<(), String> { + if let Some(claims_nonce) = nonce { + // Nonce::eq is already implemented with a constant time comparison + if claims_nonce != self { + return Err("nonce mismatch".to_string()); + } + } else { + return Err("missing nonce claim".to_string()); + } + Ok(()) + } +} + +impl NonceVerifier for F +where + F: FnOnce(Option<&Nonce>) -> Result<(), String>, +{ + fn verify(self, nonce: Option<&Nonce>) -> Result<(), String> { + self(nonce) + } +} + +/// ID token verifier. +#[derive(Clone)] +pub struct IdTokenVerifier<'a, JS, JT, JU, K> +where + JS: JwsSigningAlgorithm, + JT: JsonWebKeyType, + JU: JsonWebKeyUse, + K: JsonWebKey, +{ + acr_verifier_fn: + Arc) -> Result<(), String> + 'a + Send + Sync>, + #[allow(clippy::type_complexity)] + auth_time_verifier_fn: + Arc>) -> Result<(), String> + 'a + Send + Sync>, + iat_verifier_fn: Arc) -> Result<(), String> + 'a + Send + Sync>, + jwt_verifier: JwtClaimsVerifier<'a, JS, JT, JU, K>, + time_fn: Arc DateTime + 'a + Send + Sync>, +} +impl<'a, JS, JT, JU, K> IdTokenVerifier<'a, JS, JT, JU, K> +where + JS: JwsSigningAlgorithm, + JT: JsonWebKeyType, + JU: JsonWebKeyUse, + K: JsonWebKey, +{ + fn new(jwt_verifier: JwtClaimsVerifier<'a, JS, JT, JU, K>) -> Self { + IdTokenVerifier { + // By default, accept authorization context reference (acr claim). + acr_verifier_fn: Arc::new(|_| Ok(())), + auth_time_verifier_fn: Arc::new(|_| Ok(())), + // By default, accept any issued time (iat claim). + iat_verifier_fn: Arc::new(|_| Ok(())), + jwt_verifier, + // By default, use the current system time. + time_fn: Arc::new(Utc::now), + } + } + + /// Initializes a new verifier for a public client (i.e., one without a client secret). + pub fn new_public_client( + client_id: ClientId, + issuer: IssuerUrl, + signature_keys: JsonWebKeySet, + ) -> Self { + Self::new(JwtClaimsVerifier::new(client_id, issuer, signature_keys)) + } + + /// Initializes a no-op verifier that performs no signature, audience, or issuer verification. + /// The token's expiration time is still checked, and the token is otherwise required to conform to the expected format. + pub fn new_insecure_without_verification() -> Self { + let empty_issuer = IssuerUrl::new("https://0.0.0.0".to_owned()) + .expect("Creating empty issuer url mustn't fail"); + Self::new_public_client( + ClientId::new(String::new()), + empty_issuer, + JsonWebKeySet::new(vec![]), + ) + .insecure_disable_signature_check() + .require_audience_match(false) + .require_issuer_match(false) + } + + /// Initializes a new verifier for a confidential client (i.e., one with a client secret). + /// + /// A confidential client verifier is required in order to verify ID tokens signed using a + /// shared secret algorithm such as `HS256`, `HS384`, or `HS512`. For these algorithms, the + /// client secret is the shared secret. + pub fn new_confidential_client( + client_id: ClientId, + client_secret: ClientSecret, + issuer: IssuerUrl, + signature_keys: JsonWebKeySet, + ) -> Self { + Self::new( + JwtClaimsVerifier::new(client_id, issuer, signature_keys) + .set_client_secret(client_secret), + ) + } + + /// Specifies which JSON Web Signature algorithms are supported. + pub fn set_allowed_algs(mut self, algs: I) -> Self + where + I: IntoIterator, + { + self.jwt_verifier = self.jwt_verifier.set_allowed_algs(algs); + self + } + + /// Specifies that any signature algorithm is supported. + pub fn allow_any_alg(mut self) -> Self { + self.jwt_verifier = self.jwt_verifier.allow_any_alg(); + self + } + + /// Specifies a function for verifying the `acr` claim. + /// + /// The function should return `Ok(())` if the claim is valid, or a string describing the error + /// otherwise. + pub fn set_auth_context_verifier_fn(mut self, acr_verifier_fn: T) -> Self + where + T: Fn(Option<&AuthenticationContextClass>) -> Result<(), String> + 'a + Send + Sync, + { + self.acr_verifier_fn = Arc::new(acr_verifier_fn); + self + } + + /// Specifies a function for verifying the `auth_time` claim. + /// + /// The function should return `Ok(())` if the claim is valid, or a string describing the error + /// otherwise. + pub fn set_auth_time_verifier_fn(mut self, auth_time_verifier_fn: T) -> Self + where + T: Fn(Option>) -> Result<(), String> + 'a + Send + Sync, + { + self.auth_time_verifier_fn = Arc::new(auth_time_verifier_fn); + self + } + + /// Enables signature verification. + /// + /// Signature verification is enabled by default, so this function is only useful if + /// [`IdTokenVerifier::insecure_disable_signature_check`] was previously invoked. + pub fn enable_signature_check(mut self) -> Self { + self.jwt_verifier = self.jwt_verifier.require_signature_check(true); + self + } + + /// Disables signature verification. + /// + /// # Security Warning + /// + /// Unverified ID tokens may be subject to forgery. See [Section 16.3]( + /// https://openid.net/specs/openid-connect-core-1_0.html#TokenManufacture) for more + /// information. + pub fn insecure_disable_signature_check(mut self) -> Self { + self.jwt_verifier = self.jwt_verifier.require_signature_check(false); + self + } + + /// Specifies whether the issuer claim must match the expected issuer URL for the provider. + pub fn require_issuer_match(mut self, iss_required: bool) -> Self { + self.jwt_verifier = self.jwt_verifier.require_issuer_match(iss_required); + self + } + + /// Specifies whether the audience claim must match this client's client ID. + pub fn require_audience_match(mut self, aud_required: bool) -> Self { + self.jwt_verifier = self.jwt_verifier.require_audience_match(aud_required); + self + } + + /// Specifies a function for returning the current time. + /// + /// This function is used for verifying the ID token expiration time. + pub fn set_time_fn(mut self, time_fn: T) -> Self + where + T: Fn() -> DateTime + 'a + Send + Sync, + { + self.time_fn = Arc::new(time_fn); + self + } + + /// Specifies a function for verifying the ID token issue time. + /// + /// The function should return `Ok(())` if the claim is valid, or a string describing the error + /// otherwise. + pub fn set_issue_time_verifier_fn(mut self, iat_verifier_fn: T) -> Self + where + T: Fn(DateTime) -> Result<(), String> + 'a + Send + Sync, + { + self.iat_verifier_fn = Arc::new(iat_verifier_fn); + self + } + + /// Specifies a function for verifying audiences included in the `aud` claim that differ from + /// this client's client ID. + /// + /// The function should return `true` if the audience is trusted, or `false` otherwise. + /// + /// [Section 3.1.3.7](https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation) + /// states that *"The ID Token MUST be rejected if the ID Token does not list the Client as a + /// valid audience, or if it contains additional audiences not trusted by the Client."* + pub fn set_other_audience_verifier_fn(mut self, other_aud_verifier_fn: T) -> Self + where + T: Fn(&Audience) -> bool + 'a + Send + Sync, + { + self.jwt_verifier = self + .jwt_verifier + .set_other_audience_verifier_fn(other_aud_verifier_fn); + self + } + + pub(crate) fn verified_claims<'b, AC, GC, JE, N>( + &self, + jwt: &'b JsonWebToken, JsonWebTokenJsonPayloadSerde>, + nonce_verifier: N, + ) -> Result<&'b IdTokenClaims, ClaimsVerificationError> + where + AC: AdditionalClaims, + GC: GenderClaim, + JE: JweContentEncryptionAlgorithm, + N: NonceVerifier, + { + // The code below roughly follows the validation steps described in + // https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation + + // Steps 1--3 are handled by the generic JwtClaimsVerifier. + let partially_verified_claims = self.jwt_verifier.verified_claims(jwt)?; + + self.verify_claims(partially_verified_claims, nonce_verifier)?; + Ok(partially_verified_claims) + } + + pub(crate) fn verified_claims_owned( + &self, + jwt: JsonWebToken, JsonWebTokenJsonPayloadSerde>, + nonce_verifier: N, + ) -> Result, ClaimsVerificationError> + where + AC: AdditionalClaims, + GC: GenderClaim, + JE: JweContentEncryptionAlgorithm, + N: NonceVerifier, + { + // The code below roughly follows the validation steps described in + // https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation + + // Steps 1--3 are handled by the generic JwtClaimsVerifier. + let partially_verified_claims = self.jwt_verifier.verified_claims(jwt)?; + + self.verify_claims(&partially_verified_claims, nonce_verifier)?; + Ok(partially_verified_claims) + } + + fn verify_claims( + &self, + partially_verified_claims: &'_ IdTokenClaims, + nonce_verifier: N, + ) -> Result<(), ClaimsVerificationError> + where + AC: AdditionalClaims, + GC: GenderClaim, + N: NonceVerifier, + { + // 4. If the ID Token contains multiple audiences, the Client SHOULD verify that an azp + // Claim is present. + + // There is significant confusion and contradiction in the OpenID Connect Core spec around + // the azp claim. See https://bitbucket.org/openid/connect/issues/973/ for a detailed + // discussion. Given the lack of clarity around how this claim should be used, we defer + // any verification of it here until a use case becomes apparent. If such a use case does + // arise, we most likely want to allow clients to pass in a function for validating the + // azp claim rather than introducing logic that affects all clients of this library. + + // This naive implementation of the spec would almost certainly not be useful in practice: + /* + let azp_required = partially_verified_claims.audiences().len() > 1; + + // 5. If an azp (authorized party) Claim is present, the Client SHOULD verify that its + // client_id is the Claim Value. + if let Some(authorized_party) = partially_verified_claims.authorized_party() { + if *authorized_party != self.client_id { + return Err(ClaimsVerificationError::InvalidAudience(format!( + "authorized party must match client ID `{}` (found `{}`", + *self.client_id, **authorized_party + ))); + } + } else if azp_required { + return Err(ClaimsVerificationError::InvalidAudience(format!( + "missing authorized party claim but multiple audiences found" + ))); + } + */ + + // Steps 6--8 are handled by the generic JwtClaimsVerifier. + + // 9. The current time MUST be before the time represented by the exp Claim. + let cur_time = (*self.time_fn)(); + if cur_time >= partially_verified_claims.expiration() { + return Err(ClaimsVerificationError::Expired(format!( + "ID token expired at {} (current time is {})", + partially_verified_claims.expiration(), + cur_time + ))); + } + + // 10. The iat Claim can be used to reject tokens that were issued too far away from the + // current time, limiting the amount of time that nonces need to be stored to prevent + // attacks. The acceptable range is Client specific. + (*self.iat_verifier_fn)(partially_verified_claims.issue_time()) + .map_err(ClaimsVerificationError::Expired)?; + + // 11. If a nonce value was sent in the Authentication Request, a nonce Claim MUST be + // present and its value checked to verify that it is the same value as the one that was + // sent in the Authentication Request. The Client SHOULD check the nonce value for + // replay attacks. The precise method for detecting replay attacks is Client specific. + nonce_verifier + .verify(partially_verified_claims.nonce()) + .map_err(ClaimsVerificationError::InvalidNonce)?; + + // 12. If the acr Claim was requested, the Client SHOULD check that the asserted Claim Value + // is appropriate. The meaning and processing of acr Claim Values is out of scope for + // this specification. + (*self.acr_verifier_fn)(partially_verified_claims.auth_context_ref()) + .map_err(ClaimsVerificationError::InvalidAuthContext)?; + + // 13. If the auth_time Claim was requested, either through a specific request for this + // Claim or by using the max_age parameter, the Client SHOULD check the auth_time Claim + // value and request re-authentication if it determines too much time has elapsed since + // the last End-User authentication. + (*self.auth_time_verifier_fn)(partially_verified_claims.auth_time()) + .map_err(ClaimsVerificationError::InvalidAuthTime)?; + + Ok(()) + } +} + +/// User info verifier. +#[derive(Clone)] +pub struct UserInfoVerifier<'a, JE, JS, JT, JU, K> +where + JE: JweContentEncryptionAlgorithm, + JS: JwsSigningAlgorithm, + JT: JsonWebKeyType, + JU: JsonWebKeyUse, + K: JsonWebKey, +{ + jwt_verifier: JwtClaimsVerifier<'a, JS, JT, JU, K>, + expected_subject: Option, + _phantom: PhantomData, +} +impl<'a, JE, JS, JT, JU, K> UserInfoVerifier<'a, JE, JS, JT, JU, K> +where + JE: JweContentEncryptionAlgorithm, + JS: JwsSigningAlgorithm, + JT: JsonWebKeyType, + JU: JsonWebKeyUse, + K: JsonWebKey, +{ + /// Instantiates a user info verifier. + pub fn new( + client_id: ClientId, + issuer: IssuerUrl, + signature_keys: JsonWebKeySet, + expected_subject: Option, + ) -> Self { + UserInfoVerifier { + jwt_verifier: JwtClaimsVerifier::new(client_id, issuer, signature_keys), + expected_subject, + _phantom: PhantomData, + } + } + + pub(crate) fn expected_subject(&self) -> Option<&SubjectIdentifier> { + self.expected_subject.as_ref() + } + + /// Specifies whether the issuer claim must match the expected issuer URL for the provider. + pub fn require_issuer_match(mut self, iss_required: bool) -> Self { + self.jwt_verifier = self.jwt_verifier.require_issuer_match(iss_required); + self + } + + /// Specifies whether the audience claim must match this client's client ID. + pub fn require_audience_match(mut self, aud_required: bool) -> Self { + self.jwt_verifier = self.jwt_verifier.require_audience_match(aud_required); + self + } + + pub(crate) fn verified_claims( + &self, + user_info_jwt: JsonWebToken< + JE, + JS, + JT, + UserInfoClaimsImpl, + JsonWebTokenJsonPayloadSerde, + >, + ) -> Result, ClaimsVerificationError> + where + AC: AdditionalClaims, + GC: GenderClaim, + { + let user_info = self.jwt_verifier.verified_claims(user_info_jwt)?; + if self + .expected_subject + .iter() + .all(|expected_subject| user_info.standard_claims.sub == *expected_subject) + { + Ok(user_info) + } else { + Err(ClaimsVerificationError::InvalidSubject(format!( + "expected `{}` (found `{}`)", + // This can only happen when self.expected_subject is not None. + self.expected_subject.as_ref().unwrap().as_str(), + user_info.standard_claims.sub.as_str() + ))) + } + } +} diff --git a/src/verification/tests.rs b/src/verification/tests.rs new file mode 100644 index 0000000..444913f --- /dev/null +++ b/src/verification/tests.rs @@ -0,0 +1,1177 @@ +use crate::core::{ + CoreIdToken, CoreIdTokenClaims, CoreIdTokenVerifier, CoreJsonWebKey, CoreJsonWebKeySet, + CoreJsonWebKeyType, CoreJsonWebKeyUse, CoreJweContentEncryptionAlgorithm, + CoreJwsSigningAlgorithm, CoreRsaPrivateSigningKey, CoreUserInfoClaims, + CoreUserInfoJsonWebToken, CoreUserInfoVerifier, +}; +use crate::helpers::{timestamp_to_utc, Base64UrlEncodedBytes, Timestamp}; +use crate::jwt::tests::{TEST_RSA_PRIV_KEY, TEST_RSA_PUB_KEY}; +use crate::jwt::{JsonWebToken, JsonWebTokenHeader, JsonWebTokenJsonPayloadSerde}; +use crate::verification::{AudiencesClaim, IssuerClaim, JwtClaimsVerifier}; +use crate::{ + AccessToken, Audience, AuthenticationContextClass, AuthorizationCode, ClaimsVerificationError, + ClientId, ClientSecret, EndUserName, IssuerUrl, JsonWebKeyId, Nonce, + SignatureVerificationError, StandardClaims, SubjectIdentifier, UserInfoError, +}; + +use chrono::{TimeZone, Utc}; +use serde::{Deserialize, Serialize}; + +use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; + +type CoreJsonWebTokenHeader = JsonWebTokenHeader< + CoreJweContentEncryptionAlgorithm, + CoreJwsSigningAlgorithm, + CoreJsonWebKeyType, +>; + +type CoreJwtClaimsVerifier<'a> = JwtClaimsVerifier< + 'a, + CoreJwsSigningAlgorithm, + CoreJsonWebKeyType, + CoreJsonWebKeyUse, + CoreJsonWebKey, +>; + +fn assert_unsupported(result: Result, expected_substr: &str) { + match result { + Err(ClaimsVerificationError::Unsupported(msg)) => { + assert!(msg.contains(expected_substr)) + } + Err(err) => panic!("unexpected error: {:?}", err), + Ok(_) => panic!("validation should fail"), + } +} + +#[test] +fn test_jose_header() { + // Unexpected JWT type. + assert_unsupported( + CoreJwtClaimsVerifier::validate_jose_header( + &serde_json::from_str::( + "{\"alg\":\"RS256\",\"typ\":\"NOT_A_JWT\"}", + ) + .expect("failed to deserialize"), + ), + "unsupported JWT type", + ); + + // Nested JWTs. + assert_unsupported( + CoreJwtClaimsVerifier::validate_jose_header( + &serde_json::from_str::("{\"alg\":\"RS256\",\"cty\":\"JWT\"}") + .expect("failed to deserialize"), + ), + "nested JWT", + ); + assert_unsupported( + CoreJwtClaimsVerifier::validate_jose_header( + &serde_json::from_str::( + "{\"alg\":\"RS256\",\"cty\":\"NOT_A_JWT\"}", + ) + .expect("failed to deserialize"), + ), + "unsupported JWT content type", + ); + + // Critical fields. Adapted from https://tools.ietf.org/html/rfc7515#appendix-E + assert_unsupported( + CoreJwtClaimsVerifier::validate_jose_header( + &serde_json::from_str::( + "{\ + \"alg\":\"RS256\",\ + \"crit\":[\"http://example.invalid/UNDEFINED\"],\ + \"http://example.invalid/UNDEFINED\":true\ + }", + ) + .expect("failed to deserialize"), + ), + "critical JWT header fields are unsupported", + ); +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +struct TestClaims { + aud: Option>, + iss: Option, + payload: String, +} +impl AudiencesClaim for TestClaims { + fn audiences(&self) -> Option<&Vec> { + self.aud.as_ref() + } +} +impl IssuerClaim for TestClaims { + fn issuer(&self) -> Option<&IssuerUrl> { + self.iss.as_ref() + } +} +type TestClaimsJsonWebToken = JsonWebToken< + CoreJweContentEncryptionAlgorithm, + CoreJwsSigningAlgorithm, + CoreJsonWebKeyType, + TestClaims, + JsonWebTokenJsonPayloadSerde, +>; + +#[test] +fn test_jwt_verified_claims() { + let rsa_key = + serde_json::from_str::(TEST_RSA_PUB_KEY).expect("deserialization failed"); + + let client_id = ClientId::new("my_client".to_string()); + let issuer = IssuerUrl::new("https://example.com".to_string()).unwrap(); + let verifier = CoreJwtClaimsVerifier::new( + client_id.clone(), + issuer.clone(), + CoreJsonWebKeySet::new(vec![rsa_key.clone()]), + ); + + // Invalid JOSE header. + assert_unsupported( + verifier.verified_claims( + serde_json::from_value::(serde_json::Value::String( + "eyJhbGciOiJBMjU2R0NNIiwiY3R5IjoiSldUIn0.eyJhdWQiOlsibXlfY2xpZW50Il0sImlzcyI6Im\ + h0dHBzOi8vZXhhbXBsZS5jb20iLCJwYXlsb2FkIjoiaGVsbG8gd29ybGQifQ.YmFkX2hhc2g" + .to_string(), + )) + .expect("failed to deserialize"), + ), + "nested JWT", + ); + + // JWE-encrypted JWT. + assert_unsupported( + verifier.verified_claims( + serde_json::from_value::(serde_json::Value::String( + "eyJhbGciOiJBMjU2R0NNIn0.eyJhdWQiOlsibXlfY2xpZW50Il0sImlzcyI6Imh0dHBzOi8vZXhhbX\ + BsZS5jb20iLCJwYXlsb2FkIjoiaGVsbG8gd29ybGQifQ.YmFkX2hhc2g" + .to_string(), + )) + .expect("failed to deserialize"), + ), + "JWE encryption", + ); + + // Wrong issuer. + match verifier.verified_claims( + serde_json::from_value::(serde_json::Value::String( + "eyJhbGciOiJSUzI1NiJ9.eyJhdWQiOlsibXlfY2xpZW50Il0sImlzcyI6Imh0dHBzOi8vYXR0YWNrZXIuY\ + 29tIiwicGF5bG9hZCI6ImhlbGxvIHdvcmxkIn0.YmFkX2hhc2g" + .to_string(), + )) + .expect("failed to deserialize"), + ) { + Err(ClaimsVerificationError::InvalidIssuer(_)) => {} + other => panic!("unexpected result: {:?}", other), + } + + // Missing issuer. + match verifier.verified_claims( + serde_json::from_value::(serde_json::Value::String( + "eyJhbGciOiJSUzI1NiJ9.eyJhdWQiOlsibXlfY2xpZW50Il0sInBheWxvYWQiOiJoZWxsbyB3b3JsZCJ9.\ + YmFkX2hhc2g" + .to_string(), + )) + .expect("failed to deserialize"), + ) { + Err(ClaimsVerificationError::InvalidIssuer(_)) => {} + other => panic!("unexpected result: {:?}", other), + } + + // Ignore missing issuer. + verifier + .clone() + .require_issuer_match(false) + .verified_claims( + serde_json::from_value::(serde_json::Value::String( + "eyJhbGciOiJSUzI1NiJ9.eyJhdWQiOlsibXlfY2xpZW50Il0sInBheWxvYWQiOiJoZWxsbyB3b3JsZCJ9.\ + nv09al63NNDfb8cF3IozegXKbPaUC08zknRPKmQ5qKgXv80hjVxknkpRz7BxocB3JYTBjhYd0gyN9wAuJj\ + byZ1QaUC14HOB83awAGbehy5yFLkLadTfPT7-siBCvE2V7AF73a_21YvwdkKmJ-RaKWHzFnG8CDmioma3X\ + cWyrsdRLgvUkrWllajLRo8DCIXQ8OuZo1_o4n17PSlPxSkhKIrgaWCvG6tan40Y_1DZOFv47bx4hQUGd-J\ + h2aEjiwn65WV3M_Xb2vQMP7VgYNVaNlfxzpL4yDASItbPMWaXBt3ZUa_IOGoSx2GMnPkrQ4xp56qUth6U7\ + esWPqRSqqolnHg" + .to_string(), + )).expect("failed to deserialize"), + ).expect("verification should succeed"); + + // Wrong audience. + match verifier.verified_claims( + serde_json::from_value::(serde_json::Value::String( + "eyJhbGciOiJSUzI1NiJ9.eyJhdWQiOlsib3RoZXJfY2xpZW50Il0sImlzcyI6Imh0dHBzOi8vZXhhbXBsZ\ + S5jb20iLCJwYXlsb2FkIjoiaGVsbG8gd29ybGQifQ.YmFkX2hhc2g" + .to_string(), + )) + .expect("failed to deserialize"), + ) { + Err(ClaimsVerificationError::InvalidAudience(_)) => {} + other => panic!("unexpected result: {:?}", other), + } + + // Missing audience. + match verifier.verified_claims( + serde_json::from_value::(serde_json::Value::String( + "eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJodHRwczovL2V4YW1wbGUuY29tIiwicGF5bG9hZCI6ImhlbGxvI\ + HdvcmxkIn0.YmFkX2hhc2g" + .to_string(), + )) + .expect("failed to deserialize"), + ) { + Err(ClaimsVerificationError::InvalidAudience(_)) => {} + other => panic!("unexpected result: {:?}", other), + } + + // Ignore missing audience. + verifier + .clone() + .require_audience_match(false) + .verified_claims( + serde_json::from_value::(serde_json::Value::String( + "eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJodHRwczovL2V4YW1wbGUuY29tIiwicGF5bG9hZCI6Imhlb\ + GxvIHdvcmxkIn0.lP-Z_zGPNoKIbLQsnrZc2LAc5qJrKyb7t07ZtJUKVhcwHiCUou4bBhq5RHlElCh\ + 0ElRRP6I25lp6UszkRvIC46UV3GVze0x73kVkHSvCVI7MO75LbL9BRqrm5b4CN2zCiFBY8-EwTXnJd\ + Ri0d_U8K29TV24L2I-Z5ZILebwUue1N59AGDjx2yYLFx5NOw3TUsPyscG62aZAT321pL_jcYwTWTWw\ + 2FYm07zguwx-PUTZwGXlJiOgXQqRIbY_1bS3I_D8UWsmEB3DmV0f9z-iklgIPFawa4wHaE-hpzBAEx\ + pSieyOavA5pl0Se3XRYA-CkdDVgzG0Pt4IdnxFanfUXTw" + .to_string(), + )) + .expect("failed to deserialize"), + ) + .expect("verification should succeed"); + + // Multiple audiences, where one is a match (default = reject) + match verifier.verified_claims( + serde_json::from_value::(serde_json::Value::String( + "eyJhbGciOiJSUzI1NiJ9.eyJhdWQiOlsiYXVkMSIsIm15X2NsaWVudCIsImF1ZDIiXSwiaXNzIjoia\ + HR0cHM6Ly9leGFtcGxlLmNvbSIsInBheWxvYWQiOiJoZWxsbyB3b3JsZCJ9.N9ibisEe0kKLe1GDWM\ + ON3PmYqbL73dag-loM8pjKJNinF9SB7n4JuSu4FrNkeW4F1Cz8MIbLuWfKvDa_4v_3FstMA3GODZWH\ + BVIiuNFay2ovCfGFyykwe47dF_47g_OM5AkJc_teE5MN8lPh9V5zYCy3ON3zZ3acFPJMOPTdbU56xD\ + eFe7lil6DmV4JU9A52t5ZkJILFaIuxxXJUIDmqpPTvHkggh_QOj9C2US9bgg5b543JwT4j-HbDp51L\ + dDB4k3azOssT1ddtoAuuDOctnraMKUtqffJXexxfwA1uM6EIofSrK5v11xwgTciL9xDXAvav_G2buP\ + ol1bjGLa2t0Q" + .to_string(), + )) + .expect("failed to deserialize"), + ) { + Err(ClaimsVerificationError::InvalidAudience(_)) => {} + other => panic!("unexpected result: {:?}", other), + } + + // Multiple audiences, where one is a match (allowed) + verifier + .clone() + .set_other_audience_verifier_fn(|aud| **aud == "aud1" || **aud == "aud2") + .verified_claims( + serde_json::from_value::(serde_json::Value::String( + "eyJhbGciOiJSUzI1NiJ9.eyJhdWQiOlsiYXVkMSIsIm15X2NsaWVudCIsImF1ZDIiXSwiaXNzIjoia\ + HR0cHM6Ly9leGFtcGxlLmNvbSIsInBheWxvYWQiOiJoZWxsbyB3b3JsZCJ9.N9ibisEe0kKLe1GDWM\ + ON3PmYqbL73dag-loM8pjKJNinF9SB7n4JuSu4FrNkeW4F1Cz8MIbLuWfKvDa_4v_3FstMA3GODZWH\ + BVIiuNFay2ovCfGFyykwe47dF_47g_OM5AkJc_teE5MN8lPh9V5zYCy3ON3zZ3acFPJMOPTdbU56xD\ + eFe7lil6DmV4JU9A52t5ZkJILFaIuxxXJUIDmqpPTvHkggh_QOj9C2US9bgg5b543JwT4j-HbDp51L\ + dDB4k3azOssT1ddtoAuuDOctnraMKUtqffJXexxfwA1uM6EIofSrK5v11xwgTciL9xDXAvav_G2buP\ + ol1bjGLa2t0Q" + .to_string(), + )) + .expect("failed to deserialize"), + ) + .expect("verification should succeed"); + + // Multiple audiences, where none is a match + match verifier.verified_claims( + serde_json::from_value::(serde_json::Value::String( + "eyJhbGciOiJSUzI1NiJ9.eyJhdWQiOlsiYXVkMSIsImF1ZDIiXSwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlL\ + mNvbSIsInBheWxvYWQiOiJoZWxsbyB3b3JsZCJ9.YmFkX2hhc2g" + .to_string(), + )) + .expect("failed to deserialize"), + ) { + Err(ClaimsVerificationError::InvalidAudience(_)) => {} + other => panic!("unexpected result: {:?}", other), + } + + // Disable signature check. + verifier + .clone() + .require_signature_check(false) + .verified_claims( + serde_json::from_value::(serde_json::Value::String( + "eyJhbGciOiJSUzI1NiJ9.eyJhdWQiOlsibXlfY2xpZW50Il0sImlzcyI6Imh0dHBzOi8vZXhhbXBsZ\ + S5jb20iLCJwYXlsb2FkIjoiaGVsbG8gd29ybGQifQ.YmFkX2hhc2g" + .to_string(), + )) + .expect("failed to deserialize"), + ) + .expect("verification should succeed"); + + // "none" algorithm (unsigned JWT). + match verifier.verified_claims( + serde_json::from_value::(serde_json::Value::String( + "eyJhbGciOiJub25lIn0.eyJhdWQiOlsibXlfY2xpZW50Il0sImlzcyI6Imh0dHBzOi8vZXhhbXBsZ\ + S5jb20iLCJwYXlsb2FkIjoiaGVsbG8gd29ybGQifQ." + .to_string(), + )) + .expect("failed to deserialize"), + ) { + Err(ClaimsVerificationError::NoSignature) => {} + other => panic!("unexpected result: {:?}", other), + } + + let valid_rs256_jwt = + serde_json::from_value::(serde_json::Value::String( + "eyJhbGciOiJSUzI1NiJ9.eyJhdWQiOlsibXlfY2xpZW50Il0sImlzcyI6Imh0dHBzOi8vZXhhbXBsZ\ + S5jb20iLCJwYXlsb2FkIjoiaGVsbG8gd29ybGQifQ.UZ7vmAsDmOBzeB6e2_0POUfyhMRZKM6WSKz3\ + jB2QdmO-eZ9605EzhkJufJQ8515ryWnHv-gUHtZHQi3zilrzhBwvE2cVP83Gv2XIL1EKaMMmfISeEB\ + ShWez_FvqxN_bamh5yTROhWmoZTmof-MweBCHgINcsEd7K4e_BHHgq3aaRBpvSFlL_z4l_1NwNcTBo\ + kqjNScKZITk42AbsSuGR39L94BWLhz6WXQZ_Sn6R1Ro6roOm1b7E82jJiQEtlseQiCCvPR2JJ6LgW6\ + XTMzQ0vCqSh1A7U_IBDsjY_yag8_X3xxFh2URCtHJ47ZSjqfv6hq7OAq8tmVecOVgfIvABOg" + .to_string(), + )) + .expect("failed to deserialize"); + // Default algs + RS256 -> allowed + verifier + .verified_claims(valid_rs256_jwt.clone()) + .expect("verification should succeed"); + + let verifier_with_client_secret = CoreJwtClaimsVerifier::new( + client_id.clone(), + issuer.clone(), + CoreJsonWebKeySet::new(vec![]), + ) + .set_client_secret(ClientSecret::new("my_secret".to_string())); + let valid_hs256_jwt = + serde_json::from_value::(serde_json::Value::String( + "eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOlsibXlfY2xpZW50Il0sImlzcyI6Imh0dHBzOi8vZXhhbXBsZ\ + S5jb20iLCJwYXlsb2FkIjoiaGVsbG8gd29ybGQifQ.dTXvSWen74_rC4oiWw0ziLZNe4KZk8Jw2VZe\ + N6vLCDo" + .to_string(), + )) + .expect("failed to deserialize"); + + // Default algs + HS256 -> disallowed + match verifier_with_client_secret.verified_claims(valid_hs256_jwt.clone()) { + Err(ClaimsVerificationError::SignatureVerification( + SignatureVerificationError::DisallowedAlg(_), + )) => {} + other => panic!("unexpected result: {:?}", other), + } + + // none algs + RS256 -> allowed + verifier + .clone() + .allow_any_alg() + .verified_claims(valid_rs256_jwt.clone()) + .expect("verification should succeed"); + + // none algs + HS256 -> allowed + verifier_with_client_secret + .clone() + .allow_any_alg() + .verified_claims(valid_hs256_jwt.clone()) + .expect("verification should succeed"); + + // none algs + none -> disallowed + match verifier.clone().allow_any_alg().verified_claims( + serde_json::from_value::(serde_json::Value::String( + "eyJhbGciOiJub25lIn0.eyJhdWQiOlsibXlfY2xpZW50Il0sImlzcyI6Imh0dHBzOi8vZXhhbXBsZ\ + S5jb20iLCJwYXlsb2FkIjoiaGVsbG8gd29ybGQifQ." + .to_string(), + )) + .expect("failed to deserialize"), + ) { + Err(ClaimsVerificationError::NoSignature) => {} + other => panic!("unexpected result: {:?}", other), + } + + // HS256 + no client secret -> disallowed + match verifier + .clone() + .allow_any_alg() + .verified_claims(valid_hs256_jwt.clone()) + { + Err(ClaimsVerificationError::SignatureVerification( + SignatureVerificationError::DisallowedAlg(_), + )) => {} + other => panic!("unexpected result: {:?}", other), + } + + // HS256 + valid signature + verifier_with_client_secret + .clone() + .set_allowed_algs(vec![CoreJwsSigningAlgorithm::HmacSha256]) + .verified_claims(valid_hs256_jwt) + .expect("verification should succeed"); + + // HS256 + invalid signature + match verifier_with_client_secret + .clone() + .set_allowed_algs(vec![CoreJwsSigningAlgorithm::HmacSha256]) + .verified_claims( + serde_json::from_value::(serde_json::Value::String( + "eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOlsibXlfY2xpZW50Il0sImlzcyI6Imh0dHBzOi8vZXhhbXBsZ\ + S5jb20iLCJwYXlsb2FkIjoiaGVsbG8gd29ybGQifQ.dTXvSWen74_rC4oiWw0ziLZNe4KZk8Jw2VZe\ + N6vLCEo" + .to_string(), + )) + .expect("failed to deserialize"), + ) { + Err(ClaimsVerificationError::SignatureVerification( + SignatureVerificationError::CryptoError(_), + )) => {} + other => panic!("unexpected result: {:?}", other), + } + + // No public keys + match CoreJwtClaimsVerifier::new( + client_id.clone(), + issuer.clone(), + CoreJsonWebKeySet::new(vec![]), + ) + .verified_claims(valid_rs256_jwt.clone()) + { + Err(ClaimsVerificationError::SignatureVerification( + SignatureVerificationError::NoMatchingKey, + )) => {} + other => panic!("unexpected result: {:?}", other), + } + + let kid = JsonWebKeyId::new("bilbo.baggins@hobbiton.example".to_string()); + let n = Base64UrlEncodedBytes::new(vec![ + 159, 129, 15, 180, 3, 130, 115, 208, 37, 145, 228, 7, 63, 49, 210, 182, 0, 27, 130, 206, + 219, 77, 146, 240, 80, 22, 93, 71, 207, 202, 184, 163, 196, 28, 183, 120, 172, 117, 83, + 121, 63, 142, 249, 117, 118, 141, 26, 35, 116, 216, 113, 37, 100, 195, 188, 215, 123, 158, + 164, 52, 84, 72, 153, 64, 124, 255, 0, 153, 146, 10, 147, 26, 36, 196, 65, 72, 82, 171, 41, + 189, 176, 169, 92, 6, 83, 243, 108, 96, 230, 11, 249, 11, 98, 88, 221, 165, 111, 55, 4, + 123, 165, 194, 209, 208, 41, 175, 156, 157, 64, 186, 199, 170, 65, 199, 138, 13, 209, 6, + 138, 221, 105, 158, 128, 143, 234, 1, 30, 161, 68, 29, 138, 79, 123, 180, 233, 123, 227, + 159, 85, 241, 221, 212, 78, 156, 75, 163, 53, 21, 151, 3, 212, 211, 75, 96, 62, 101, 20, + 122, 79, 35, 214, 211, 192, 153, 108, 117, 237, 238, 132, 106, 130, 209, 144, 174, 16, 120, + 60, 150, 28, 240, 56, 122, 237, 33, 6, 210, 208, 85, 91, 111, 217, 55, 250, 213, 83, 83, + 135, 224, 255, 114, 255, 190, 120, 148, 20, 2, 176, 184, 34, 234, 42, 116, 182, 5, 140, 29, + 171, 249, 179, 74, 118, 203, 99, 184, 127, 170, 44, 104, 71, 184, 226, 131, 127, 255, 145, + 24, 110, 107, 28, 20, 145, 28, 249, 137, 168, 144, 146, 168, 28, 230, 1, 221, 172, 211, + 249, 207, + ]); + let e = Base64UrlEncodedBytes::new(vec![1, 0, 1]); + + // Wrong key type (symmetric key) + match CoreJwtClaimsVerifier::new( + client_id.clone(), + issuer.clone(), + CoreJsonWebKeySet::new(vec![CoreJsonWebKey { + kty: CoreJsonWebKeyType::Symmetric, + use_: Some(CoreJsonWebKeyUse::Signature), + kid: Some(kid.clone()), + n: None, + e: None, + k: Some(Base64UrlEncodedBytes::new(vec![1, 2, 3, 4])), + crv: None, + x: None, + y: None, + d: None, + #[cfg(feature = "jwk-alg")] + alg: None, + }]), + ) + .verified_claims(valid_rs256_jwt.clone()) + { + Err(ClaimsVerificationError::SignatureVerification( + SignatureVerificationError::NoMatchingKey, + )) => {} + other => panic!("unexpected result: {:?}", other), + } + + // Correct public key, but with signing disallowed + match CoreJwtClaimsVerifier::new( + client_id.clone(), + issuer.clone(), + CoreJsonWebKeySet::new(vec![CoreJsonWebKey { + kty: CoreJsonWebKeyType::RSA, + use_: Some(CoreJsonWebKeyUse::Encryption), + kid: Some(kid), + n: Some(n), + e: Some(e), + k: None, + crv: None, + x: None, + y: None, + d: None, + #[cfg(feature = "jwk-alg")] + alg: None, + }]), + ) + .verified_claims(valid_rs256_jwt.clone()) + { + Err(ClaimsVerificationError::SignatureVerification( + SignatureVerificationError::NoMatchingKey, + )) => {} + other => panic!("unexpected result: {:?}", other), + } + + // Wrong key ID + match verifier.verified_claims( + serde_json::from_value::(serde_json::Value::String( + "eyJhbGciOiJSUzI1NiIsImtpZCI6Indyb25nX2tleSJ9.eyJhdWQiOlsibXlfY2xpZW50Il0sImlzcyI6I\ + mh0dHBzOi8vZXhhbXBsZS5jb20iLCJwYXlsb2FkIjoiaGVsbG8gd29ybGQifQ.lVLomyIyO8WmyS1VZWPu\ + cGhRTUyK9RCw90fJC5CfDWUCgt1CBn-aP_ieWWBGfjb4ccR4dl57OYxdLl0Day8QN5pTCBud9QKpQ0rKQX\ + K8eBlOW8uSosx8q5pwU_bRyy-XuKJiPlDCOwTEHOp_hOgZFGjoN27MH3Xm8kc0iT3PgyqQ46-wsqHY9S02\ + hdJORX7vqYwQLZF8_k_L8K0IG_dC-1Co0g5oAf37oVSdl8hE-ScQ9K-AiSpS-cGYyldbMhyKNDL3ry2cuI\ + EUgYSIznkVFuM7RrEdNK222z5PF11ijYx-TM7BIDggbcIyJm-UqpmvVaJImmj5FNkMzuHYznLtdg" + .to_string(), + )) + .expect("failed to deserialize"), + ) { + Err(ClaimsVerificationError::SignatureVerification( + SignatureVerificationError::NoMatchingKey, + )) => {} + other => panic!("unexpected result: {:?}", other), + } + + // Client secret + public key + verifier + .clone() + .set_client_secret(ClientSecret::new("my_secret".to_string())) + .verified_claims(valid_rs256_jwt.clone()) + .expect("verification should succeed"); + + // Multiple matching public keys: no KID specified + match CoreJwtClaimsVerifier::new( + client_id.clone(), + issuer.clone(), + CoreJsonWebKeySet::new(vec![rsa_key.clone(), rsa_key.clone()]), + ) + .verified_claims(valid_rs256_jwt.clone()) + { + Err(ClaimsVerificationError::SignatureVerification( + SignatureVerificationError::AmbiguousKeyId(_), + )) => {} + other => panic!("unexpected result: {:?}", other), + } + + // Multiple matching public keys: KID specified + match CoreJwtClaimsVerifier::new( + client_id, + issuer, + CoreJsonWebKeySet::new(vec![rsa_key.clone(), rsa_key]), + ) + .verified_claims( + serde_json::from_value::(serde_json::Value::String( + "eyJhbGciOiJSUzI1NiIsImtpZCI6ImJpbGJvLmJhZ2dpbnNAaG9iYml0b24uZXhhbXBsZSJ9.eyJhdWQiO\ + lsibXlfY2xpZW50Il0sImlzcyI6Imh0dHBzOi8vZXhhbXBsZS5jb20iLCJwYXlsb2FkIjoiaGVsbG8gd29\ + ybGQifQ.jH0v2fQGvH2MD0jn5pQP6W6AF5rJlizyofdyRUIt7E3GraGA1LYDiLAVIfhST3uwJopP-TgtBk\ + zc-zyJSvgTR63S8iI1YlHypItpx7r4I9ydzo8GSN5RrZudcU2esY4uEnLbVl17ZVNu4IyTExeKJ0sPM0Hj\ + qkOA4XaP2cJwsK-bookNHSA8NRE6adRMrHAKJbor5jrGjpkZAKHbnQFK-wu-nEV_OjS9jpN_FboRZVcDTZ\ + GFzeFbqFqHdRn6UWPFnVpVnUhih16UjNH1om6gwc0uFoPWTDxJlXQCFbHMhZtgCbUkXQBH7twPMc4YUziw\ + S8GIRKCcXjdrP5oyxmcitQ" + .to_string(), + )) + .expect("failed to deserialize"), + ) { + Err(ClaimsVerificationError::SignatureVerification( + SignatureVerificationError::AmbiguousKeyId(_), + )) => {} + other => panic!("unexpected result: {:?}", other), + } + + // RS256 + valid signature + verifier + .verified_claims(valid_rs256_jwt) + .expect("verification should succeed"); + + // RS256 + invalid signature + match verifier.verified_claims( + serde_json::from_value::(serde_json::Value::String( + "eyJhbGciOiJSUzI1NiJ9.eyJhdWQiOlsibXlfY2xpZW50Il0sImlzcyI6Imh0dHBzOi8vZXhhbXBsZS5jb\ + 20iLCJwYXlsb2FkIjoiaGVsbG8gd29ybGQifQ.YmFkX2hhc2g" + .to_string(), + )) + .expect("failed to deserialize"), + ) { + Err(ClaimsVerificationError::SignatureVerification( + SignatureVerificationError::CryptoError(_), + )) => {} + other => panic!("unexpected result: {:?}", other), + } +} + +type CoreIdTokenJwt = JsonWebToken< + CoreJweContentEncryptionAlgorithm, + CoreJwsSigningAlgorithm, + CoreJsonWebKeyType, + CoreIdTokenClaims, + JsonWebTokenJsonPayloadSerde, +>; + +#[test] +fn test_id_token_verified_claims() { + let rsa_key = + serde_json::from_str::(TEST_RSA_PUB_KEY).expect("deserialization failed"); + + let client_id = ClientId::new("my_client".to_string()); + let issuer = IssuerUrl::new("https://example.com".to_string()).unwrap(); + let mock_current_time = AtomicUsize::new(1544932149); + let mock_is_valid_issue_time = AtomicBool::new(true); + // Extra scope needed to ensure closures are destroyed before the values they borrow. + { + let public_client_verifier = CoreIdTokenVerifier::new_public_client( + client_id.clone(), + issuer.clone(), + CoreJsonWebKeySet::new(vec![rsa_key.clone()]), + ) + .set_time_fn(|| { + timestamp_to_utc(&Timestamp::Seconds( + mock_current_time.load(Ordering::Relaxed).into(), + )) + .unwrap() + }) + .set_issue_time_verifier_fn(|_| { + if mock_is_valid_issue_time.load(Ordering::Relaxed) { + Ok(()) + } else { + Err("Invalid iat claim".to_string()) + } + }); + + let insecure_verifier = CoreIdTokenVerifier::new_insecure_without_verification() + .set_time_fn(|| { + timestamp_to_utc(&Timestamp::Seconds( + mock_current_time.load(Ordering::Relaxed).into(), + )) + .unwrap() + }); + + // This JWTs below have an issue time of 1544928549 and an expiration time of 1544932149. + + let test_jwt_without_nonce = + serde_json::from_value::(serde_json::Value::String( + "eyJhbGciOiJSUzI1NiJ9.eyJhdWQiOlsibXlfY2xpZW50Il0sImlzcyI6Imh0dHBzOi8vZXhhbXBsZ\ + S5jb20iLCJzdWIiOiJzdWJqZWN0IiwiZXhwIjoxNTQ0OTMyMTQ5LCJpYXQiOjE1NDQ5Mjg1NDl9.nN\ + aTxNwclnTHd1Q9POkddm5wB1w3wJ-gwQWHomhimttk3SWQTLhxI0SSjWrHahGxlfkjufJlSyt-t_VO\ + SdcROvIYZTDznDfFZz3oSOev-p9XiZ-EZTS-U6N11Y923sDQjbTMeukz1F3ZFEfn5Mv2xjdEoJccCe\ + 7SaGuDmVqMqTLXMtsw9NCE_KDd0oKSwDzbJIBBPEfG3JjbKg0Dln7ENHg9wzoNFQzPXrkKzjneBgD3\ + vuwFCV5y-e8xUBdLaLZF1kdkDZJIA48uRROLlWjsM8pEptosA5QK07luQCZNqcaZWEczoGXeQs8PyA\ + zkNV7JEmti3bJnWSN-ud4cFU0LiQ" + .to_string(), + )) + .expect("failed to deserialize"); + + // Invalid JWT claims + match public_client_verifier.verified_claims( + &serde_json::from_value::(serde_json::Value::String( + "eyJhbGciOiJSUzI1NiJ9.eyJhdWQiOlsibXlfY2xpZW50Il0sImlzcyI6Imh0dHBzOi8vYXR0YWNrZ\ + XIuY29tIiwic3ViIjoic3ViamVjdCIsImV4cCI6MTU0NDkzMjE0OSwiaWF0IjoxNTQ0OTI4NTQ5LCJ\ + ub25jZSI6InRoZV9ub25jZSIsImFjciI6InRoZV9hY3IifQ.Pkicxk0dTU5BkSxgqTON6lE7A7ir3l\ + aADRyoeRoCNDX3AOx7BXCbfzbda6HJiPskN2nu56w0q-0OdkDSIHls-2xTUlLEJv2Bv0BLYwV5ZVJ8\ + hoc-rTd0_oLUb5NzyD80RyVByjVMK8bh6cwysTnr8QDxsEiFZbFo3mVJob2yjPZnNOdcNJWPcVVueP\ + 8vqMJnx5kHih1gKZpWj_dMN9b2AW6zVLOInW3Ox__gx6fsFFz7rjxItG-PTY_OQMzthqeHUyq4o9y7\ + Jv8mB_jFkTZGVKHTPpObHV-qptJ_rnlwvF_mP5GARBLng-4Yd7nmSr31onYL48QDjGOrwPqQ-IyaCQ" + .to_string(), + )) + .expect("failed to deserialize"), |_: Option<&Nonce>| Ok(())) { + Err(ClaimsVerificationError::InvalidIssuer(_)) => {} + other => panic!("unexpected result: {:?}", other), + } + + // TODO: disallowed algs + + // Expired token + mock_current_time.store(1544928549 + 3600, Ordering::Relaxed); + match public_client_verifier + .verified_claims(&test_jwt_without_nonce, |_: Option<&Nonce>| Ok(())) + { + Err(ClaimsVerificationError::Expired(_)) => {} + other => panic!("unexpected result: {:?}", other), + } + mock_current_time.store(1544928549 + 1, Ordering::Relaxed); + + // Invalid issue time + mock_is_valid_issue_time.store(false, Ordering::Relaxed); + match public_client_verifier + .verified_claims(&test_jwt_without_nonce, |_: Option<&Nonce>| Ok(())) + { + Err(ClaimsVerificationError::Expired(_)) => {} + other => panic!("unexpected result: {:?}", other), + } + mock_is_valid_issue_time.store(true, Ordering::Relaxed); + + let valid_nonce = Nonce::new("the_nonce".to_string()); + + // Successful verification w/o checking nonce + public_client_verifier + .verified_claims(&test_jwt_without_nonce, |_: Option<&Nonce>| Ok(())) + .expect("verification should succeed"); + + // Missing nonce + match public_client_verifier.verified_claims(&test_jwt_without_nonce, &valid_nonce) { + Err(ClaimsVerificationError::InvalidNonce(_)) => {} + other => panic!("unexpected result: {:?}", other), + } + + // Missing nonce w/ closure + match public_client_verifier.verified_claims( + &test_jwt_without_nonce, + |nonce: Option<&Nonce>| { + if nonce.iter().any(|n| n.secret() == valid_nonce.secret()) { + Ok(()) + } else { + Err("invalid nonce".to_string()) + } + }, + ) { + Err(ClaimsVerificationError::InvalidNonce(_)) => {} + other => panic!("unexpected result: {:?}", other), + } + + let test_jwt_with_nonce = + serde_json::from_value::(serde_json::Value::String( + "eyJhbGciOiJSUzI1NiJ9.eyJhdWQiOlsibXlfY2xpZW50Il0sImlzcyI6Imh0dHBzOi8vZXhhbXBsZ\ + S5jb20iLCJzdWIiOiJzdWJqZWN0IiwiZXhwIjoxNTQ0OTMyMTQ5LCJpYXQiOjE1NDQ5Mjg1NDksIm5\ + vbmNlIjoidGhlX25vbmNlIiwiYWNyIjoidGhlX2FjciIsImF1dGhfdGltZSI6MTU0NDkyODU0OH0.W\ + XA7SS9aMh_6rvBEgQce5D2J84OqphmmnCLGgEKRTN5G-UuQTNOBp8VS5_4f3xgzMEEMvGJJauJoALk\ + muUeHB-N_ESrkmB3tgDzBSYBa7kuYPHUPYpdjZM2UVolqI9RYyHaWwKjL_Io5YyAazB5lH5ibPaiBl\ + UNKGs3cmVsEB22UGMFKM6cek7GinrHQe_aJQsMU839-c2zzlEyFSeI8QBphQtG6AN82IPkNRv8QWmw\ + ZjUiB5a-W73Z3gURYMNs7f32BjAUNoJzW0Qj34vzD2djoSHhltE0wHKBzPqGhUM1Y3A-a3q-LS2g1h\ + 6qgXb_KQ_Mmok8v8ld0cW_aYRLfNg" + .to_string(), + )) + .expect("failed to deserialize"); + + // Invalid nonce + match public_client_verifier.verified_claims( + &test_jwt_with_nonce, + &Nonce::new("different_nonce".to_string()), + ) { + Err(ClaimsVerificationError::InvalidNonce(_)) => {} + other => panic!("unexpected result: {:?}", other), + } + + // Invalid AuthenticationContextClass reference + match public_client_verifier + .clone() + .set_auth_context_verifier_fn(|acr| { + assert_eq!(**acr.unwrap(), "the_acr"); + Err("Invalid acr claim".to_string()) + }) + .verified_claims(&test_jwt_with_nonce, &valid_nonce) + { + Err(ClaimsVerificationError::InvalidAuthContext(_)) => {} + other => panic!("unexpected result: {:?}", other), + } + + let test_jwt_without_auth_time = + serde_json::from_value::(serde_json::Value::String( + "eyJhbGciOiJSUzI1NiJ9.eyJhdWQiOlsibXlfY2xpZW50Il0sImlzcyI6Imh0dHBzOi8vZXhhbXBsZ\ + S5jb20iLCJzdWIiOiJzdWJqZWN0IiwiZXhwIjoxNTQ0OTMyMTQ5LCJpYXQiOjE1NDQ5Mjg1NDksIm5\ + vbmNlIjoidGhlX25vbmNlIiwiYWNyIjoidGhlX2FjciJ9.c_lU1VRasTg0mB4lwdOzbzvFS_XShMLN\ + lAPUpHBaMtCSPtI71L2x3hIByfkqIrAED-Qc_am2gNJ20bifidlkTOO6nyaBrJuaSjwT8aqajEbXon\ + 5JFswwPvqCIWjd0eV5dXC1MZunpd7ANXSC7Qw16v3m_crc9wcI_fLFCzuAKrWYokGvNy0gr1CxcgVg\ + aE9qR0eqaatetzCuaOJhYOq4njrRlGZWtbj5Q56q3zhxJ_yS8K8gv1QcB4sHjUyXIj21jzjUD87zVG\ + dJsn8E-nFJSltBdQhEaLksTBH6ZZhkeGicQ8cEPnNeS4L1vfVyAd_cjl64JHLmzw8RUp8XuoF9nA" + .to_string(), + )) + .expect("failed to deserialize"); + + // Missing auth_time (ok) + public_client_verifier + .verified_claims(&test_jwt_without_auth_time, |_: Option<&Nonce>| Ok(())) + .expect("verification should succeed"); + + // Missing auth_time (error) + match public_client_verifier + .clone() + .set_auth_time_verifier_fn(|auth_time| { + assert!(auth_time.is_none()); + Err("Invalid auth_time claim".to_string()) + }) + .verified_claims(&test_jwt_without_auth_time, |_: Option<&Nonce>| Ok(())) + { + Err(ClaimsVerificationError::InvalidAuthTime(_)) => {} + other => panic!("unexpected result: {:?}", other), + } + + // Invalid auth_time + match public_client_verifier + .clone() + .set_auth_time_verifier_fn(|auth_time| { + assert_eq!( + auth_time.unwrap(), + timestamp_to_utc(&Timestamp::Seconds(1544928548.into())).unwrap(), + ); + Err("Invalid auth_time claim".to_string()) + }) + .verified_claims(&test_jwt_with_nonce, &valid_nonce) + { + Err(ClaimsVerificationError::InvalidAuthTime(_)) => {} + other => panic!("unexpected result: {:?}", other), + } + + // Successful verification with nonce, acr, and auth_time specified (no expected Nonce) + public_client_verifier + .verified_claims(&test_jwt_with_nonce, |_: Option<&Nonce>| Ok(())) + .expect("verification should succeed"); + insecure_verifier + .verified_claims(&test_jwt_with_nonce, |_: Option<&Nonce>| Ok(())) + .expect("verification should succeed"); + + // Successful verification with nonce, acr, and auth_time specified (w/ expected Nonce) + public_client_verifier + .verified_claims(&test_jwt_with_nonce, &valid_nonce) + .expect("verification should succeed"); + insecure_verifier + .verified_claims(&test_jwt_with_nonce, &valid_nonce) + .expect("verification should succeed"); + + // Successful verification with nonce, acr, and auth_time specified (w/ closure) + public_client_verifier + .verified_claims(&test_jwt_with_nonce, |nonce: Option<&Nonce>| { + if nonce.iter().any(|n| n.secret() == valid_nonce.secret()) { + Ok(()) + } else { + Err("invalid nonce".to_string()) + } + }) + .expect("verification should succeed"); + insecure_verifier + .verified_claims(&test_jwt_with_nonce, |nonce: Option<&Nonce>| { + if nonce.iter().any(|n| n.secret() == valid_nonce.secret()) { + Ok(()) + } else { + Err("invalid nonce".to_string()) + } + }) + .expect("verification should succeed"); + + // HS256 w/ default algs + let test_jwt_hs256 = serde_json::from_value::(serde_json::Value::String( + "eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOlsibXlfY2xpZW50Il0sImlzcyI6Imh0dHBzOi8vZXhhbXBsZ\ + S5jb20iLCJzdWIiOiJzdWJqZWN0IiwiZXhwIjoxNTQ0OTMyMTQ5LCJpYXQiOjE1NDQ5Mjg1NDksIm5\ + vbmNlIjoidGhlX25vbmNlIn0.xUnSwSbcHsHWyJxwKGg69BIo_CktcyN5BVulGDb_QzE" + .to_string(), + )) + .expect("failed to deserialize"); + let private_client_verifier = CoreIdTokenVerifier::new_confidential_client( + client_id.clone(), + ClientSecret::new("my_secret".to_string()), + issuer.clone(), + CoreJsonWebKeySet::new(vec![rsa_key.clone()]), + ) + .set_time_fn(|| { + timestamp_to_utc(&Timestamp::Seconds( + mock_current_time.load(Ordering::Relaxed).into(), + )) + .unwrap() + }); + match private_client_verifier.verified_claims(&test_jwt_hs256, &valid_nonce) { + Err(ClaimsVerificationError::SignatureVerification(_)) => {} + other => panic!("unexpected result: {:?}", other), + } + insecure_verifier + .clone() + .verified_claims(&test_jwt_hs256, &valid_nonce) + .expect("verification should succeed"); + + // HS256 w/ set_allowed_algs + private_client_verifier + .clone() + .set_allowed_algs(vec![CoreJwsSigningAlgorithm::HmacSha256]) + .verified_claims(&test_jwt_hs256, &valid_nonce) + .expect("verification should succeed"); + + // HS256 w/ allow_any_alg + private_client_verifier + .clone() + .allow_any_alg() + .verified_claims(&test_jwt_hs256, &valid_nonce) + .expect("verification should succeed"); + + // Invalid signature + let private_client_verifier_with_other_secret = + CoreIdTokenVerifier::new_confidential_client( + client_id, + ClientSecret::new("other_secret".to_string()), + issuer, + CoreJsonWebKeySet::new(vec![rsa_key]), + ) + .allow_any_alg() + .set_time_fn(|| { + timestamp_to_utc(&Timestamp::Seconds( + mock_current_time.load(Ordering::Relaxed).into(), + )) + .unwrap() + }); + match private_client_verifier_with_other_secret + .verified_claims(&test_jwt_hs256, &valid_nonce) + { + Err(ClaimsVerificationError::SignatureVerification(_)) => {} + other => panic!("unexpected result: {:?}", other), + } + + // Invalid signature w/ signature check disabled + private_client_verifier_with_other_secret + .clone() + .insecure_disable_signature_check() + .verified_claims(&test_jwt_hs256, &valid_nonce) + .expect("verification should succeed"); + }; +} + +#[test] +fn test_new_id_token() { + let client_id = ClientId::new("my_client".to_string()); + let issuer = IssuerUrl::new("https://example.com".to_string()).unwrap(); + let nonce = Nonce::new("the_nonce".to_string()); + let rsa_priv_key = CoreRsaPrivateSigningKey::from_pem(TEST_RSA_PRIV_KEY, None).unwrap(); + + let id_token = CoreIdToken::new( + CoreIdTokenClaims::new( + issuer.clone(), + vec![Audience::new((*client_id).clone())], + Utc.timestamp_opt(1544932149, 0) + .single() + .expect("valid timestamp"), + Utc.timestamp_opt(1544928549, 0) + .single() + .expect("valid timestamp"), + StandardClaims::new(SubjectIdentifier::new("subject".to_string())), + Default::default(), + ) + .set_nonce(Some(nonce.clone())) + .set_auth_context_ref(Some(AuthenticationContextClass::new("the_acr".to_string()))) + .set_auth_time(Some( + Utc.timestamp_opt(1544928548, 0) + .single() + .expect("valid timestamp"), + )), + &rsa_priv_key, + CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256, + Some(&AccessToken::new("the_access_token".to_string())), + Some(&AuthorizationCode::new( + "the_authorization_code".to_string(), + )), + ) + .unwrap(); + + let serialized_jwt: serde_json::Value = serde_json::to_value(&id_token).unwrap(); + let expected_serialized_jwt = + "eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJodHRwczovL2V4YW1wbGUuY29tIiwiYXVkIjpbIm15X2NsaWVudCJdL\ + CJleHAiOjE1NDQ5MzIxNDksImlhdCI6MTU0NDkyODU0OSwiYXV0aF90aW1lIjoxNTQ0OTI4NTQ4LCJub25jZSI\ + 6InRoZV9ub25jZSIsImFjciI6InRoZV9hY3IiLCJhdF9oYXNoIjoiWjNJQUNVR00tbXhIV3lZUXZpSzhFUSIsI\ + mNfaGFzaCI6Imo2OW1CZmFIbmRMM1Y1RmNoak9LVXciLCJzdWIiOiJzdWJqZWN0In0.CHCWFcIqbCZhZwZH4oY\ + _mlcRy5aUQQtlNI0VHNYxiILn9ppRHLL4Bn_LMn9VP8tGXkfZWxCgP25ZTyBXXKfk0fQvnukVdyM0bCOpQbiBg\ + 5gB9c46l_f-ZznDoHWonpnKky2Gmzk3ocb3TCUQ9GSeRXAzRdRNWTT0ElWNBsLWU4j2IIdnghM78gkXwOC76Rk\ + pshgB73ubtuHGdIf5L9Ec3hifHlVjzKuvedAM4SIOjdBOelgtBlF3463ufX_Ut91CjP5TzLMsuK3Lh_vyo8ttn\ + S41rBDuetR2ENvR0yj5RjkX_SPY3V0yCW8_NPPu1CHu_1oL0Nma0ohCbF3vnUJcwg"; + assert_eq!(expected_serialized_jwt, serialized_jwt.as_str().unwrap()); + + let rsa_pub_key = + serde_json::from_str::(TEST_RSA_PUB_KEY).expect("deserialization failed"); + + let mock_current_time = AtomicUsize::new(1544932148); + let time_fn = || { + timestamp_to_utc(&Timestamp::Seconds( + mock_current_time.load(Ordering::Relaxed).into(), + )) + .unwrap() + }; + let verifier = CoreIdTokenVerifier::new_public_client( + client_id, + issuer, + CoreJsonWebKeySet::new(vec![rsa_pub_key]), + ) + .set_time_fn(time_fn); + let claims = id_token.claims(&verifier, &nonce).unwrap(); + let unverified = id_token + .claims( + &CoreIdTokenVerifier::new_insecure_without_verification().set_time_fn(time_fn), + &nonce, + ) + .unwrap(); + assert_eq!(claims, unverified); +} + +#[test] +fn test_user_info_verified_claims() { + let rsa_key = + serde_json::from_str::(TEST_RSA_PUB_KEY).expect("deserialization failed"); + + let client_id = ClientId::new("my_client".to_string()); + let issuer = IssuerUrl::new("https://example.com".to_string()).unwrap(); + let sub = SubjectIdentifier::new("the_subject".to_string()); + + let verifier = CoreUserInfoVerifier::new( + client_id.clone(), + issuer.clone(), + CoreJsonWebKeySet::new(vec![rsa_key.clone()]), + Some(sub.clone()), + ); + + let json_claims = "{\ + \"sub\": \"the_subject\",\ + \"name\": \"Jane Doe\"\ + }"; + + // JSON response (default args) + assert_eq!( + CoreUserInfoClaims::from_json::( + json_claims.as_bytes(), + Some(&sub) + ) + .expect("verification should succeed") + .name() + .unwrap() + .iter() + .collect::>(), + vec![(None, &EndUserName::new("Jane Doe".to_string()))], + ); + + // Invalid subject + match CoreUserInfoClaims::from_json::( + json_claims.as_bytes(), + Some(&SubjectIdentifier::new("wrong_subject".to_string())), + ) { + Err(UserInfoError::ClaimsVerification(ClaimsVerificationError::InvalidSubject(_))) => {} + other => panic!("unexpected result: {:?}", other), + } + + let jwt_claims = serde_json::from_value::(serde_json::Value::String( + "eyJhbGciOiJSUzI1NiJ9.eyJhdWQiOlsibXlfY2xpZW50Il0sImlzcyI6Imh0dHBzOi8vZXhhb\ + XBsZS5jb20iLCJzdWIiOiJ0aGVfc3ViamVjdCIsIm5hbWUiOiJKYW5lIERvZSJ9.aX7VpexLAd\ + 43HtC1cFTot3jmqsr105rB50mzTcS1TXzWcxLbqYf1K7Kf-S1oP-ZCL_dnL9-nu3iDK_vRa6xT\ + nGGt3I1JwhoIv6znSS3JOPT1wtekyD-sLcUwqsJHWBBiTSBwlmGG_kVRuGkBtXgVZ9aGlqg9u1\ + FlxvyGUJ5q1o9gdb8mKql5ojgsThTNo9qdW3lPIVsiDO-n4mMp4HuOp1re4ZDDkHxiExjtLQAV\ + kR4q3SlhJC2mkr4mw3_0a2AW52ocWDiwY_lPcdmohmwFaB8aHlivYLFnmKGQIatEW-KDaW5fFo\ + JYreNkplo4FvzXYyxgxAsqHjHMI8MZVEa1IA" + .to_string(), + )) + .expect("failed to deserialize"); + + // Valid JWT response (default args) + jwt_claims + .clone() + .claims(&verifier) + .expect("verification should succeed"); + + // JWT response with invalid signature + match serde_json::from_value::(serde_json::Value::String( + "eyJhbGciOiJSUzI1NiJ9.eyJhdWQiOlsibXlfY2xpZW50Il0sImlzcyI6Imh0dHBzOi8vZXhhb\ + XBsZS5jb20iLCJzdWIiOiJ0aGVfc3ViamVjdCIsIm5hbWUiOiJKYW5lIERvZSJ9.bX7VpexLAd\ + 43HtC1cFTot3jmqsr105rB50mzTcS1TXzWcxLbqYf1K7Kf-S1oP-ZCL_dnL9-nu3iDK_vRa6xT\ + nGGt3I1JwhoIv6znSS3JOPT1wtekyD-sLcUwqsJHWBBiTSBwlmGG_kVRuGkBtXgVZ9aGlqg9u1\ + FlxvyGUJ5q1o9gdb8mKql5ojgsThTNo9qdW3lPIVsiDO-n4mMp4HuOp1re4ZDDkHxiExjtLQAV\ + kR4q3SlhJC2mkr4mw3_0a2AW52ocWDiwY_lPcdmohmwFaB8aHlivYLFnmKGQIatEW-KDaW5fFo\ + JYreNkplo4FvzXYyxgxAsqHjHMI8MZVEa1IA" + .to_string(), + )) + .expect("failed to deserialize") + .claims(&verifier) + { + Err(ClaimsVerificationError::SignatureVerification( + SignatureVerificationError::CryptoError(_), + )) => {} + other => panic!("unexpected result: {:?}", other), + } + + // JWT response with invalid issuer claim (error) + match jwt_claims.clone().claims(&CoreUserInfoVerifier::new( + client_id.clone(), + IssuerUrl::new("https://attacker.com".to_string()).unwrap(), + CoreJsonWebKeySet::new(vec![rsa_key.clone()]), + Some(sub.clone()), + )) { + Err(ClaimsVerificationError::InvalidIssuer(_)) => {} + other => panic!("unexpected result: {:?}", other), + } + + // JWT response with invalid issuer claim (allowed) + jwt_claims + .clone() + .claims( + &CoreUserInfoVerifier::new( + client_id, + IssuerUrl::new("https://attacker.com".to_string()).unwrap(), + CoreJsonWebKeySet::new(vec![rsa_key.clone()]), + Some(sub.clone()), + ) + .require_issuer_match(false), + ) + .expect("verification should succeed"); + + // JWT response with invalid audience claim (error) + match jwt_claims.clone().claims(&CoreUserInfoVerifier::new( + ClientId::new("wrong_client".to_string()), + issuer.clone(), + CoreJsonWebKeySet::new(vec![rsa_key.clone()]), + Some(sub.clone()), + )) { + Err(ClaimsVerificationError::InvalidAudience(_)) => {} + other => panic!("unexpected result: {:?}", other), + } + + // JWT response with invalid audience claim (allowed) + jwt_claims + .claims( + &CoreUserInfoVerifier::new( + ClientId::new("wrong_client".to_string()), + issuer, + CoreJsonWebKeySet::new(vec![rsa_key]), + Some(sub), + ) + .require_audience_match(false), + ) + .expect("verification should succeed"); +} + +#[test] +fn test_new_user_info_claims() { + let claims = CoreUserInfoClaims::new( + StandardClaims { + sub: SubjectIdentifier::new("the_subject".to_string()), + name: Some(EndUserName::new("John Doe".to_string()).into()), + given_name: None, + family_name: None, + middle_name: None, + nickname: None, + preferred_username: None, + profile: None, + picture: None, + website: None, + email: None, + email_verified: None, + gender: None, + birthday: None, + birthdate: None, + zoneinfo: None, + locale: None, + phone_number: None, + phone_number_verified: None, + address: None, + updated_at: Some( + Utc.timestamp_opt(1544928548, 0) + .single() + .expect("valid timestamp"), + ), + }, + Default::default(), + ); + + assert_eq!( + "{\"sub\":\"the_subject\",\"name\":\"John Doe\",\"updated_at\":1544928548}", + serde_json::to_string(&claims).unwrap() + ); + + let rsa_priv_key = CoreRsaPrivateSigningKey::from_pem(TEST_RSA_PRIV_KEY, None).unwrap(); + let claims_jwt = CoreUserInfoJsonWebToken::new( + claims, + &rsa_priv_key, + CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256, + ) + .unwrap(); + assert_eq!( + "eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ0aGVfc3ViamVjdCIsIm5hbWUiOiJKb2huIERvZSIsInVwZGF0ZWRfY\ + XQiOjE1NDQ5Mjg1NDh9.nJ7Buckt_p_ACXkyVRCQLqyaW8KhDsk5H9Nu7PdNf4daEcEWm-lGjoSTAfAbDPgHAZ\ + 78knomgLgDxiGWrj1qdFTIEFep32I3q18VBP_DcMdyuQafipK6T98RgZFWP8YnxlxLPHeJQlRsdMpemHK4vxas\ + ZD4A4aIn0K7z5J9RvrR3L7DWnc3fJQ0VU2v5QLePyqNWnFxks5eyl8Ios8JrZhwr4Q8GES8Q4Iw8Sz6W9vYpHK\ + 2r1YdaACMM4g_TTtV91lpjn-Li2-HxW9NERdLvYvF6HwGIwbss26trp2yjNTARlxBUT6LR7y82oPIJKXIKL1GD\ + YeSLeErhb6oTQ0a5gQ", + serde_json::to_value(claims_jwt).unwrap().as_str().unwrap() + ); +}