Skip to content

Commit

Permalink
Support Microsoft's style tenant id placeholders in issuer URLs
Browse files Browse the repository at this point in the history
  • Loading branch information
HannesSommer committed Jul 16, 2023
1 parent 6cc525b commit af236a9
Show file tree
Hide file tree
Showing 2 changed files with 71 additions and 7 deletions.
35 changes: 29 additions & 6 deletions src/discovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<F, HC, RE>(
issuer_url: IssuerUrl,
pub async fn discover_advanced_async<F, HC, RE>(
issuer_url: &IssuerUrl,
expected_issuer_url: &IssuerUrl,
http_client: HC,
) -> Result<Self, DiscoveryError<RE>>
where
Expand All @@ -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
Expand All @@ -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<F, HC, RE>(
issuer_url: IssuerUrl,
http_client: HC,
) -> Result<Self, DiscoveryError<RE>>
where
F: Future<Output = Result<HttpResponse, RE>>,
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,
Expand All @@ -355,7 +378,7 @@ where
}

fn discovery_response<RE>(
issuer_url: &IssuerUrl,
expected_issuer_url: &IssuerUrl,
discovery_response: HttpResponse,
) -> Result<Self, DiscoveryError<RE>>
where
Expand All @@ -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)
Expand Down
43 changes: 42 additions & 1 deletion src/verification.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ where
client_secret: Option<ClientSecret>,
iss_required: bool,
issuer: IssuerUrl,
other_issuer_verifier_fn: Arc<dyn Fn(&IssuerUrl) -> bool + 'a + Send + Sync>,
is_signature_check_enabled: bool,
other_aud_verifier_fn: Arc<dyn Fn(&Audience) -> bool + 'a + Send + Sync>,
signature_keys: JsonWebKeySet<JS, JT, JU, K>,
Expand All @@ -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
Expand All @@ -170,6 +173,14 @@ where
self
}

pub fn set_other_issuer_verifier_fn<T>(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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<T>(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.
///
Expand Down

0 comments on commit af236a9

Please sign in to comment.