pas-external 0.5.0

Ppoppo Accounts System (PAS) external SDK -- OAuth2 PKCE, PASETO verification, Axum middleware, session liveness
Documentation
//! [`PasAuth`] — the single public entry point that wires a PAS OAuth2 router
//! together with a matching [`SessionValidator`].
//!
//! Construct one per application, then mount the router and share the
//! validator with whatever custom Axum middleware needs to read the
//! authenticated context. Both halves share the same [`SessionStore`] and
//! cookie name by construction, so the "write side" (OAuth callback) and the
//! "read side" (auth middleware) cannot drift out of sync.

use std::sync::Arc;

use axum::Router;

use super::config::PasAuthConfig;
use super::routes;
use super::session::SessionResolver;
use super::state::AuthState;
use super::validator::SessionValidator;
use super::sv_cache::{MemorySvBackend, SvCachePort};
use super::traits::{AccountResolver, SessionStore};
use crate::oauth::AuthClient;
use crate::pas_port::PasAuthPort;
use crate::session_liveness::TokenCipher;

/// Bundle of the PAS auth router + a matching [`SessionValidator`].
///
/// The `P` type parameter is the [`PasAuthPort`] adapter. In production
/// it defaults to [`AuthClient`]; the only consumers passing a different
/// `P` are the SDK's own boundary tests (using `MemoryPasAuth`).
pub struct PasAuth<S, P = AuthClient>
where
    S: SessionStore,
    P: PasAuthPort,
{
    router: Router,
    base_resolver: SessionResolver<S>,
    store: Arc<S>,
    pas: Arc<P>,
    default_cache: Arc<MemorySvBackend>,
    /// Mirrored from [`PasAuthConfig`]'s `with_refresh_token_cipher`. Used by
    /// resolvers to decrypt at-rest ciphertext via [`pas_refresh`]. `None`
    /// when `with_refresh_token_cipher` was not called.
    cipher: Option<Arc<TokenCipher>>,
}

// Production constructor: P = AuthClient.
impl<S: SessionStore> PasAuth<S, AuthClient> {
    /// Assemble the PAS auth router + supporting handles.
    pub fn new<U: AccountResolver>(
        config: PasAuthConfig,
        account_resolver: Arc<U>,
        session_store: Arc<S>,
    ) -> Self {
        let cookie_name: Arc<str> = Arc::from(config.settings.session_cookie_name.as_str());
        let cipher = config
            .settings
            .refresh_token_cipher
            .as_ref()
            .cloned()
            .map(Arc::new);
        let auth_client = Arc::new(config.client);

        let state = AuthState {
            client: Arc::clone(&auth_client),
            account_resolver,
            session_store: Arc::clone(&session_store),
            settings: config.settings,
        };

        let router = routes::build_router(state);
        let base_resolver = SessionResolver::new(Arc::clone(&session_store), cookie_name);

        Self {
            router,
            base_resolver,
            store: session_store,
            pas: auth_client,
            default_cache: Arc::new(MemorySvBackend::new()),
            cipher,
        }
    }
}

// Resolver-construction methods are generic over P so SDK tests can
// build a PasAuth-equivalent shape against MemoryPasAuth without going
// through PasAuthConfig.
impl<S, P> PasAuth<S, P>
where
    S: SessionStore,
    P: PasAuthPort,
{
    /// Build the default sv-aware [`SessionValidator`], backed by an
    /// in-process [`MemorySvBackend`] (60 s TTL).
    #[must_use]
    pub fn session_validator(&self) -> SessionValidator<S, P> {
        SessionValidator::new(
            self.base_resolver.clone(),
            Arc::clone(&self.store),
            Arc::clone(&self.pas),
            Arc::clone(&self.default_cache),
            self.cipher.as_ref().map(Arc::clone),
        )
    }

    /// Build a [`SessionValidator`] backed by a custom [`SvCachePort`]
    /// substrate (KVRocks, Redis, etc).
    ///
    /// Key namespace + TTL stay locked to the spec — the backend never
    /// sees them as parameters it can drift.
    #[must_use]
    pub fn session_validator_with_backend<B: SvCachePort>(
        &self,
        backend: B,
    ) -> SessionValidator<S, P, B> {
        SessionValidator::new(
            self.base_resolver.clone(),
            Arc::clone(&self.store),
            Arc::clone(&self.pas),
            Arc::new(backend),
            self.cipher.as_ref().map(Arc::clone),
        )
    }

    /// Consume the bundle and return the router.
    pub fn into_router(self) -> Router {
        self.router
    }
}