From af236a9ae8c936d72a62ae64fb5036c9abd7b45a Mon Sep 17 00:00:00 2001 From: Hannes Sommer Date: Sun, 16 Jul 2023 13:27:33 +0200 Subject: [PATCH] Support Microsoft's style tenant id placeholders in issuer URLs --- src/discovery.rs | 35 +++++++++++++++++++++++++++++------ src/verification.rs | 43 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 71 insertions(+), 7 deletions(-) diff --git a/src/discovery.rs b/src/discovery.rs index 57bfcee2..2779a061 100644 --- a/src/discovery.rs +++ b/src/discovery.rs @@ -316,9 +316,14 @@ where /// /// Asynchronously fetches the OpenID Connect Discovery document and associated JSON Web Key Set /// from the OpenID Connect Provider. + /// It supports providing a custom expected issuer for IdPs that do not return exactly the same. + /// For instance, discovering Microsoft's OIDC configuration with the `common` tenant id at + /// https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration + /// will declare an issuer `https://login.microsoftonline.com/{tenantid}/v2.0` /// - pub async fn discover_async( - issuer_url: IssuerUrl, + pub async fn discover_advanced_async( + issuer_url: &IssuerUrl, + expected_issuer_url: &IssuerUrl, http_client: HC, ) -> Result> where @@ -333,7 +338,9 @@ where let provider_metadata = http_client(Self::discovery_request(discovery_url)) .await .map_err(DiscoveryError::Request) - .and_then(|http_response| Self::discovery_response(&issuer_url, http_response))?; + .and_then(|http_response| { + Self::discovery_response(expected_issuer_url, http_response) + })?; JsonWebKeySet::fetch_async(provider_metadata.jwks_uri(), http_client) .await @@ -343,6 +350,22 @@ where }) } + /// + /// 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, + { + Self::discover_advanced_async(&issuer_url, &issuer_url, http_client).await + } + fn discovery_request(discovery_url: url::Url) -> HttpRequest { HttpRequest { url: discovery_url, @@ -355,7 +378,7 @@ where } fn discovery_response( - issuer_url: &IssuerUrl, + expected_issuer_url: &IssuerUrl, discovery_response: HttpResponse, ) -> Result> where @@ -382,11 +405,11 @@ where ) .map_err(DiscoveryError::Parse)?; - if provider_metadata.issuer() != issuer_url { + if provider_metadata.issuer() != expected_issuer_url { Err(DiscoveryError::Validation(format!( "unexpected issuer URI `{}` (expected `{}`)", provider_metadata.issuer().as_str(), - issuer_url.as_str() + expected_issuer_url.as_str() ))) } else { Ok(provider_metadata) diff --git a/src/verification.rs b/src/verification.rs index 6bd219c6..0a648e4b 100644 --- a/src/verification.rs +++ b/src/verification.rs @@ -128,6 +128,7 @@ where client_secret: Option, iss_required: bool, issuer: IssuerUrl, + other_issuer_verifier_fn: Arc bool + 'a + Send + Sync>, is_signature_check_enabled: bool, other_aud_verifier_fn: Arc bool + 'a + Send + Sync>, signature_keys: JsonWebKeySet, @@ -151,6 +152,8 @@ where client_secret: None, iss_required: true, issuer, + // Secure default: reject all other issuers. + other_issuer_verifier_fn: Arc::new(|_| false), 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 @@ -170,6 +173,14 @@ where self } + pub fn set_other_issuer_verifier_fn(mut self, other_issuer_verifier_fn: T) -> Self + where + T: Fn(&IssuerUrl) -> bool + 'a + Send + Sync, + { + self.other_issuer_verifier_fn = Arc::new(other_issuer_verifier_fn); + self + } + pub fn require_signature_check(mut self, sig_required: bool) -> Self { self.is_signature_check_enabled = sig_required; self @@ -280,7 +291,7 @@ where let unverified_claims = jwt.unverified_payload_ref(); if self.iss_required { if let Some(issuer) = unverified_claims.issuer() { - if *issuer != self.issuer { + if *issuer != self.issuer && !(self.other_issuer_verifier_fn)(issuer) { return Err(ClaimsVerificationError::InvalidIssuer(format!( "expected `{}` (found `{}`)", *self.issuer, **issuer @@ -680,6 +691,36 @@ where self } + /// + /// Specifies a function for verifying the issuer claim in case it doesn't match exactly the + /// expected issuer. The default implementation rejects all other issuers. + /// + /// The function should return `true` if the issuer is trusted, or `false` otherwise. + /// + /// [Section 3.1.3.7](https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation) + /// states that *"The Issuer Identifier for the OpenID Provider (which is typically obtained + /// during Discovery) MUST exactly match the value of the iss (issuer) Claim."* + /// + /// Thus, *this function is only needed when the IdP doesn't comply with this requirement!* + /// + /// Example: Discovering Microsoft's OIDC configuration with the `common` tenant id at + /// https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration + /// will declare an issuer `https://login.microsoftonline.com/{tenantid}/v2.0` while returning + /// the actual tenant ID of the user being authenticated (not `common` !) interpolated for + /// `{tenantid}` in the `iss` claim, e.g. (fictitious tenant) + /// https://login.microsoftonline.com/a4ed8e24-23a7-11ee-977f-d7ef594af8a1/v2.0 + /// + /// + pub fn set_other_issuer_verifier_fn(mut self, other_issuer_verifier_fn: T) -> Self + where + T: Fn(&IssuerUrl) -> bool + 'a + Send + Sync, + { + self.jwt_verifier = self + .jwt_verifier + .set_other_issuer_verifier_fn(other_issuer_verifier_fn); + self + } + /// /// Specifies whether the audience claim must match this client's client ID. ///