aauth 0.0.2

Rust implementation of the AAuth authorization protocol
Documentation
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use std::task::{Context, Poll};

use axum::body::Body;
use axum::http::{Request, Response, StatusCode};
use tower::{Layer, Service};

use crate::headers::{AAuthRequirementParams, build_aauth_requirement};
use crate::jwt::VerifiedToken;
use crate::metadata::MetadataFetcher;
use crate::server::deferred::{
    DeferRequirement, PendingContext, PendingKind, PendingRecord, PendingSnapshot, PendingStore,
    ResourcePendingContext, build_accepted, generate_pending_id, pending_location,
};
use crate::server::policy::{ResourceAccessContext, ResourceConsentDecision, ResourceConsentPolicy};
use crate::server::resource::audience::resolve_resource_token_audience;
use crate::server::resource::keys::ResourceTokenSigner;
use crate::server::resource::opaque::OpaqueAccessStore;
use crate::server::resource::policy::ResourceAccessMode;
use crate::server::resource::{
    ResourceTokenOptions, VerifyTokenOptions, create_resource_token, verify_token,
};
use crate::signature::verify_request_signature;
use crate::types::RequirementLevel;

#[derive(Clone)]
pub struct ResourceAuthLayer<P, S, O>
where
    P: ResourceConsentPolicy,
    S: PendingStore,
    O: OpaqueAccessStore + Clone,
{
    pub fetcher: Arc<dyn MetadataFetcher>,
    pub resource_url: String,
    pub mode: ResourceAccessMode<P, S, O>,
    pub resource_token_signer: Arc<dyn ResourceTokenSigner>,
}

impl<P, S, O> ResourceAuthLayer<P, S, O>
where
    P: ResourceConsentPolicy,
    S: PendingStore,
    O: OpaqueAccessStore + Clone,
{
    pub fn new(
        fetcher: Arc<dyn MetadataFetcher>,
        resource_url: impl Into<String>,
        mode: ResourceAccessMode<P, S, O>,
        resource_token_signer: Arc<dyn ResourceTokenSigner>,
    ) -> Self {
        Self {
            fetcher,
            resource_url: resource_url.into(),
            mode,
            resource_token_signer,
        }
    }
}

impl<S, P, St, O> Layer<S> for ResourceAuthLayer<P, St, O>
where
    P: ResourceConsentPolicy,
    St: PendingStore,
    O: OpaqueAccessStore + Clone,
{
    type Service = ResourceAuthService<S, P, St, O>;

    fn layer(&self, inner: S) -> Self::Service {
        ResourceAuthService {
            inner,
            fetcher: Arc::clone(&self.fetcher),
            resource_url: self.resource_url.clone(),
            mode: self.mode.clone(),
            resource_token_signer: Arc::clone(&self.resource_token_signer),
        }
    }
}

#[derive(Clone)]
pub struct ResourceAuthService<S, P, St, O>
where
    P: ResourceConsentPolicy,
    St: PendingStore,
    O: OpaqueAccessStore + Clone,
{
    inner: S,
    fetcher: Arc<dyn MetadataFetcher>,
    resource_url: String,
    mode: ResourceAccessMode<P, St, O>,
    resource_token_signer: Arc<dyn ResourceTokenSigner>,
}

impl<S, B, P, St, O> Service<Request<B>> for ResourceAuthService<S, P, St, O>
where
    S: Service<Request<B>, Response = Response<Body>> + Clone + Send + 'static,
    S::Future: Send + 'static,
    S::Error: std::fmt::Display + Send + 'static,
    B: Send + 'static,
    P: ResourceConsentPolicy + Clone + Send + Sync + 'static,
    St: PendingStore + Clone + Send + Sync + 'static,
    O: OpaqueAccessStore + Clone + Send + Sync + 'static,
{
    type Response = Response<Body>;
    type Error = S::Error;
    type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;

    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
        self.inner.poll_ready(cx)
    }

    fn call(&mut self, mut req: Request<B>) -> Self::Future {
        let fetcher = Arc::clone(&self.fetcher);
        let resource_url = self.resource_url.clone();
        let mode = self.mode.clone();
        let resource_token_signer = Arc::clone(&self.resource_token_signer);
        let mut inner = self.inner.clone();

        Box::pin(async move {
            let (method, authority, path) = request_signature_parts(&req);

            let verified_sig =
                match verify_request_signature(&method, &authority, &path, req.headers()) {
                    Ok(v) => v,
                    Err(e) => return Ok(unauthorized(e.to_string())),
                };

            if let ResourceAccessMode::ResourceManaged { opaque, .. } = &mode {
                if let Some(opaque_token) = extract_aauth_access(req.headers()) {
                    let agent_iss = agent_iss_from_jwt(&verified_sig.jwt);
                    if opaque.validate(&opaque_token, &agent_iss) {
                        if let Ok(VerifiedToken::Agent(agent)) =
                            VerifiedToken::decode_unverified(&verified_sig.jwt)
                        {
                            req.extensions_mut().insert(VerifiedToken::Agent(agent));
                            return inner.call(req).await;
                        }
                    }
                    return Ok(unauthorized("invalid opaque access token".into()));
                }
            }

            let verified = match verify_token(VerifyTokenOptions {
                jwt: verified_sig.jwt.clone(),
                http_signature_thumbprint: verified_sig.thumbprint.clone(),
                fetcher: Arc::clone(&fetcher),
            })
            .await
            {
                Ok(v) => v,
                Err(e) => return Ok(unauthorized(e.to_string())),
            };

            match (&mode, &verified) {
                (ResourceAccessMode::IdentityBased, _) => {
                    req.extensions_mut().insert(verified);
                    inner.call(req).await
                }
                (
                    ResourceAccessMode::PsAsserted {
                        require_auth_token: true,
                        access_server_url,
                        person_server_fallback,
                    },
                    VerifiedToken::Agent(agent),
                ) => {
                    let audience = match resolve_resource_token_audience(
                        agent,
                        access_server_url.as_deref(),
                        person_server_fallback.as_deref(),
                    ) {
                        Ok(aud) => aud,
                        Err(e) => return Ok(unauthorized(e.to_string())),
                    };

                    let resource_token = match create_resource_token(
                        ResourceTokenOptions {
                            resource: resource_url,
                            audience,
                            agent: agent.iss.clone(),
                            agent_jkt: verified_sig.thumbprint,
                            scope: None,
                            mission: None,
                            lifetime: None,
                        },
                        resource_token_signer.as_ref(),
                    )
                    .await
                    {
                        Ok(token) => token,
                        Err(e) => return Ok(unauthorized(e)),
                    };

                    let header = match build_aauth_requirement(
                        RequirementLevel::AuthToken,
                        Some(&AAuthRequirementParams {
                            resource_token: Some(&resource_token),
                            ..Default::default()
                        }),
                    ) {
                        Ok(h) => h,
                        Err(e) => return Ok(unauthorized(e.to_string())),
                    };

                    Ok(Response::builder()
                        .status(StatusCode::UNAUTHORIZED)
                        .header("AAuth-Requirement", header)
                        .body(Body::from("Auth token required"))
                        .expect("valid response"))
                }
                (ResourceAccessMode::PsAsserted { .. }, _) => {
                    req.extensions_mut().insert(verified);
                    inner.call(req).await
                }
                (
                    ResourceAccessMode::ResourceManaged {
                        policy,
                        pending,
                        opaque,
                        interaction_url,
                        pending_base_url,
                        pending_path,
                        pending_ttl_secs,
                    },
                    VerifiedToken::Agent(agent),
                ) => {
                    let ctx = ResourceAccessContext {
                        resource_url: resource_url.clone(),
                        agent_claims: agent.clone(),
                        scope: None,
                    };

                    match policy.evaluate(&ctx).await {
                        Ok(ResourceConsentDecision::GrantOpaque) => {
                            let token = opaque.issue(&agent.iss);
                            let mut response = Response::builder().status(StatusCode::OK);
                            response = response.header("AAuth-Access", token.as_str());
                            Ok(response.body(Body::empty()).expect("valid response"))
                        }
                        Ok(ResourceConsentDecision::Deny(err)) => Ok(Response::builder()
                            .status(StatusCode::FORBIDDEN)
                            .body(Body::from(err.error))
                            .expect("valid response")),
                        Ok(ResourceConsentDecision::Defer(mut requirement)) => {
                            if let DeferRequirement::Interaction { url, code } = &mut requirement {
                                if url.is_empty() {
                                    *url = interaction_url.clone();
                                }
                                if code.is_empty() {
                                    *code = crate::interaction_code::generate_code();
                                }
                            }
                            let id = generate_pending_id();
                            let location =
                                pending_location(pending_base_url, pending_path, &id);
                            let record = PendingRecord::new(
                                id,
                                PendingKind::ResourceAccess,
                                PendingContext::Resource(ResourcePendingContext {
                                    resource_url: resource_url.clone(),
                                    agent_claims: agent.clone(),
                                    scope: None,
                                }),
                                PendingSnapshot::waiting(requirement.clone()),
                                *pending_ttl_secs,
                            );
                            if pending.create(record).await.is_err() {
                                return Ok(unauthorized("pending store error".into()));
                            }
                            match build_accepted(&location, &requirement) {
                                Ok(accepted) => {
                                    let mut response = Response::builder().status(accepted.status);
                                    for (k, v) in accepted.headers.iter() {
                                        response = response.header(k, v);
                                    }
                                    Ok(response.body(Body::empty()).expect("valid response"))
                                }
                                Err(e) => Ok(unauthorized(e.to_string())),
                            }
                        }
                        Err(e) => Ok(unauthorized(e.to_string())),
                    }
                }
                (ResourceAccessMode::ResourceManaged { .. }, VerifiedToken::Auth(_)) => {
                    req.extensions_mut().insert(verified);
                    inner.call(req).await
                }
            }
        })
    }
}

fn agent_iss_from_jwt(jwt: &str) -> String {
    VerifiedToken::decode_unverified(jwt)
        .map(|t| t.iss().to_string())
        .unwrap_or_default()
}

fn extract_aauth_access(headers: &axum::http::HeaderMap) -> Option<String> {
    let value = headers.get("authorization").and_then(|v| v.to_str().ok())?;
    let rest = value.strip_prefix("AAuth ")?;
    if rest.is_empty() {
        return None;
    }
    Some(rest.to_string())
}

fn request_signature_parts<B>(req: &Request<B>) -> (String, String, String) {
    let method = req.method().as_str().to_string();
    let uri = req.uri();
    let authority = req
        .headers()
        .get("host")
        .and_then(|h| h.to_str().ok())
        .map(str::to_string)
        .unwrap_or_else(|| {
            uri.host()
                .map(|host| match uri.port_u16() {
                    Some(port) => format!("{host}:{port}"),
                    None => host.to_string(),
                })
                .unwrap_or_default()
        });
    let path = uri.path().to_string();
    (method, authority, path)
}

fn unauthorized(message: String) -> Response<Body> {
    Response::builder()
        .status(StatusCode::UNAUTHORIZED)
        .body(Body::from(message))
        .expect("valid response")
}