Skip to main content

systemprompt_api/services/middleware/session/
attestation.rs

1//! Server-side attestation of a claimed session id.
2//!
3//! A session id is only evidence if the server issued it. [`attest_session`] is
4//! the single predicate both credential paths use: the JWT middleware checks
5//! the `session_id` claim with it, and the gateway checks the `x-session-id`
6//! header presented alongside an API key with it. Keeping one implementation is
7//! the point — two copies would drift, and the audit spine would then mean
8//! different things depending on which credential wrote the row.
9//!
10//! Copyright (c) systemprompt.io — Business Source License 1.1.
11//! See <https://systemprompt.io> for licensing details.
12
13use std::sync::Arc;
14use systemprompt_identifiers::{SessionId, UserId};
15use systemprompt_traits::AnalyticsProvider;
16
17#[derive(Debug, thiserror::Error)]
18pub enum SessionAttestationError {
19    #[error("Session missing or revoked")]
20    Missing,
21    #[error("Session user mismatch")]
22    UserMismatch,
23    #[error("Failed to check session: {0}")]
24    Lookup(String),
25}
26
27pub async fn attest_session(
28    analytics_provider: &Arc<dyn AnalyticsProvider>,
29    session_id: &SessionId,
30    user_id: &UserId,
31    route_context: &str,
32) -> Result<(), SessionAttestationError> {
33    let session = analytics_provider
34        .find_active_session_by_id(session_id)
35        .await
36        .map_err(|e| SessionAttestationError::Lookup(e.to_string()))?;
37
38    let Some(session) = session else {
39        tracing::info!(
40            session_id = %session_id.as_str(),
41            user_id = %user_id.as_str(),
42            route = %route_context,
43            "session attestation failed: session missing or revoked"
44        );
45        return Err(SessionAttestationError::Missing);
46    };
47
48    if let Some(session_user_id) = session.user_id.as_ref()
49        && session_user_id.as_str() != user_id.as_str()
50    {
51        tracing::warn!(
52            session_id = %session_id.as_str(),
53            claimed_user_id = %user_id.as_str(),
54            session_user_id = %session_user_id.as_str(),
55            route = %route_context,
56            "session attestation failed: session user mismatch"
57        );
58        return Err(SessionAttestationError::UserMismatch);
59    }
60
61    Ok(())
62}