pas-external 4.0.2

Ppoppo Accounts System (PAS) external SDK -- OAuth2 PKCE, PASETO verification, Axum middleware, session liveness
Documentation
//! Session resolution handle for custom Axum middleware.
//!
//! [`SessionResolver`] is a cheap, cloneable handle owned by consumers that
//! encapsulates both the session cookie name and the [`SessionStore`] backing
//! it. Consumers never touch the cookie name directly — that's an internal
//! detail of the PAS middleware — which eliminates the dual-SSOT class of bugs
//! where the name on the write side (OAuth callback) drifts from the name on
//! the read side (auth middleware).

use std::sync::Arc;

use axum_extra::extract::PrivateCookieJar;

use super::traits::SessionStore;
use crate::types::SessionId;

/// Result of resolving a session cookie against the [`SessionStore`].
///
/// Consumers must handle all three variants — the tri-state is intentional so
/// that the compiler can enforce the distinction between "no cookie at all"
/// (a first-time visitor) and "cookie present but stale" (a visitor whose
/// session was invalidated or expired, and who should likely see a
/// `?reason=session_expired` signal).
#[derive(Debug)]
pub enum SessionResolution<A> {
    /// No session cookie was presented.
    NoCookie,
    /// A session cookie was presented but the stored session is missing,
    /// expired, or otherwise invalid. The cookie should be cleared and the
    /// user routed to login.
    Expired,
    /// The session cookie resolved to a live authentication context.
    Authenticated(A),
}

/// Cheap cloneable handle used by custom middleware to resolve a session from
/// a [`PrivateCookieJar`].
///
/// Obtain one from [`super::PasAuth::resolver`] after constructing [`super::PasAuth`]
/// so that the router (write side) and your middleware (read side) agree on
/// the cookie name by construction.
pub struct SessionResolver<S: SessionStore> {
    session_store: Arc<S>,
    cookie_name: Arc<str>,
}

// Manual Clone avoids adding `S: Clone` bound.
impl<S: SessionStore> Clone for SessionResolver<S> {
    fn clone(&self) -> Self {
        Self {
            session_store: Arc::clone(&self.session_store),
            cookie_name: Arc::clone(&self.cookie_name),
        }
    }
}

impl<S: SessionStore> SessionResolver<S> {
    pub(super) fn new(session_store: Arc<S>, cookie_name: Arc<str>) -> Self {
        Self {
            session_store,
            cookie_name,
        }
    }

    /// Crate-internal accessor used by [`super::SvAwareSessionResolver`]
    /// to extract the session_id from the cookie jar during the refresh
    /// path. Not part of the public API — consumers don't construct
    /// session ids by hand; they receive `SessionId` from the resolved
    /// `AuthContext`.
    pub(super) fn cookie_name(&self) -> &str {
        &self.cookie_name
    }

    /// Resolve a session cookie into an authentication context.
    ///
    /// # Errors
    ///
    /// Returns the underlying [`SessionStore::Error`] if the store lookup
    /// fails. Missing cookies and missing sessions are represented as
    /// [`SessionResolution::NoCookie`] / [`SessionResolution::Expired`]
    /// and are *not* errors.
    pub async fn resolve(
        &self,
        jar: &PrivateCookieJar,
    ) -> Result<SessionResolution<S::AuthContext>, S::Error> {
        let Some(cookie) = jar.get(&self.cookie_name) else {
            return Ok(SessionResolution::NoCookie);
        };
        let session_id = SessionId(cookie.value().to_string());
        match self.session_store.find(&session_id).await? {
            Some(ctx) => Ok(SessionResolution::Authenticated(ctx)),
            None => Ok(SessionResolution::Expired),
        }
    }
}