securitydept-oauth-resource-server 0.2.0

OAuth Resource Server of SecurityDept, a layered authentication and authorization toolkit built as reusable Rust crates.
Documentation
mod introspection;
#[cfg(feature = "jwe")]
mod jwe;
#[cfg(feature = "jwe")]
mod watcher;

use std::sync::Arc;

use securitydept_creds::{JwtClaimsTrait, JwtValidation, TokenData, TokenFormat, TokenJwtClaims};
use tracing::debug;

use self::introspection::OAuthResourceServerVerifierIntrospection;
#[cfg(feature = "jwe")]
use self::jwe::OAuthResourceServerVerifierJwe;
use crate::{
    OAuthResourceServerConfig, OAuthResourceServerError, OAuthResourceServerMetadata,
    OAuthResourceServerResult, VerificationPolicy, VerifiedAccessToken, VerifiedOpaqueToken,
    VerifiedToken, models::scope_contains_all,
};

pub struct OAuthResourceServerVerifier {
    provider: Arc<securitydept_oauth_provider::OAuthProviderRuntime>,
    policy: VerificationPolicy,
    introspection: Option<OAuthResourceServerVerifierIntrospection>,
    #[cfg(feature = "jwe")]
    jwe: Option<OAuthResourceServerVerifierJwe>,
}

impl OAuthResourceServerVerifier {
    pub async fn from_config(config: OAuthResourceServerConfig) -> OAuthResourceServerResult<Self> {
        config.validate()?;
        let provider = Arc::new(
            securitydept_oauth_provider::OAuthProviderRuntime::from_config(
                config.provider_config(),
            )
            .await?,
        );
        Self::from_provider(provider, config).await
    }

    pub async fn from_provider(
        provider: Arc<securitydept_oauth_provider::OAuthProviderRuntime>,
        config: OAuthResourceServerConfig,
    ) -> OAuthResourceServerResult<Self> {
        config.validate()?;

        Ok(Self {
            provider,
            policy: VerificationPolicy::new(
                config.audiences.clone(),
                config.required_scopes.clone(),
                config.clock_skew,
            ),
            introspection: config
                .introspection
                .as_ref()
                .map(OAuthResourceServerVerifierIntrospection::from_config),
            #[cfg(feature = "jwe")]
            jwe: match config.jwe.as_ref() {
                Some(jwe_config) => {
                    Some(OAuthResourceServerVerifierJwe::from_config(jwe_config).await?)
                }
                None => None,
            },
        })
    }

    pub async fn metadata(&self) -> OAuthResourceServerResult<OAuthResourceServerMetadata> {
        let metadata = self.provider.metadata().await?;
        Ok(OAuthResourceServerMetadata {
            issuer: metadata.issuer,
            jwks_uri: metadata.jwks_uri,
            introspection_url: metadata.introspection_endpoint,
        })
    }

    pub fn policy(&self) -> &VerificationPolicy {
        &self.policy
    }

    pub fn provider(&self) -> &Arc<securitydept_oauth_provider::OAuthProviderRuntime> {
        &self.provider
    }

    pub async fn verify_token<CLAIMS>(
        &self,
        token: &str,
    ) -> OAuthResourceServerResult<VerifiedToken<CLAIMS>>
    where
        CLAIMS: JwtClaimsTrait,
    {
        match TokenFormat::from_token(token) {
            TokenFormat::Opaque => Ok(VerifiedToken::from(
                self.verify_opaque_access_token(token).await?,
            )),
            _ => Ok(VerifiedToken::from(self.verify_access_token(token).await?)),
        }
    }

    pub async fn verify_opaque_access_token(
        &self,
        token: &str,
    ) -> OAuthResourceServerResult<VerifiedOpaqueToken> {
        self.introspection
            .as_ref()
            .ok_or(OAuthResourceServerError::UnsupportedTokenFormat {
                token_format: TokenFormat::Opaque,
            })?
            .introspect(token, &self.provider, &self.policy)
            .await
    }

    pub async fn verify_rfc9068_access_token(
        &self,
        token: &str,
    ) -> OAuthResourceServerResult<VerifiedAccessToken<TokenJwtClaims>> {
        let token_data = self
            .verify_structured_token_data::<TokenJwtClaims>(token)
            .await?;
        validate_rfc9068_scope_policy(&token_data, &self.policy)?;

        Ok(VerifiedAccessToken {
            token_data,
            metadata: self.metadata().await?,
        })
    }

    pub async fn verify_access_token<CLAIMS>(
        &self,
        token: &str,
    ) -> OAuthResourceServerResult<VerifiedAccessToken<CLAIMS>>
    where
        CLAIMS: JwtClaimsTrait,
    {
        let token_data = self.verify_structured_token_data::<CLAIMS>(token).await?;

        Ok(VerifiedAccessToken {
            token_data,
            metadata: self.metadata().await?,
        })
    }

    async fn verify_structured_token_data<CLAIMS>(
        &self,
        token: &str,
    ) -> OAuthResourceServerResult<TokenData<CLAIMS>>
    where
        CLAIMS: JwtClaimsTrait,
    {
        match TokenFormat::from_token(token) {
            TokenFormat::JWT => self.verify_jwt_token_data(token).await,
            TokenFormat::Opaque => Err(OAuthResourceServerError::UnsupportedTokenFormat {
                token_format: TokenFormat::Opaque,
            }),
            TokenFormat::JWE => self.verify_jwe_token_data(token).await,
        }
    }

    async fn verify_jwt_token_data<CLAIMS>(
        &self,
        token: &str,
    ) -> OAuthResourceServerResult<TokenData<CLAIMS>>
    where
        CLAIMS: JwtClaimsTrait,
    {
        match self.verify_jwt_with_current_jwks(token).await {
            Ok(token_data) => Ok(token_data),
            Err(error) if should_retry_with_refreshed_jwks(&error) => {
                debug!("Retrying access token verification after JWKS refresh");
                let metadata = self.provider.refresh_jwks().await?;
                verify_jwt_with_policy::<CLAIMS>(
                    token,
                    &metadata.jwks,
                    &OAuthResourceServerMetadata {
                        issuer: metadata.issuer,
                        jwks_uri: metadata.jwks_uri,
                        introspection_url: metadata.introspection_endpoint,
                    },
                    &self.policy,
                )
            }
            Err(error) => Err(error),
        }
    }

    async fn verify_jwt_with_current_jwks<CLAIMS>(
        &self,
        token: &str,
    ) -> OAuthResourceServerResult<TokenData<CLAIMS>>
    where
        CLAIMS: JwtClaimsTrait,
    {
        let metadata = self.provider.metadata().await?;
        let resource_metadata = OAuthResourceServerMetadata {
            issuer: metadata.issuer.clone(),
            jwks_uri: metadata.jwks_uri.clone(),
            introspection_url: metadata.introspection_endpoint.clone(),
        };
        verify_jwt_with_policy::<CLAIMS>(token, &metadata.jwks, &resource_metadata, &self.policy)
    }

    async fn verify_jwe_token_data<CLAIMS>(
        &self,
        token: &str,
    ) -> OAuthResourceServerResult<TokenData<CLAIMS>>
    where
        CLAIMS: JwtClaimsTrait,
    {
        #[cfg(feature = "jwe")]
        {
            return match self.verify_jwe_with_current_jwks(token).await {
                Ok(token_data) => Ok(token_data),
                Err(error) if should_retry_with_refreshed_jwks(&error) => {
                    debug!("Retrying JWE access token verification after JWKS refresh");
                    let metadata = self.provider.refresh_jwks().await?;
                    self.jwe
                        .as_ref()
                        .ok_or(OAuthResourceServerError::UnsupportedTokenFormat {
                            token_format: TokenFormat::JWE,
                        })?
                        .verify_token_data::<CLAIMS>(
                            token,
                            &metadata.jwks,
                            &OAuthResourceServerMetadata {
                                issuer: metadata.issuer,
                                jwks_uri: metadata.jwks_uri,
                                introspection_url: metadata.introspection_endpoint,
                            },
                            &self.policy,
                        )
                        .await
                }
                Err(error) => Err(error),
            };
        }

        #[cfg(not(feature = "jwe"))]
        {
            let _ = token;
            Err(OAuthResourceServerError::UnsupportedTokenFormat {
                token_format: TokenFormat::JWE,
            })
        }
    }

    #[cfg(feature = "jwe")]
    async fn verify_jwe_with_current_jwks<CLAIMS>(
        &self,
        token: &str,
    ) -> OAuthResourceServerResult<TokenData<CLAIMS>>
    where
        CLAIMS: JwtClaimsTrait,
    {
        let metadata = self.provider.metadata().await?;
        self.jwe
            .as_ref()
            .ok_or(OAuthResourceServerError::UnsupportedTokenFormat {
                token_format: TokenFormat::JWE,
            })?
            .verify_token_data::<CLAIMS>(
                token,
                &metadata.jwks,
                &OAuthResourceServerMetadata {
                    issuer: metadata.issuer,
                    jwks_uri: metadata.jwks_uri,
                    introspection_url: metadata.introspection_endpoint,
                },
                &self.policy,
            )
            .await
    }
}

#[cfg(not(feature = "jwe"))]
fn verify_jwt_with_policy<CLAIMS>(
    token: &str,
    jwks: &openidconnect::core::CoreJsonWebKeySet,
    metadata: &OAuthResourceServerMetadata,
    policy: &VerificationPolicy,
) -> OAuthResourceServerResult<TokenData<CLAIMS>>
where
    CLAIMS: JwtClaimsTrait,
{
    securitydept_creds::verify_token_rfc9068_with_jwks_without_jwe(
        token,
        jwks,
        |mut validation: JwtValidation| {
            apply_validation_policy(&mut validation, metadata, policy);
            Ok(validation)
        },
    )
    .map_err(|source| OAuthResourceServerError::TokenValidation { source })
}

#[cfg(feature = "jwe")]
fn verify_jwt_with_policy<CLAIMS>(
    token: &str,
    jwks: &openidconnect::core::CoreJsonWebKeySet,
    metadata: &OAuthResourceServerMetadata,
    policy: &VerificationPolicy,
) -> OAuthResourceServerResult<TokenData<CLAIMS>>
where
    CLAIMS: JwtClaimsTrait,
{
    use crate::LocalJweDecryptionKeySet;

    securitydept_creds::verify_token_rfc9068_with_jwks(
        token,
        jwks,
        &LocalJweDecryptionKeySet::new(Vec::new()),
        |mut validation: JwtValidation| {
            apply_validation_policy(&mut validation, metadata, policy);
            Ok(validation)
        },
    )
    .map_err(|source| OAuthResourceServerError::TokenValidation { source })
}

pub(super) fn apply_validation_policy(
    validation: &mut JwtValidation,
    metadata: &OAuthResourceServerMetadata,
    policy: &VerificationPolicy,
) {
    validation.leeway = policy.clock_skew().as_secs();
    validation.validate_nbf = true;
    validation.set_required_spec_claims(&["exp", "iss"]);
    validation.set_issuer(&[metadata.issuer.as_str()]);
    if !policy.allowed_audiences().is_empty() {
        validation.set_audience(policy.allowed_audiences());
    } else {
        validation.validate_aud = false;
    }
}

fn validate_rfc9068_scope_policy(
    token_data: &TokenData<TokenJwtClaims>,
    policy: &VerificationPolicy,
) -> OAuthResourceServerResult<()> {
    match token_data {
        TokenData::JWT(data) => validate_scope_policy(data.claims.scope.as_ref(), policy),
        #[cfg(feature = "jwe")]
        TokenData::JWE(data) => validate_scope_policy(data.claims().scope.as_ref(), policy),
        TokenData::Opaque => Err(OAuthResourceServerError::UnsupportedTokenFormat {
            token_format: TokenFormat::Opaque,
        }),
        #[cfg(not(feature = "jwe"))]
        _ => Err(OAuthResourceServerError::UnsupportedTokenFormat {
            token_format: TokenFormat::JWE,
        }),
    }
}

fn validate_scope_policy(
    scope: Option<&securitydept_creds::Scope>,
    policy: &VerificationPolicy,
) -> OAuthResourceServerResult<()> {
    if scope_contains_all(scope, policy.required_scopes()) {
        return Ok(());
    }

    Err(OAuthResourceServerError::PolicyViolation {
        message: if policy.required_scopes().is_empty() {
            "Access token scopes failed policy validation".to_string()
        } else {
            format!(
                "Access token is missing one or more required scopes: {}",
                policy.required_scopes().join(", ")
            )
        },
    })
}

fn should_retry_with_refreshed_jwks(error: &OAuthResourceServerError) -> bool {
    matches!(
        error,
        OAuthResourceServerError::TokenValidation {
            source: securitydept_creds::CredsError::InvalidCredentialsFormat { .. }
        }
    )
}