1use super::{
2 CompletedSession, IssuedSession, IssuedSessionRecord, SessionIssueError, SessionOverrides,
3};
4use alibi_core::entity::AuthUser;
5use alibi_core::{AuthContext, AuthError, AuthResponse, AuthResult, UpdateUser};
6use chrono::Utc;
7
8pub(crate) async fn ordinary_session<S: alibi_core::AuthSchema>(
12 request: &impl alibi_core::session::SessionRequest,
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
77fn completion_extensions() -> impl Iterator<Item = alibi_core::types::RequestExtensions> {
78 alibi_core::hooks::current_request_hook_context()
79 .map(|request| request.extensions)
80 .into_iter()
81 .chain(
82 alibi_core::endpoint::current_endpoint_call_context()
83 .map(|call| call.extensions().clone()),
84 )
85}
86
87pub(crate) fn record_completed_session<S: alibi_core::AuthSchema>(
88 user: &S::User,
89 session: &S::Session,
90) {
91 for extensions in completion_extensions() {
92 extensions.insert(CompletedSession::<S> {
93 user: user.clone(),
94 session: session.clone(),
95 user_view: None,
96 user_record: None,
97 session_record: None,
98 });
99 }
100}
101
102pub(crate) fn record_completed_session_record<S: alibi_core::AuthSchema>(
103 user: &alibi_core::AdapterRecord<S::User>,
104 session: &alibi_core::AdapterRecord<S::Session>,
105) {
106 for extensions in completion_extensions() {
107 extensions.insert(CompletedSession::<S> {
108 user: user.stored().clone(),
109 session: session.stored().clone(),
110 user_view: None,
111 user_record: Some(user.clone()),
112 session_record: Some(session.clone()),
113 });
114 }
115}
116
117pub(crate) fn record_completed_session_user_view<S: alibi_core::AuthSchema>(
120 original_user: &impl AuthUser,
121 session: &impl alibi_core::AuthSession,
122 view: &alibi_core::wire::UserView,
123) {
124 use alibi_core::AuthSession;
125 for extensions in completion_extensions() {
126 if let Some(completed) = extensions.get::<CompletedSession<S>>()
127 && completed.user.id() == original_user.id()
128 && view.id == original_user.id().as_ref()
129 && completed.session.token() == session.token()
130 {
131 extensions.insert(CompletedSession::<S> {
132 user: completed.user.clone(),
133 session: completed.session.clone(),
134 user_view: Some(view.clone()),
135 user_record: completed.user_record.clone(),
136 session_record: completed.session_record.clone(),
137 });
138 }
139 }
140}
141
142pub(crate) fn response_has_session_cookie<S: alibi_core::AuthSchema>(
143 ctx: &AuthContext<S>,
144 response: &AuthResponse,
145) -> bool {
146 response.headers.get_all("set-cookie").any(|header| {
147 cookie::Cookie::parse(header.clone()).is_ok_and(|cookie| {
148 cookie.name() == ctx.config.session.cookie_name
149 && cookie
150 .value()
151 .split('.')
152 .next()
153 .is_some_and(|value| !value.is_empty())
154 })
155 })
156}
157
158pub(crate) fn completed_response_session<S: alibi_core::AuthSchema>(
159 req: &impl alibi_core::session::SessionRequest,
160 ctx: &AuthContext<S>,
161 response: &AuthResponse,
162) -> Option<std::sync::Arc<CompletedSession<S>>> {
163 if response_has_session_cookie(ctx, response) {
166 req.extensions().get::<CompletedSession<S>>()
167 } else {
168 None
169 }
170}
171
172#[must_use]
174pub fn admin_plugin_enabled(ctx: &AuthContext<impl alibi_core::AuthSchema>) -> bool {
175 ctx.get_metadata("admin.enabled")
176 .and_then(serde_json::Value::as_bool)
177 .unwrap_or(false)
178}
179
180pub fn admin_banned_user_message(ctx: &AuthContext<impl alibi_core::AuthSchema>) -> Option<String> {
183 ctx.get_metadata("admin.banned_user_message")
184 .and_then(|value| value.as_str())
185 .map(ToOwned::to_owned)
186}
187
188pub(crate) async fn resolve_admin_banned_user_message<S: alibi_core::AuthSchema>(
190 ctx: &AuthContext<S>,
191 user: &impl AuthUser,
192) -> AuthResult<String> {
193 if let Some(policy) = ctx
194 .extensions
195 .get::<crate::admin::BannedUserMessagePolicy>()
196 {
197 return policy.message(user).await;
198 }
199 Ok(admin_banned_user_message(ctx).unwrap_or_else(|| {
200 "You have been banned from this application. Please contact support if you believe this is an error.".to_owned()
201 }))
202}
203
204pub async fn issue_user_session<S: alibi_core::AuthSchema>(
211 ctx: &AuthContext<S>,
212 user_id: &str,
213 ip_address: Option<String>,
214 user_agent: Option<String>,
215) -> Result<IssuedSession<S>, SessionIssueError> {
216 issue_user_session_record(ctx, user_id, ip_address, user_agent)
217 .await
218 .map(IssuedSessionRecord::into_stored)
219}
220
221pub async fn issue_user_session_record<S: alibi_core::AuthSchema>(
226 ctx: &AuthContext<S>,
227 user_id: &str,
228 ip_address: Option<String>,
229 user_agent: Option<String>,
230) -> Result<IssuedSessionRecord<S>, SessionIssueError> {
231 issue_user_session_inner(ctx, user_id, ip_address, user_agent, None, true, None).await
232}
233
234pub async fn issue_user_session_with_fields<S: alibi_core::AuthSchema>(
243 ctx: &AuthContext<S>,
244 user_id: &str,
245 ip_address: Option<String>,
246 user_agent: Option<String>,
247 fields: SessionOverrides,
248) -> Result<IssuedSession<S>, SessionIssueError> {
249 issue_user_session_with_fields_record(ctx, user_id, ip_address, user_agent, fields)
250 .await
251 .map(IssuedSessionRecord::into_stored)
252}
253
254pub async fn issue_user_session_with_fields_record<S: alibi_core::AuthSchema>(
261 ctx: &AuthContext<S>,
262 user_id: &str,
263 ip_address: Option<String>,
264 user_agent: Option<String>,
265 fields: SessionOverrides,
266) -> Result<IssuedSessionRecord<S>, SessionIssueError> {
267 if let Some(name) = fields.additional_fields.keys().find(|name| {
270 matches!(
271 name.as_str(),
272 "id" | "token"
273 | "userId"
274 | "user_id"
275 | "expiresAt"
276 | "expires_at"
277 | "createdAt"
278 | "created_at"
279 | "updatedAt"
280 | "updated_at"
281 )
282 }) {
283 return Err(
284 AuthError::bad_request(format!("Session issuance owns the {name} field")).into(),
285 );
286 }
287 issue_user_session_inner(
288 ctx,
289 user_id,
290 ip_address,
291 user_agent,
292 Some(fields),
293 true,
294 None,
295 )
296 .await
297}
298
299pub(crate) async fn create_user_session_record<S: alibi_core::AuthSchema>(
302 ctx: &AuthContext<S>,
303 user_id: &str,
304 ip_address: Option<String>,
305 user_agent: Option<String>,
306) -> Result<IssuedSessionRecord<S>, SessionIssueError> {
307 issue_user_session_inner(ctx, user_id, ip_address, user_agent, None, false, None).await
308}
309
310pub async fn issue_user_session_with_overrides<S: alibi_core::AuthSchema>(
316 ctx: &AuthContext<S>,
317 user_id: &str,
318 ip_address: Option<String>,
319 user_agent: Option<String>,
320 current_session: &impl alibi_core::AuthSession,
321) -> Result<IssuedSession<S>, SessionIssueError> {
322 issue_user_session_with_overrides_record(ctx, user_id, ip_address, user_agent, current_session)
323 .await
324 .map(IssuedSessionRecord::into_stored)
325}
326
327pub async fn issue_user_session_with_overrides_record<S: alibi_core::AuthSchema>(
332 ctx: &AuthContext<S>,
333 user_id: &str,
334 ip_address: Option<String>,
335 user_agent: Option<String>,
336 current_session: &impl alibi_core::AuthSession,
337) -> Result<IssuedSessionRecord<S>, SessionIssueError> {
338 let overrides = SessionOverrides {
339 additional_fields: current_session
340 .additional_fields()
341 .into_iter()
342 .map(|(name, value)| (name, alibi_core::utils::json::JsValue::from(value)))
343 .collect(),
344 impersonated_by: current_session.impersonated_by().map(str::to_owned),
345 active_organization_id: current_session.active_organization_id().map(str::to_owned),
346 active_team_id: current_session.active_team_id().map(str::to_owned),
347 };
348 issue_user_session_inner(
349 ctx,
350 user_id,
351 ip_address,
352 user_agent,
353 Some(overrides),
354 true,
355 None,
356 )
357 .await
358}
359
360pub(crate) async fn issue_selected_user_session_record<S: alibi_core::AuthSchema>(
363 ctx: &AuthContext<S>,
364 user: alibi_core::AdapterRecord<S::User>,
365 ip_address: Option<String>,
366 user_agent: Option<String>,
367) -> Result<IssuedSessionRecord<S>, SessionIssueError> {
368 let id = user.id().into_owned();
369 issue_user_session_inner(ctx, &id, ip_address, user_agent, None, true, Some(user)).await
370}
371
372pub(crate) async fn issue_selected_user_session_in_transaction<S: alibi_core::AuthSchema>(
373 ctx: &AuthContext<S>,
374 database: &dyn alibi_core::AuthTransaction<S>,
375 mut user: alibi_core::AdapterRecord<S::User>,
376 ip_address: Option<String>,
377 user_agent: Option<String>,
378) -> Result<IssuedSessionRecord<S>, SessionIssueError> {
379 if admin_plugin_enabled(ctx) && user.banned() {
380 if user.ban_expires().is_some_and(|expiry| expiry < Utc::now()) {
381 user = database
382 .update_user_record(
383 &user.id(),
384 UpdateUser {
385 banned: Some(false),
386 ban_reason: None,
387 ban_expires: None,
388 ..Default::default()
389 },
390 )
391 .await?;
392 } else {
393 return Err(SessionIssueError::Banned {
394 message: resolve_admin_banned_user_message(ctx, &user).await?,
395 });
396 }
397 }
398 let session = ctx
399 .session_manager()
400 .create_session_record_in_transaction(&user, ip_address, user_agent, database)
401 .await?;
402 Ok(IssuedSessionRecord { user, session })
403}
404pub(crate) async fn publish_committed_session<S: alibi_core::AuthSchema>(
405 ctx: &AuthContext<S>,
406 issued: &IssuedSessionRecord<S>,
407) -> AuthResult<()> {
408 alibi_core::session::cookie_cache::runtime::emit_issuance(ctx, &issued.user, &issued.session)
409 .await?;
410 record_completed_session_record::<S>(&issued.user, &issued.session);
411 Ok(())
412}
413
414pub(in crate::helpers) async fn issue_user_session_inner<S: alibi_core::AuthSchema>(
415 ctx: &AuthContext<S>,
416 user_id: &str,
417 ip_address: Option<String>,
418 user_agent: Option<String>,
419 overrides: Option<SessionOverrides>,
420 publish: bool,
421 selected: Option<alibi_core::AdapterRecord<S::User>>,
422) -> Result<IssuedSessionRecord<S>, SessionIssueError> {
423 let user = match selected {
424 Some(user) => user,
425 None => ctx
426 .database
427 .get_user_by_id_record(user_id)
428 .await?
429 .ok_or(AuthError::UserNotFound)?,
430 };
431
432 if admin_plugin_enabled(ctx) && user.banned() {
433 if user
434 .ban_expires()
435 .is_some_and(|expires| expires < Utc::now())
436 {
437 _ = ctx
438 .database
439 .update_user_record(
440 user_id,
441 UpdateUser {
442 banned: Some(false),
443 ban_reason: None,
444 ban_expires: None,
445 ..Default::default()
446 },
447 )
448 .await?;
449 } else {
450 return Err(SessionIssueError::Banned {
451 message: resolve_admin_banned_user_message(ctx, &user).await?,
452 });
453 }
454 }
455
456 let session = match overrides {
457 None => {
458 ctx.session_manager()
459 .create_session_record(&user, ip_address, user_agent)
460 .await?
461 }
462 Some(overrides) => {
463 ctx.database
464 .create_session_record(alibi_core::CreateSession {
465 additional_fields: overrides.additional_fields,
466 token: None,
467 user_id: user.id().to_string(),
468 expires_at: Utc::now() + ctx.config.session.expires_in,
469 ip_address,
470 user_agent,
471 impersonated_by: overrides.impersonated_by,
472 active_organization_id: overrides.active_organization_id,
473 active_team_id: overrides.active_team_id,
474 })
475 .await?
476 }
477 };
478
479 if publish {
480 alibi_core::session::cookie_cache::runtime::emit_issuance(ctx, &user, &session).await?;
481 record_completed_session_record::<S>(&user, &session);
482 }
483 Ok(IssuedSessionRecord { user, session })
484}
485
486#[must_use]
488pub fn get_cookie(req: &impl alibi_core::session::SessionRequest, name: &str) -> Option<String> {
489 let header = req.session_headers().get("cookie")?;
490 header.split(';').find_map(|cookie| {
491 let trimmed = cookie.trim();
492 let (cookie_name, cookie_value) = trimmed.split_once('=')?;
493 (cookie_name == name).then_some(cookie_value.to_owned())
494 })
495}
496
497pub fn delete_session_cookie_headers(config: &alibi_core::AuthConfig) -> AuthResult<Vec<String>> {
499 alibi_core::utils::cookie_utils::delete_session_cookie_headers(config)
500}