aauth 0.0.2

Rust implementation of the AAuth authorization protocol
Documentation
use std::sync::Arc;

use jsonwebtoken::{DecodingKey, decode_header};

use crate::error::{AAuthError, Result, TokenError};
use crate::jwt::{ResourceClaims, VerifiedToken, decode_resource_token_unverified, jwk_thumbprint};
use crate::metadata::MetadataFetcher;
use crate::types::JwtTyp;

const CLOCK_SKEW: i64 = 60;

pub struct VerifyTokenOptions {
    pub jwt: String,
    pub http_signature_thumbprint: String,
    pub fetcher: Arc<dyn MetadataFetcher>,
}

pub struct VerifyResourceTokenOptions {
    pub jwt: String,
    pub expected_agent: Option<String>,
    pub expected_agent_jkt: Option<String>,
    pub fetcher: Arc<dyn MetadataFetcher>,
}

pub async fn verify_token(options: VerifyTokenOptions) -> Result<VerifiedToken> {
    let typ = JwtTyp::from_jwt(&options.jwt)?;
    let error_code = match typ {
        JwtTyp::Agent | JwtTyp::Auth => typ.verify_error_code(),
        JwtTyp::Resource => {
            return Err(TokenError::new(
                typ.verify_error_code(),
                format!("Unsupported JWT typ for verification: {typ}"),
            )
            .into());
        }
    };

    let claims = VerifiedToken::decode_unverified(&options.jwt)?;

    let now = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap()
        .as_secs() as i64;
    if claims.exp() < now - CLOCK_SKEW {
        return Err(TokenError::new(error_code, "Token has expired").into());
    }

    let cnf_thumbprint = jwk_thumbprint(claims.cnf_jwk())?;
    if cnf_thumbprint != options.http_signature_thumbprint {
        return Err(TokenError::new(
            "key_binding_failed",
            "cnf.jwk thumbprint does not match HTTP signature key",
        )
        .into());
    }

    let decoding_key = fetch_decoding_key(
        claims.iss(),
        claims.dwk(),
        error_code,
        &options.fetcher,
        &options.jwt,
    )
    .await?;

    VerifiedToken::decode_verified(&options.jwt, &decoding_key).map_err(|e| {
        if let AAuthError::Token { code, message } = e {
            TokenError::new(code, message)
        } else {
            TokenError::new(
                error_code,
                format!("JWT signature verification failed: {e}"),
            )
        }
        .into()
    })
}

pub async fn verify_resource_token(options: VerifyResourceTokenOptions) -> Result<ResourceClaims> {
    let error_code = JwtTyp::Resource.verify_error_code();
    let claims = decode_resource_token_unverified(&options.jwt)?;

    let now = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap()
        .as_secs() as i64;
    if (claims.exp as i64) < now - CLOCK_SKEW {
        return Err(TokenError::new(error_code, "Resource token has expired").into());
    }

    if claims.dwk != "aauth-resource.json" {
        return Err(TokenError::new(error_code, "invalid resource token dwk").into());
    }

    if let Some(expected) = &options.expected_agent {
        if &claims.agent != expected {
            return Err(TokenError::new(error_code, "resource token agent mismatch").into());
        }
    }

    if let Some(expected_jkt) = &options.expected_agent_jkt {
        if &claims.agent_jkt != expected_jkt {
            return Err(TokenError::new(error_code, "resource token agent_jkt mismatch").into());
        }
    }

    let decoding_key = fetch_decoding_key(
        &claims.iss,
        &claims.dwk,
        error_code,
        &options.fetcher,
        &options.jwt,
    )
    .await?;

    jsonwebtoken::decode::<ResourceClaims>(
        &options.jwt,
        &decoding_key,
        &crate::jwt::verified_validation(),
    )
    .map(|data| data.claims)
    .map_err(|e| {
        TokenError::new(
            error_code,
            format!("Resource token signature verification failed: {e}"),
        )
        .into()
    })
}

async fn fetch_decoding_key(
    iss: &str,
    dwk: &str,
    error_code: &str,
    fetcher: &Arc<dyn MetadataFetcher>,
    jwt: &str,
) -> Result<DecodingKey> {
    let jwks_uri = fetcher
        .resolve_jwks_uri(iss, dwk)
        .await
        .map_err(|e| match e {
            AAuthError::Token { code, message } => TokenError::new(code, message),
            other => TokenError::new("metadata_fetch_failed", other.to_string()),
        })?;

    let jwks = fetcher.fetch_jwks(&jwks_uri).await.map_err(|e| {
        TokenError::new(
            error_code,
            format!("Failed to fetch JWKS from {jwks_uri}: {e}"),
        )
    })?;

    let header = decode_header(jwt).map_err(|e| AAuthError::Message(e.to_string()))?;
    let kid = header
        .kid
        .ok_or_else(|| AAuthError::Message("missing kid".into()))?;

    let jwk = jwks
        .find(&kid)
        .ok_or_else(|| AAuthError::Message(format!("unknown kid: {kid}")))?;

    DecodingKey::from_jwk(jwk).map_err(|e| AAuthError::Message(e.to_string()).into())
}