systemprompt-oauth 0.2.1

OAuth 2.0 / OIDC with PKCE, token introspection, and audience/issuer validation for systemprompt.io AI governance infrastructure. WebAuthn and JWT auth for the MCP governance pipeline.
Documentation
use super::{AnonymousSessionInfo, MAX_SESSION_AGE_SECONDS, SessionCreationService};
use crate::services::generation::{JwtSigningParams, generate_anonymous_jwt};
use systemprompt_identifiers::{ClientId, SessionId, UserId};

const SESSION_LOOKUP_TIMEOUT_MS: u64 = 500;
const MAX_SESSIONS_PER_FINGERPRINT: i64 = 5;

impl SessionCreationService {
    pub(super) async fn try_reuse_session_at_limit(
        &self,
        fingerprint: &str,
        client_id: &ClientId,
        jwt_secret: &str,
    ) -> Option<AnonymousSessionInfo> {
        let fp_provider = self.fingerprint_provider.as_ref()?;

        let active_count = fp_provider
            .count_active_sessions(fingerprint)
            .await
            .map_err(|e| {
                tracing::warn!(error = %e, fingerprint = %fingerprint, "Failed to count active sessions");
                e
            })
            .ok()?;
        if active_count < MAX_SESSIONS_PER_FINGERPRINT {
            return None;
        }

        let session_id_str = fp_provider
            .find_reusable_session(fingerprint)
            .await
            .map_err(|e| {
                tracing::warn!(error = %e, fingerprint = %fingerprint, "Failed to find reusable session");
                e
            })
            .ok()
            .flatten()?;

        let existing_session = self
            .analytics_provider
            .find_recent_session_by_fingerprint(fingerprint, MAX_SESSION_AGE_SECONDS)
            .await
            .map_err(|e| {
                tracing::warn!(error = %e, fingerprint = %fingerprint, "Failed to find recent session");
                e
            })
            .ok()
            .flatten()?;

        let user_id_str = existing_session.user_id.as_ref()?;
        let user_id = UserId::new(user_id_str.clone());
        let session_id = SessionId::new(session_id_str);

        let config = systemprompt_models::Config::get()
            .map_err(|e| {
                tracing::warn!(error = %e, "Failed to get config for session reuse");
                e
            })
            .ok()?;
        let signing = JwtSigningParams {
            secret: jwt_secret,
            issuer: &config.jwt_issuer,
        };
        let token = generate_anonymous_jwt(&user_id, &session_id, client_id, &signing)
            .map_err(|e| {
                tracing::warn!(error = %e, "Failed to generate JWT for session reuse");
                e
            })
            .ok()?;

        tracing::debug!(
            fingerprint = %fingerprint,
            session_id = %session_id,
            active_sessions = active_count,
            "Reusing session due to fingerprint session limit"
        );

        Some(AnonymousSessionInfo {
            session_id,
            user_id,
            is_new: false,
            jwt_token: token,
            fingerprint_hash: fingerprint.to_string(),
        })
    }

    pub(super) async fn try_find_existing_session(
        &self,
        fingerprint: &str,
        client_id: &ClientId,
        jwt_secret: &str,
    ) -> Option<AnonymousSessionInfo> {
        let lookup_result = tokio::time::timeout(
            tokio::time::Duration::from_millis(SESSION_LOOKUP_TIMEOUT_MS),
            self.analytics_provider
                .find_recent_session_by_fingerprint(fingerprint, MAX_SESSION_AGE_SECONDS),
        )
        .await;

        let existing_session = lookup_result
            .map_err(|_| {
                tracing::debug!(fingerprint = %fingerprint, "Session lookup timed out");
            })
            .ok()?
            .map_err(|e| {
                tracing::warn!(error = %e, fingerprint = %fingerprint, "Failed to find existing session");
                e
            })
            .ok()??;
        let user_id_str = existing_session.user_id.as_ref()?;

        let user_id = UserId::new(user_id_str.clone());
        let session_id = SessionId::new(existing_session.session_id.clone());

        let config = systemprompt_models::Config::get()
            .map_err(|e| {
                tracing::warn!(error = %e, "Failed to get config for session lookup");
                e
            })
            .ok()?;
        let signing = JwtSigningParams {
            secret: jwt_secret,
            issuer: &config.jwt_issuer,
        };
        let token = generate_anonymous_jwt(&user_id, &session_id, client_id, &signing)
            .map_err(|e| {
                tracing::warn!(error = %e, "Failed to generate JWT for session lookup");
                e
            })
            .ok()?;

        Some(AnonymousSessionInfo {
            session_id,
            user_id,
            is_new: false,
            jwt_token: token,
            fingerprint_hash: fingerprint.to_string(),
        })
    }
}