systemprompt_api/services/middleware/session/
mod.rs1mod lifecycle;
12mod skip;
13
14pub use skip::should_skip_session_tracking;
15
16use axum::extract::Request;
17use axum::http::header;
18use axum::middleware::Next;
19use axum::response::Response;
20use std::sync::Arc;
21use systemprompt_analytics::AnalyticsService;
22use systemprompt_identifiers::{AgentName, ContextId, SessionId, UserId};
23use systemprompt_models::api::ApiError;
24use systemprompt_models::auth::UserType;
25use systemprompt_models::execution::context::RequestContext;
26use systemprompt_oauth::services::SessionCreationService;
27use systemprompt_runtime::AppContext;
28use systemprompt_security::{
29 CookieExtractor, HeaderExtractor, TokenExtractor, extract_user_context,
30};
31use systemprompt_traits::AnalyticsProvider;
32use systemprompt_users::UserService;
33use uuid::Uuid;
34
35#[derive(Clone, Debug)]
36pub struct SessionMiddleware {
37 analytics_service: Arc<AnalyticsService>,
38 session_creation_service: Arc<SessionCreationService>,
39}
40
41impl SessionMiddleware {
42 pub fn new(ctx: &AppContext) -> anyhow::Result<Self> {
43 let user_service = UserService::new(ctx.db_pool())?;
44 let concrete = Arc::clone(ctx.analytics_service());
45 let analytics: Arc<dyn AnalyticsProvider> = concrete;
46 let session_creation_service = Arc::new(SessionCreationService::new(
47 analytics,
48 Arc::new(user_service),
49 ));
50
51 Ok(Self {
52 analytics_service: Arc::clone(ctx.analytics_service()),
53 session_creation_service,
54 })
55 }
56
57 pub async fn handle(&self, mut request: Request, next: Next) -> Result<Response, ApiError> {
58 let headers = request.headers();
59 let uri = request.uri().clone();
60 let method = request.method().clone();
61
62 let should_skip = should_skip_session_tracking(uri.path());
63
64 tracing::debug!(
65 path = %uri.path(),
66 should_skip = should_skip,
67 "Session middleware evaluating request"
68 );
69
70 let trace_id = HeaderExtractor::extract_trace_id(headers);
71
72 let (req_ctx, jwt_cookie) = if should_skip {
73 (
74 self.anonymous_context("untracked", trace_id, headers, &uri)
75 .await?,
76 None,
77 )
78 } else {
79 self.tracked_context(trace_id, headers, &uri, &method)
80 .await?
81 };
82
83 tracing::debug!(
84 path = %uri.path(),
85 session_id = %req_ctx.session_id(),
86 "Session middleware setting context"
87 );
88
89 request.extensions_mut().insert(req_ctx);
90
91 let mut response = next.run(request).await;
92
93 if let Some(token) = jwt_cookie {
94 let cookie = format!(
95 "{}={token}; HttpOnly; SameSite=Strict; Path=/; Max-Age=604800",
96 CookieExtractor::DEFAULT_COOKIE_NAME
97 );
98 if let Ok(cookie_value) = cookie.parse() {
99 response
100 .headers_mut()
101 .insert(header::SET_COOKIE, cookie_value);
102 }
103 }
104
105 Ok(response)
106 }
107
108 async fn anonymous_context(
113 &self,
114 session_prefix: &str,
115 trace_id: systemprompt_identifiers::TraceId,
116 headers: &http::HeaderMap,
117 uri: &http::Uri,
118 ) -> Result<RequestContext, ApiError> {
119 let (user_id, fingerprint) = self
120 .session_creation_service
121 .ensure_anonymous_user(headers, Some(uri))
122 .await
123 .map_err(|e| {
124 tracing::error!(error = %e, session_prefix, "Failed to ensure anonymous user");
125 ApiError::internal_error("Service temporarily unavailable")
126 })?;
127
128 Ok(RequestContext::new(
129 SessionId::new(format!("{session_prefix}_{}", Uuid::new_v4())),
130 trace_id,
131 ContextId::generate(),
132 AgentName::system(),
133 )
134 .with_actor(systemprompt_identifiers::Actor::anonymous(user_id))
135 .with_user_type(UserType::Anon)
136 .with_tracked(false)
137 .with_fingerprint_hash(fingerprint))
138 }
139
140 async fn tracked_context(
141 &self,
142 trace_id: systemprompt_identifiers::TraceId,
143 headers: &http::HeaderMap,
144 uri: &http::Uri,
145 method: &http::Method,
146 ) -> Result<(RequestContext, Option<String>), ApiError> {
147 let analytics = self.analytics_service.extract_analytics(headers, Some(uri));
148 let is_bot = AnalyticsService::is_bot(&analytics);
149
150 tracing::debug!(
151 path = %uri.path(),
152 is_bot = is_bot,
153 user_agent = ?analytics.user_agent,
154 "Session middleware bot check"
155 );
156
157 if is_bot {
158 return Ok((
159 self.anonymous_context("bot", trace_id, headers, uri)
160 .await?,
161 None,
162 ));
163 }
164
165 let token_result = TokenExtractor::browser_only().extract(headers).ok();
166
167 let (session_id, user_id, jwt_token, jwt_cookie, fingerprint_hash) = self
168 .resolve_session(token_result, headers, uri, method)
169 .await?;
170
171 let context_id =
172 HeaderExtractor::extract_context_id(headers).unwrap_or_else(ContextId::generate);
173
174 let mut ctx = RequestContext::new(session_id, trace_id, context_id, AgentName::system())
175 .with_actor(systemprompt_identifiers::Actor::user(user_id))
176 .with_auth_token(jwt_token)
177 .with_user_type(UserType::Anon)
178 .with_tracked(true);
179 if let Some(fp) = fingerprint_hash {
180 ctx = ctx.with_fingerprint_hash(fp);
181 }
182 Ok((ctx, jwt_cookie))
183 }
184
185 async fn resolve_session(
186 &self,
187 token_result: Option<String>,
188 headers: &http::HeaderMap,
189 uri: &http::Uri,
190 method: &http::Method,
191 ) -> Result<(SessionId, UserId, String, Option<String>, Option<String>), ApiError> {
192 let Some(token) = token_result else {
193 let (sid, uid, token, is_new, fp) =
194 lifecycle::create_new_session(&self.session_creation_service, headers, uri, method)
195 .await?;
196 let jwt_cookie = if is_new { Some(token.clone()) } else { None };
197 return Ok((sid, uid, token, jwt_cookie, Some(fp)));
198 };
199
200 let Ok(jwt_context) = extract_user_context(&token) else {
201 let (sid, uid, token, is_new, fp) =
202 lifecycle::create_new_session(&self.session_creation_service, headers, uri, method)
203 .await?;
204 let jwt_cookie = if is_new { Some(token.clone()) } else { None };
205 return Ok((sid, uid, token, jwt_cookie, Some(fp)));
206 };
207
208 let session_exists = self
209 .analytics_service
210 .find_active_session_by_id(&jwt_context.session_id)
211 .await
212 .map_err(|e| {
213 tracing::warn!(error = %e, "find_active_session_by_id failed");
214 e
215 })
216 .ok()
217 .flatten()
218 .is_some();
219
220 if session_exists {
221 return Ok((
222 jwt_context.session_id,
223 jwt_context.user_id,
224 token,
225 None,
226 None,
227 ));
228 }
229
230 tracing::info!(
231 old_session_id = %jwt_context.session_id,
232 user_id = %jwt_context.user_id,
233 "JWT valid but session missing, refreshing with new session"
234 );
235 match lifecycle::refresh_session_for_user(
236 &self.session_creation_service,
237 &self.analytics_service,
238 &jwt_context.user_id,
239 headers,
240 uri,
241 )
242 .await
243 {
244 Ok((sid, uid, new_token, _, fp)) => {
245 Ok((sid, uid, new_token.clone(), Some(new_token), Some(fp)))
246 },
247 Err(e) if e.error_key.as_deref() == Some("user_not_found") => {
248 tracing::warn!(
249 user_id = %jwt_context.user_id,
250 "JWT references non-existent user, creating new anonymous session"
251 );
252 let (sid, uid, token, _, fp) = lifecycle::create_new_session(
253 &self.session_creation_service,
254 headers,
255 uri,
256 method,
257 )
258 .await?;
259 Ok((sid, uid, token.clone(), Some(token), Some(fp)))
260 },
261 Err(e) => Err(e),
262 }
263 }
264}