systemprompt_api/services/middleware/session/
mod.rs1mod attestation;
25mod context;
26mod lifecycle;
27mod skip;
28
29pub use attestation::{SessionAttestationError, attest_session};
30pub use skip::should_skip_session_tracking;
31
32use axum::extract::{ConnectInfo, Request};
33use axum::http::header;
34use axum::middleware::Next;
35use axum::response::Response;
36use ipnet::IpNet;
37use std::net::SocketAddr;
38use std::sync::Arc;
39use systemprompt_analytics::{AnalyticsService, SessionAnalytics};
40use systemprompt_models::api::ApiError;
41use systemprompt_oauth::services::SessionCreationService;
42use systemprompt_runtime::AppContext;
43use systemprompt_security::{CookieExtractor, HeaderExtractor};
44use systemprompt_traits::{AnalyticsProvider, ExtractSignals};
45use systemprompt_users::UserService;
46
47struct RequestMeta<'a> {
48 headers: &'a http::HeaderMap,
49 uri: &'a http::Uri,
50 analytics: &'a SessionAnalytics,
51}
52
53#[derive(Clone, Debug)]
54pub struct SessionMiddleware {
55 analytics_service: Arc<AnalyticsService>,
56 session_creation_service: Arc<SessionCreationService>,
57 trusted_proxies: Arc<Vec<IpNet>>,
58 ignored_forwarded_warn: Arc<systemprompt_logging::LogThrottle>,
59 degraded_warn: Arc<systemprompt_logging::LogThrottle>,
60}
61
62const IGNORED_FORWARDED_WARN_INTERVAL_SECS: u64 = 3600;
63const DEGRADED_WARN_INTERVAL_SECS: u64 = 60;
64
65impl SessionMiddleware {
66 pub fn new(ctx: &AppContext) -> Self {
67 let user_service = UserService::new(Arc::clone(ctx.user_repository()));
68 let concrete = Arc::clone(ctx.analytics_service());
69 let analytics: Arc<dyn AnalyticsProvider> = concrete;
70 let session_creation_service = Arc::new(SessionCreationService::new(
71 analytics,
72 Arc::new(user_service),
73 ));
74
75 Self {
76 analytics_service: Arc::clone(ctx.analytics_service()),
77 session_creation_service,
78 trusted_proxies: Arc::new(ctx.config().trusted_proxies.clone()),
79 ignored_forwarded_warn: Arc::new(systemprompt_logging::LogThrottle::new(
80 IGNORED_FORWARDED_WARN_INTERVAL_SECS,
81 )),
82 degraded_warn: Arc::new(systemprompt_logging::LogThrottle::new(
83 DEGRADED_WARN_INTERVAL_SECS,
84 )),
85 }
86 }
87
88 pub async fn handle(&self, mut request: Request, next: Next) -> Result<Response, ApiError> {
89 let caller_ip = super::client_addr::resolve_client_ip(
90 request.headers(),
91 request.extensions().get::<ConnectInfo<SocketAddr>>(),
92 &self.trusted_proxies,
93 );
94 if let Some(peer) = request.extensions().get::<ConnectInfo<SocketAddr>>()
95 && super::client_addr::forwarded_headers_ignored(
96 request.headers(),
97 peer.0.ip(),
98 &self.trusted_proxies,
99 )
100 && self.ignored_forwarded_warn.allow()
101 {
102 tracing::warn!(
103 peer_ip = %peer.0.ip(),
104 "ignoring forwarded client-IP headers from untrusted private peer; if this \
105 server runs behind a proxy, add the peer's range to server.trusted_proxies"
106 );
107 }
108 let uri = request.uri().clone();
109 let headers = request.headers();
110 let analytics = self.analytics_service.extract_analytics(
111 headers,
112 ExtractSignals {
113 uri: Some(&uri),
114 caller_ip,
115 },
116 );
117 let meta = RequestMeta {
118 headers,
119 uri: &uri,
120 analytics: &analytics,
121 };
122
123 let should_skip = should_skip_session_tracking(uri.path());
124
125 tracing::debug!(
126 path = %uri.path(),
127 should_skip = should_skip,
128 "Session middleware evaluating request"
129 );
130
131 let trace_id = HeaderExtractor::extract_trace_id(headers);
132
133 let (req_ctx, jwt_cookie) = self
134 .establish_or_degrade(should_skip, trace_id, &meta, uri.path())
135 .await;
136
137 tracing::debug!(
138 path = %uri.path(),
139 session_id = %req_ctx.session_id(),
140 "Session middleware setting context"
141 );
142
143 request.extensions_mut().insert(req_ctx);
144
145 let mut response = next.run(request).await;
146
147 if let Some(token) = jwt_cookie {
148 let cookie = format!(
149 "{}={token}; HttpOnly; SameSite=Strict; Path=/; Max-Age=604800",
150 CookieExtractor::DEFAULT_COOKIE_NAME
151 );
152 if let Ok(cookie_value) = cookie.parse() {
153 response
154 .headers_mut()
155 .insert(header::SET_COOKIE, cookie_value);
156 }
157 }
158
159 Ok(response)
160 }
161}