securitydept-oauth-resource-server 0.2.0

OAuth Resource Server of SecurityDept, a layered authentication and authorization toolkit built as reusable Rust crates.
Documentation
use oauth2::TokenIntrospectionResponse;

use crate::{
    OAuthResourceServerError, OAuthResourceServerIntrospectionConfig, OAuthResourceServerMetadata,
    OAuthResourceServerResult, VerificationPolicy, VerifiedOpaqueToken,
};

pub(super) struct OAuthResourceServerVerifierIntrospection {
    config: OAuthResourceServerIntrospectionConfig,
}

impl OAuthResourceServerVerifierIntrospection {
    pub fn from_config(config: &OAuthResourceServerIntrospectionConfig) -> Self {
        Self {
            config: config.clone(),
        }
    }

    pub async fn introspect(
        &self,
        token: &str,
        provider: &securitydept_oauth_provider::OAuthProviderRuntime,
        policy: &VerificationPolicy,
    ) -> OAuthResourceServerResult<VerifiedOpaqueToken> {
        let metadata = provider.metadata().await?;
        let response = provider
            .introspect(
                self.config.client_id.as_deref().unwrap_or_default(),
                self.config.client_secret.as_deref(),
                token,
                self.config.token_type_hint.as_deref(),
            )
            .await?;

        let resource_metadata = OAuthResourceServerMetadata {
            issuer: metadata.issuer,
            jwks_uri: metadata.jwks_uri,
            introspection_url: metadata.introspection_endpoint,
        };

        validate_introspection_response(&response, &resource_metadata, policy)?;

        Ok(VerifiedOpaqueToken {
            response,
            metadata: resource_metadata,
        })
    }
}

fn validate_introspection_response(
    response: &openidconnect::core::CoreTokenIntrospectionResponse,
    metadata: &OAuthResourceServerMetadata,
    policy: &VerificationPolicy,
) -> OAuthResourceServerResult<()> {
    if !response.active() {
        return Err(OAuthResourceServerError::PolicyViolation {
            message: "Opaque token is not active".to_string(),
        });
    }

    if let Some(issuer) = response.iss()
        && issuer != metadata.issuer.as_str()
    {
        return Err(OAuthResourceServerError::PolicyViolation {
            message: format!(
                "Opaque token issuer mismatch: expected {}, got {issuer}",
                metadata.issuer
            ),
        });
    }

    if !policy.allowed_audiences().is_empty() {
        let audience = response
            .aud()
            .ok_or_else(|| OAuthResourceServerError::PolicyViolation {
                message: "Opaque token is missing audience".to_string(),
            })?;
        let has_match = audience.iter().any(|value| {
            policy
                .allowed_audiences()
                .iter()
                .any(|expected| expected == value)
        });
        if !has_match {
            return Err(OAuthResourceServerError::PolicyViolation {
                message: "Opaque token audience does not satisfy verifier policy".to_string(),
            });
        }
    }

    if !policy.required_scopes().is_empty() {
        let scopes =
            response
                .scopes()
                .ok_or_else(|| OAuthResourceServerError::PolicyViolation {
                    message: "Opaque token is missing scopes".to_string(),
                })?;
        let has_all = policy
            .required_scopes()
            .iter()
            .all(|required| scopes.iter().any(|scope| scope.as_ref() == required));
        if !has_all {
            return Err(OAuthResourceServerError::PolicyViolation {
                message: format!(
                    "Opaque token is missing one or more required scopes: {}",
                    policy.required_scopes().join(", ")
                ),
            });
        }
    }

    Ok(())
}