1use super::*;
2pub(in crate::plugins) async fn ordinary_session<S: alibi_core::AuthSchema>(
6 request: &AuthRequest,
7 ctx: &AuthContext<S>,
8) -> AuthResult<(alibi_core::AuthenticatedUser<S>, alibi_core::SessionView)> {
9 ctx.require_cached_session(request).await.map_err(|error| {
10 if matches!(error, AuthError::Unauthenticated) {
11 AuthError::Upstream {
12 status: 401,
13 code: "UNAUTHORIZED",
14 message: "Unauthorized",
15 }
16 } else {
17 error
18 }
19 })
20}
21
22pub fn expires_in_to_at(expires_in_secs: Option<i64>) -> AuthResult<Option<String>> {
31 match expires_in_secs {
32 Some(secs) => {
33 let duration = chrono::Duration::try_seconds(secs)
34 .ok_or_else(|| AuthError::bad_request("expiresIn is out of range"))?;
35 let dt = Utc::now()
36 .checked_add_signed(duration)
37 .ok_or_else(|| AuthError::bad_request("expiresIn is out of range"))?;
38 Ok(Some(dt.to_rfc3339()))
39 }
40 None => Ok(None),
41 }
42}
43
44pub async fn response_session<S: alibi_core::AuthSchema>(
53 ctx: &AuthContext<S>,
54 response: &alibi_core::AuthResponse,
55) -> AuthResult<Option<IssuedSession<S>>> {
56 let token = response
57 .headers
58 .get_all("set-cookie")
59 .filter_map(|header| {
60 let cookie = cookie::Cookie::parse(header.clone()).ok()?;
61 (cookie.name() == ctx.config.session.cookie_name && !cookie.value().is_empty())
62 .then(|| {
63 alibi_core::utils::cookie_utils::verify_cookie_value(
64 cookie.value(),
65 ctx.config.current_secret(),
66 )
67 })
68 .flatten()
69 })
70 .last();
71 let Some(token) = token else {
72 return Ok(None);
73 };
74 let Some(session) = ctx.database.get_session(&token).await? else {
75 return Ok(None);
76 };
77 let Some(user) = ctx.session_user(&session).await? else {
78 return Ok(None);
79 };
80 Ok(Some(IssuedSession { user, session }))
81}
82
83pub(in crate::plugins) fn record_completed_session<S: alibi_core::AuthSchema>(
84 user: &S::User,
85 session: &S::Session,
86) {
87 if let Some(request) = alibi_core::hooks::current_request_hook_context() {
88 request.extensions.insert(CompletedSession::<S> {
89 user: user.clone(),
90 session: session.clone(),
91 user_view: None,
92 user_record: None,
93 session_record: None,
94 });
95 }
96}
97
98pub(in crate::plugins) fn record_completed_session_record<S: alibi_core::AuthSchema>(
99 user: &alibi_core::AdapterRecord<S::User>,
100 session: &alibi_core::AdapterRecord<S::Session>,
101) {
102 if let Some(request) = alibi_core::hooks::current_request_hook_context() {
103 request.extensions.insert(CompletedSession::<S> {
104 user: user.stored().clone(),
105 session: session.stored().clone(),
106 user_view: None,
107 user_record: Some(user.clone()),
108 session_record: Some(session.clone()),
109 });
110 }
111}
112
113pub(in crate::plugins) fn record_completed_session_user_view<S: alibi_core::AuthSchema>(
116 original_user: &impl AuthUser,
117 session: &impl alibi_core::AuthSession,
118 view: alibi_core::wire::UserView,
119) {
120 use alibi_core::AuthSession;
121 if let Some(request) = alibi_core::hooks::current_request_hook_context()
122 && let Some(completed) = request.extensions.get::<CompletedSession<S>>()
123 && completed.user.id() == original_user.id()
124 && view.id == original_user.id().as_ref()
125 && completed.session.token() == session.token()
126 {
127 request.extensions.insert(CompletedSession::<S> {
128 user: completed.user.clone(),
129 session: completed.session.clone(),
130 user_view: Some(view),
131 user_record: completed.user_record.clone(),
132 session_record: completed.session_record.clone(),
133 });
134 }
135}
136
137pub(in crate::plugins) fn response_has_session_cookie<S: alibi_core::AuthSchema>(
138 ctx: &AuthContext<S>,
139 response: &alibi_core::AuthResponse,
140) -> bool {
141 response.headers.get_all("set-cookie").any(|header| {
142 cookie::Cookie::parse(header.clone()).is_ok_and(|cookie| {
143 cookie.name() == ctx.config.session.cookie_name
144 && cookie
145 .value()
146 .split('.')
147 .next()
148 .is_some_and(|value| !value.is_empty())
149 })
150 })
151}
152
153pub(in crate::plugins) fn completed_response_session<S: alibi_core::AuthSchema>(
154 req: &AuthRequest,
155 ctx: &AuthContext<S>,
156 response: &alibi_core::AuthResponse,
157) -> Option<std::sync::Arc<CompletedSession<S>>> {
158 let selected = response_has_session_cookie(ctx, response);
161 selected
162 .then(|| req.extensions().get::<CompletedSession<S>>())
163 .flatten()
164}
165
166#[must_use]
168pub fn admin_plugin_enabled(ctx: &AuthContext<impl alibi_core::AuthSchema>) -> bool {
169 ctx.get_metadata("admin.enabled")
170 .and_then(serde_json::Value::as_bool)
171 .unwrap_or(false)
172}
173
174pub fn admin_banned_user_message(ctx: &AuthContext<impl alibi_core::AuthSchema>) -> Option<String> {
177 ctx.get_metadata("admin.banned_user_message")
178 .and_then(|value| value.as_str())
179 .map(ToOwned::to_owned)
180}
181
182pub(in crate::plugins) async fn resolve_admin_banned_user_message<S: alibi_core::AuthSchema>(
187 ctx: &AuthContext<S>,
188 user: &impl AuthUser,
189) -> AuthResult<String> {
190 if let Some(policy) = ctx
191 .extensions
192 .get::<super::super::admin::BannedUserMessagePolicy>()
193 {
194 return policy.message(user).await;
195 }
196 Ok(admin_banned_user_message(ctx).unwrap_or_else(|| {
197 "You have been banned from this application. Please contact support if you believe this is an error.".to_owned()
198 }))
199}
200
201pub async fn issue_user_session<S: alibi_core::AuthSchema>(
208 ctx: &AuthContext<S>,
209 user_id: &str,
210 ip_address: Option<String>,
211 user_agent: Option<String>,
212) -> Result<IssuedSession<S>, SessionIssueError> {
213 issue_user_session_record(ctx, user_id, ip_address, user_agent)
214 .await
215 .map(IssuedSessionRecord::into_stored)
216}
217
218pub async fn issue_user_session_record<S: alibi_core::AuthSchema>(
223 ctx: &AuthContext<S>,
224 user_id: &str,
225 ip_address: Option<String>,
226 user_agent: Option<String>,
227) -> Result<IssuedSessionRecord<S>, SessionIssueError> {
228 issue_user_session_inner(ctx, user_id, ip_address, user_agent, None, true, None).await
229}
230
231pub async fn issue_user_session_with_fields<S: alibi_core::AuthSchema>(
240 ctx: &AuthContext<S>,
241 user_id: &str,
242 ip_address: Option<String>,
243 user_agent: Option<String>,
244 fields: SessionOverrides,
245) -> Result<IssuedSession<S>, SessionIssueError> {
246 issue_user_session_with_fields_record(ctx, user_id, ip_address, user_agent, fields)
247 .await
248 .map(IssuedSessionRecord::into_stored)
249}
250
251pub async fn issue_user_session_with_fields_record<S: alibi_core::AuthSchema>(
258 ctx: &AuthContext<S>,
259 user_id: &str,
260 ip_address: Option<String>,
261 user_agent: Option<String>,
262 fields: SessionOverrides,
263) -> Result<IssuedSessionRecord<S>, SessionIssueError> {
264 for name in fields.additional_fields.keys() {
267 if matches!(
268 name.as_str(),
269 "id" | "token"
270 | "userId"
271 | "user_id"
272 | "expiresAt"
273 | "expires_at"
274 | "createdAt"
275 | "created_at"
276 | "updatedAt"
277 | "updated_at"
278 ) {
279 return Err(
280 AuthError::bad_request(format!("Session issuance owns the {name} field")).into(),
281 );
282 }
283 }
284 issue_user_session_inner(
285 ctx,
286 user_id,
287 ip_address,
288 user_agent,
289 Some(fields),
290 true,
291 None,
292 )
293 .await
294}
295
296pub(in crate::plugins) async fn create_user_session_record<S: alibi_core::AuthSchema>(
299 ctx: &AuthContext<S>,
300 user_id: &str,
301 ip_address: Option<String>,
302 user_agent: Option<String>,
303) -> Result<IssuedSessionRecord<S>, SessionIssueError> {
304 issue_user_session_inner(ctx, user_id, ip_address, user_agent, None, false, None).await
305}
306
307pub async fn issue_user_session_with_overrides<S: alibi_core::AuthSchema>(
313 ctx: &AuthContext<S>,
314 user_id: &str,
315 ip_address: Option<String>,
316 user_agent: Option<String>,
317 current_session: &impl alibi_core::AuthSession,
318) -> Result<IssuedSession<S>, SessionIssueError> {
319 issue_user_session_with_overrides_record(ctx, user_id, ip_address, user_agent, current_session)
320 .await
321 .map(IssuedSessionRecord::into_stored)
322}
323
324pub async fn issue_user_session_with_overrides_record<S: alibi_core::AuthSchema>(
329 ctx: &AuthContext<S>,
330 user_id: &str,
331 ip_address: Option<String>,
332 user_agent: Option<String>,
333 current_session: &impl alibi_core::AuthSession,
334) -> Result<IssuedSessionRecord<S>, SessionIssueError> {
335 let overrides = SessionOverrides {
336 additional_fields: current_session
337 .additional_fields()
338 .into_iter()
339 .map(|(name, value)| (name, alibi_core::utils::json::JsValue::from(value)))
340 .collect(),
341 impersonated_by: current_session.impersonated_by().map(str::to_owned),
342 active_organization_id: current_session.active_organization_id().map(str::to_owned),
343 active_team_id: current_session.active_team_id().map(str::to_owned),
344 };
345 issue_user_session_inner(
346 ctx,
347 user_id,
348 ip_address,
349 user_agent,
350 Some(overrides),
351 true,
352 None,
353 )
354 .await
355}
356
357pub(in crate::plugins) async fn issue_selected_user_session_record<S: alibi_core::AuthSchema>(
360 ctx: &AuthContext<S>,
361 user: alibi_core::AdapterRecord<S::User>,
362 ip_address: Option<String>,
363 user_agent: Option<String>,
364) -> Result<IssuedSessionRecord<S>, SessionIssueError> {
365 let id = user.id().into_owned();
366 issue_user_session_inner(ctx, &id, ip_address, user_agent, None, true, Some(user)).await
367}
368
369pub(in crate::plugins::helpers) async fn issue_user_session_inner<S: alibi_core::AuthSchema>(
370 ctx: &AuthContext<S>,
371 user_id: &str,
372 ip_address: Option<String>,
373 user_agent: Option<String>,
374 overrides: Option<SessionOverrides>,
375 publish: bool,
376 selected: Option<alibi_core::AdapterRecord<S::User>>,
377) -> Result<IssuedSessionRecord<S>, SessionIssueError> {
378 let user = match selected {
379 Some(user) => user,
380 None => ctx
381 .database
382 .get_user_by_id_record(user_id)
383 .await?
384 .ok_or(AuthError::UserNotFound)?,
385 };
386
387 if admin_plugin_enabled(ctx) && user.banned() {
388 if user
389 .ban_expires()
390 .is_some_and(|expires| expires < Utc::now())
391 {
392 drop(
393 ctx.database
394 .update_user_record(
395 user_id,
396 UpdateUser {
397 banned: Some(false),
398 ban_reason: None,
399 ban_expires: None,
400 ..Default::default()
401 },
402 )
403 .await?,
404 );
405 } else {
406 return Err(SessionIssueError::Banned {
407 message: resolve_admin_banned_user_message(ctx, &user).await?,
408 });
409 }
410 }
411
412 let session = match overrides {
413 None => {
414 ctx.session_manager()
415 .create_session_record(&user, ip_address, user_agent)
416 .await?
417 }
418 Some(overrides) => {
419 ctx.database
420 .create_session_record(alibi_core::CreateSession {
421 additional_fields: overrides.additional_fields,
422 token: None,
423 user_id: user.id().to_string(),
424 expires_at: Utc::now() + ctx.config.session.expires_in,
425 ip_address,
426 user_agent,
427 impersonated_by: overrides.impersonated_by,
428 active_organization_id: overrides.active_organization_id,
429 active_team_id: overrides.active_team_id,
430 })
431 .await?
432 }
433 };
434
435 if publish {
436 alibi_core::session::cookie_cache::runtime::emit_issuance(ctx, &user, &session).await?;
437 record_completed_session_record::<S>(&user, &session);
438 }
439 Ok(IssuedSessionRecord { user, session })
440}
441
442#[must_use]
444pub fn get_cookie(req: &AuthRequest, name: &str) -> Option<String> {
445 let header = req.headers.get("cookie")?;
446 header.split(';').find_map(|cookie| {
447 let trimmed = cookie.trim();
448 let (cookie_name, cookie_value) = trimmed.split_once('=')?;
449 (cookie_name == name).then_some(cookie_value.to_owned())
450 })
451}
452
453pub fn delete_session_cookie_headers(config: &alibi_core::AuthConfig) -> AuthResult<Vec<String>> {
455 alibi_core::utils::cookie_utils::delete_session_cookie_headers(config)
456}