1use super::{
2 CompletedSession, IssuedSession, IssuedSessionRecord, SessionIssueError, SessionOverrides,
3};
4use alibi_core::entity::AuthUser;
5use alibi_core::{AuthContext, AuthError, AuthRequest, AuthResponse, AuthResult, UpdateUser};
6use chrono::Utc;
7
8pub(crate) async fn ordinary_session<S: alibi_core::AuthSchema>(
12 request: &AuthRequest,
13 ctx: &AuthContext<S>,
14) -> AuthResult<(alibi_core::AuthenticatedUser<S>, alibi_core::SessionView)> {
15 ctx.require_cached_session(request)
16 .await
17 .map_err(super::unauthorized_if_unauthenticated)
18}
19
20pub fn expires_in_to_at(expires_in_secs: Option<i64>) -> AuthResult<Option<String>> {
29 expires_in_secs
30 .map(|secs| {
31 chrono::Duration::try_seconds(secs)
32 .and_then(|duration| Utc::now().checked_add_signed(duration))
33 .map(|expires_at| expires_at.to_rfc3339())
34 .ok_or_else(|| AuthError::bad_request("expiresIn is out of range"))
35 })
36 .transpose()
37}
38
39pub async fn response_session<S: alibi_core::AuthSchema>(
48 ctx: &AuthContext<S>,
49 response: &AuthResponse,
50) -> AuthResult<Option<IssuedSession<S>>> {
51 let token = response
52 .headers
53 .get_all("set-cookie")
54 .filter_map(|header| {
55 let cookie = cookie::Cookie::parse(header.clone()).ok()?;
56 if cookie.name() != ctx.config.session.cookie_name || cookie.value().is_empty() {
57 return None;
58 }
59 alibi_core::utils::cookie_utils::verify_cookie_value(
60 cookie.value(),
61 ctx.config.current_secret(),
62 )
63 })
64 .last();
65 let Some(token) = token else {
66 return Ok(None);
67 };
68 let Some(session) = ctx.database.get_session(&token).await? else {
69 return Ok(None);
70 };
71 let Some(user) = ctx.session_user(&session).await? else {
72 return Ok(None);
73 };
74 Ok(Some(IssuedSession { user, session }))
75}
76
77pub(crate) fn record_completed_session<S: alibi_core::AuthSchema>(
78 user: &S::User,
79 session: &S::Session,
80) {
81 if let Some(request) = alibi_core::hooks::current_request_hook_context() {
82 request.extensions.insert(CompletedSession::<S> {
83 user: user.clone(),
84 session: session.clone(),
85 user_view: None,
86 user_record: None,
87 session_record: None,
88 });
89 }
90}
91
92pub(crate) fn record_completed_session_record<S: alibi_core::AuthSchema>(
93 user: &alibi_core::AdapterRecord<S::User>,
94 session: &alibi_core::AdapterRecord<S::Session>,
95) {
96 if let Some(request) = alibi_core::hooks::current_request_hook_context() {
97 request.extensions.insert(CompletedSession::<S> {
98 user: user.stored().clone(),
99 session: session.stored().clone(),
100 user_view: None,
101 user_record: Some(user.clone()),
102 session_record: Some(session.clone()),
103 });
104 }
105}
106
107pub(crate) fn record_completed_session_user_view<S: alibi_core::AuthSchema>(
110 original_user: &impl AuthUser,
111 session: &impl alibi_core::AuthSession,
112 view: alibi_core::wire::UserView,
113) {
114 use alibi_core::AuthSession;
115 if let Some(request) = alibi_core::hooks::current_request_hook_context()
116 && let Some(completed) = request.extensions.get::<CompletedSession<S>>()
117 && completed.user.id() == original_user.id()
118 && view.id == original_user.id().as_ref()
119 && completed.session.token() == session.token()
120 {
121 request.extensions.insert(CompletedSession::<S> {
122 user: completed.user.clone(),
123 session: completed.session.clone(),
124 user_view: Some(view),
125 user_record: completed.user_record.clone(),
126 session_record: completed.session_record.clone(),
127 });
128 }
129}
130
131pub(crate) fn response_has_session_cookie<S: alibi_core::AuthSchema>(
132 ctx: &AuthContext<S>,
133 response: &AuthResponse,
134) -> bool {
135 response.headers.get_all("set-cookie").any(|header| {
136 cookie::Cookie::parse(header.clone()).is_ok_and(|cookie| {
137 cookie.name() == ctx.config.session.cookie_name
138 && cookie
139 .value()
140 .split('.')
141 .next()
142 .is_some_and(|value| !value.is_empty())
143 })
144 })
145}
146
147pub(crate) fn completed_response_session<S: alibi_core::AuthSchema>(
148 req: &AuthRequest,
149 ctx: &AuthContext<S>,
150 response: &AuthResponse,
151) -> Option<std::sync::Arc<CompletedSession<S>>> {
152 if response_has_session_cookie(ctx, response) {
155 req.extensions().get::<CompletedSession<S>>()
156 } else {
157 None
158 }
159}
160
161#[must_use]
163pub fn admin_plugin_enabled(ctx: &AuthContext<impl alibi_core::AuthSchema>) -> bool {
164 ctx.get_metadata("admin.enabled")
165 .and_then(serde_json::Value::as_bool)
166 .unwrap_or(false)
167}
168
169pub fn admin_banned_user_message(ctx: &AuthContext<impl alibi_core::AuthSchema>) -> Option<String> {
172 ctx.get_metadata("admin.banned_user_message")
173 .and_then(|value| value.as_str())
174 .map(ToOwned::to_owned)
175}
176
177pub(crate) async fn resolve_admin_banned_user_message<S: alibi_core::AuthSchema>(
179 ctx: &AuthContext<S>,
180 user: &impl AuthUser,
181) -> AuthResult<String> {
182 if let Some(policy) = ctx
183 .extensions
184 .get::<crate::admin::BannedUserMessagePolicy>()
185 {
186 return policy.message(user).await;
187 }
188 Ok(admin_banned_user_message(ctx).unwrap_or_else(|| {
189 "You have been banned from this application. Please contact support if you believe this is an error.".to_owned()
190 }))
191}
192
193pub async fn issue_user_session<S: alibi_core::AuthSchema>(
200 ctx: &AuthContext<S>,
201 user_id: &str,
202 ip_address: Option<String>,
203 user_agent: Option<String>,
204) -> Result<IssuedSession<S>, SessionIssueError> {
205 issue_user_session_record(ctx, user_id, ip_address, user_agent)
206 .await
207 .map(IssuedSessionRecord::into_stored)
208}
209
210pub async fn issue_user_session_record<S: alibi_core::AuthSchema>(
215 ctx: &AuthContext<S>,
216 user_id: &str,
217 ip_address: Option<String>,
218 user_agent: Option<String>,
219) -> Result<IssuedSessionRecord<S>, SessionIssueError> {
220 issue_user_session_inner(ctx, user_id, ip_address, user_agent, None, true, None).await
221}
222
223pub async fn issue_user_session_with_fields<S: alibi_core::AuthSchema>(
232 ctx: &AuthContext<S>,
233 user_id: &str,
234 ip_address: Option<String>,
235 user_agent: Option<String>,
236 fields: SessionOverrides,
237) -> Result<IssuedSession<S>, SessionIssueError> {
238 issue_user_session_with_fields_record(ctx, user_id, ip_address, user_agent, fields)
239 .await
240 .map(IssuedSessionRecord::into_stored)
241}
242
243pub async fn issue_user_session_with_fields_record<S: alibi_core::AuthSchema>(
250 ctx: &AuthContext<S>,
251 user_id: &str,
252 ip_address: Option<String>,
253 user_agent: Option<String>,
254 fields: SessionOverrides,
255) -> Result<IssuedSessionRecord<S>, SessionIssueError> {
256 if let Some(name) = fields.additional_fields.keys().find(|name| {
259 matches!(
260 name.as_str(),
261 "id" | "token"
262 | "userId"
263 | "user_id"
264 | "expiresAt"
265 | "expires_at"
266 | "createdAt"
267 | "created_at"
268 | "updatedAt"
269 | "updated_at"
270 )
271 }) {
272 return Err(
273 AuthError::bad_request(format!("Session issuance owns the {name} field")).into(),
274 );
275 }
276 issue_user_session_inner(
277 ctx,
278 user_id,
279 ip_address,
280 user_agent,
281 Some(fields),
282 true,
283 None,
284 )
285 .await
286}
287
288pub(crate) async fn create_user_session_record<S: alibi_core::AuthSchema>(
291 ctx: &AuthContext<S>,
292 user_id: &str,
293 ip_address: Option<String>,
294 user_agent: Option<String>,
295) -> Result<IssuedSessionRecord<S>, SessionIssueError> {
296 issue_user_session_inner(ctx, user_id, ip_address, user_agent, None, false, None).await
297}
298
299pub async fn issue_user_session_with_overrides<S: alibi_core::AuthSchema>(
305 ctx: &AuthContext<S>,
306 user_id: &str,
307 ip_address: Option<String>,
308 user_agent: Option<String>,
309 current_session: &impl alibi_core::AuthSession,
310) -> Result<IssuedSession<S>, SessionIssueError> {
311 issue_user_session_with_overrides_record(ctx, user_id, ip_address, user_agent, current_session)
312 .await
313 .map(IssuedSessionRecord::into_stored)
314}
315
316pub async fn issue_user_session_with_overrides_record<S: alibi_core::AuthSchema>(
321 ctx: &AuthContext<S>,
322 user_id: &str,
323 ip_address: Option<String>,
324 user_agent: Option<String>,
325 current_session: &impl alibi_core::AuthSession,
326) -> Result<IssuedSessionRecord<S>, SessionIssueError> {
327 let overrides = SessionOverrides {
328 additional_fields: current_session
329 .additional_fields()
330 .into_iter()
331 .map(|(name, value)| (name, alibi_core::utils::json::JsValue::from(value)))
332 .collect(),
333 impersonated_by: current_session.impersonated_by().map(str::to_owned),
334 active_organization_id: current_session.active_organization_id().map(str::to_owned),
335 active_team_id: current_session.active_team_id().map(str::to_owned),
336 };
337 issue_user_session_inner(
338 ctx,
339 user_id,
340 ip_address,
341 user_agent,
342 Some(overrides),
343 true,
344 None,
345 )
346 .await
347}
348
349pub(crate) async fn issue_selected_user_session_record<S: alibi_core::AuthSchema>(
352 ctx: &AuthContext<S>,
353 user: alibi_core::AdapterRecord<S::User>,
354 ip_address: Option<String>,
355 user_agent: Option<String>,
356) -> Result<IssuedSessionRecord<S>, SessionIssueError> {
357 let id = user.id().into_owned();
358 issue_user_session_inner(ctx, &id, ip_address, user_agent, None, true, Some(user)).await
359}
360
361pub(in crate::helpers) async fn issue_user_session_inner<S: alibi_core::AuthSchema>(
362 ctx: &AuthContext<S>,
363 user_id: &str,
364 ip_address: Option<String>,
365 user_agent: Option<String>,
366 overrides: Option<SessionOverrides>,
367 publish: bool,
368 selected: Option<alibi_core::AdapterRecord<S::User>>,
369) -> Result<IssuedSessionRecord<S>, SessionIssueError> {
370 let user = match selected {
371 Some(user) => user,
372 None => ctx
373 .database
374 .get_user_by_id_record(user_id)
375 .await?
376 .ok_or(AuthError::UserNotFound)?,
377 };
378
379 if admin_plugin_enabled(ctx) && user.banned() {
380 if user
381 .ban_expires()
382 .is_some_and(|expires| expires < Utc::now())
383 {
384 _ = ctx
385 .database
386 .update_user_record(
387 user_id,
388 UpdateUser {
389 banned: Some(false),
390 ban_reason: None,
391 ban_expires: None,
392 ..Default::default()
393 },
394 )
395 .await?;
396 } else {
397 return Err(SessionIssueError::Banned {
398 message: resolve_admin_banned_user_message(ctx, &user).await?,
399 });
400 }
401 }
402
403 let session = match overrides {
404 None => {
405 ctx.session_manager()
406 .create_session_record(&user, ip_address, user_agent)
407 .await?
408 }
409 Some(overrides) => {
410 ctx.database
411 .create_session_record(alibi_core::CreateSession {
412 additional_fields: overrides.additional_fields,
413 token: None,
414 user_id: user.id().to_string(),
415 expires_at: Utc::now() + ctx.config.session.expires_in,
416 ip_address,
417 user_agent,
418 impersonated_by: overrides.impersonated_by,
419 active_organization_id: overrides.active_organization_id,
420 active_team_id: overrides.active_team_id,
421 })
422 .await?
423 }
424 };
425
426 if publish {
427 alibi_core::session::cookie_cache::runtime::emit_issuance(ctx, &user, &session).await?;
428 record_completed_session_record::<S>(&user, &session);
429 }
430 Ok(IssuedSessionRecord { user, session })
431}
432
433#[must_use]
435pub fn get_cookie(req: &AuthRequest, name: &str) -> Option<String> {
436 let header = req.headers.get("cookie")?;
437 header.split(';').find_map(|cookie| {
438 let trimmed = cookie.trim();
439 let (cookie_name, cookie_value) = trimmed.split_once('=')?;
440 (cookie_name == name).then_some(cookie_value.to_owned())
441 })
442}
443
444pub fn delete_session_cookie_headers(config: &alibi_core::AuthConfig) -> AuthResult<Vec<String>> {
446 alibi_core::utils::cookie_utils::delete_session_cookie_headers(config)
447}