1use super::StatusResponse;
2use super::authentication_helpers::{
3 JsonField, JsonFieldKind, RequestBody, json_type, parse_body, validation_response,
4 with_session_cookies,
5};
6use super::helpers::{
7 admin_plugin_enabled, delete_session_cookie_headers, unauthorized_if_unauthenticated,
8};
9use alibi_core::endpoint::{
10 EndpointCall, EndpointDefinition, EndpointInput, EndpointResponse, ServerEndpoint,
11};
12use alibi_core::entity::{AuthSession, AuthUser};
13use alibi_core::error::codes;
14use alibi_core::field_policy::{FieldInputError, FieldValues, SessionFields};
15use alibi_core::session::SessionRequest;
16use alibi_core::utils::cookie_utils::{related_cookie_name, verify_cookie_value};
17use alibi_core::utils::json::JsValue;
18use alibi_core::wire::{SessionView, UserView};
19use alibi_core::{
20 AuthContext, AuthError, AuthPlugin, AuthRequest, AuthResponse, AuthResult, AuthRoute,
21 HttpMethod, SuccessResponse,
22};
23use async_trait::async_trait;
24use serde::{Deserialize, Serialize};
25
26pub struct SessionManagementPlugin {
28 config: SessionManagementConfig,
29}
30
31#[derive(Debug, Clone, alibi_core::PluginConfig)]
32#[plugin(name = "SessionManagementPlugin")]
33pub struct SessionManagementConfig {
34 #[config(default = true)]
35 pub enable_session_listing: bool,
36 #[config(default = true)]
37 pub enable_session_revocation: bool,
38 #[config(default = true)]
39 pub require_authentication: bool,
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct RevokeSessionRequest {
45 pub token: String,
47}
48
49impl RequestBody for RevokeSessionRequest {
50 const FIELDS: &'static [JsonField] = &[JsonField::string("token", true)];
51}
52
53#[derive(Debug, Clone, Default, Serialize, Deserialize)]
55#[serde(rename_all = "camelCase")]
56pub struct SignOutRequest {
57 #[serde(rename = "callbackURL", skip_serializing_if = "Option::is_none")]
58 pub callback_url: Option<String>,
59 #[serde(skip_serializing_if = "Option::is_none")]
60 pub disable_redirect: Option<bool>,
61 #[serde(skip_serializing_if = "Option::is_none")]
62 pub state: Option<String>,
63}
64impl RequestBody for SignOutRequest {
65 const FIELDS: &'static [JsonField] = &[
66 JsonField::string("callbackURL", false),
67 JsonField {
68 name: "disableRedirect",
69 kind: JsonFieldKind::Boolean,
70 required: false,
71 },
72 JsonField::string("state", false),
73 ];
74}
75
76#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct SignOutResponse {
79 pub success: bool,
80 #[serde(skip_serializing_if = "Option::is_none")]
81 pub url: Option<String>,
82 #[serde(skip_serializing_if = "Option::is_none")]
83 pub redirect: Option<bool>,
84}
85
86#[derive(Debug, Clone, Serialize, Deserialize)]
88pub struct UpdateSessionResponse {
89 pub session: SessionView,
90}
91
92#[derive(Debug, Clone, Serialize, Deserialize)]
95pub struct GetSessionResponse {
96 pub session: SessionView,
97 pub user: UserView,
98 #[serde(rename = "needsRefresh", skip_serializing_if = "Option::is_none")]
99 pub needs_refresh: Option<bool>,
100}
101
102#[derive(Debug, Clone, Default, Serialize, Deserialize)]
104#[serde(rename_all = "camelCase", default)]
105pub struct GetSessionQuery {
106 pub disable_cookie_cache: bool,
108 pub disable_refresh: bool,
110}
111
112#[async_trait]
113impl<S: alibi_core::AuthSchema> AuthPlugin<S> for SessionManagementPlugin {
114 route_openapi_metadata!(S);
115
116 fn name(&self) -> &'static str {
117 "session-management"
118 }
119
120 fn server_endpoints(&self) -> Vec<EndpointDefinition> {
121 let mut endpoints = vec![crate::endpoint::definition(
122 "getSession",
123 "get_session",
124 Some("/get-session"),
125 HttpMethod::Get,
126 )];
127 endpoints.push(crate::endpoint::definition(
128 "updateSession",
129 "updateSession",
130 Some("/update-session"),
131 HttpMethod::Post,
132 ));
133 endpoints.push(crate::endpoint::definition(
134 "signOut",
135 "signOut",
136 Some("/sign-out"),
137 HttpMethod::Post,
138 ));
139 if self.config.enable_session_listing {
140 endpoints.push(crate::endpoint::definition(
141 "listSessions",
142 "listSessions",
143 Some("/list-sessions"),
144 HttpMethod::Get,
145 ));
146 }
147 if self.config.enable_session_revocation {
148 for (operation, path) in [
149 ("revokeSession", "/revoke-session"),
150 ("revokeOtherSessions", "/revoke-other-sessions"),
151 ("revokeSessions", "/revoke-sessions"),
152 ] {
153 endpoints.push(crate::endpoint::definition(
154 operation,
155 operation,
156 Some(path),
157 HttpMethod::Post,
158 ));
159 }
160 }
161 endpoints
162 }
163
164 fn validate_endpoint(
165 &self,
166 call: &EndpointCall,
167 _: &AuthContext<S>,
168 ) -> AuthResult<EndpointInput> {
169 if call.operation_id() == "updateSession" {
170 if !matches!(call.body(), Some(JsValue::Object(_))) {
171 return Err(crate::endpoint::validation(format!(
172 "[body] Invalid input: expected record, received {}",
173 json_type(call.body()),
174 )));
175 }
176 return Ok(EndpointInput {
177 body: call.body().cloned(),
178 query: call.query().cloned(),
179 });
180 }
181 if call.operation_id() == "signOut" {
182 let body = call
183 .body()
184 .map(|body| {
185 crate::endpoint::validate_fields(Some(body), "body", SignOutRequest::FIELDS)
186 })
187 .transpose()?;
188 if call.headers().is_none() {
189 return Err(crate::endpoint::validation("Headers is required"));
190 }
191 return Ok(EndpointInput {
192 body,
193 query: call.query().cloned(),
194 });
195 }
196 if call.operation_id() == "listSessions" {
197 if call.headers().is_none() {
198 return Err(crate::endpoint::validation("Headers is required"));
199 }
200 return Ok(EndpointInput {
201 body: call.body().cloned(),
202 query: call.query().cloned(),
203 });
204 }
205 if matches!(
206 call.operation_id(),
207 "revokeSession" | "revokeOtherSessions" | "revokeSessions"
208 ) {
209 let body = if call.operation_id() == "revokeSession" {
210 Some(crate::endpoint::validate_fields(
211 call.body(),
212 "body",
213 RevokeSessionRequest::FIELDS,
214 )?)
215 } else {
216 call.body().cloned()
217 };
218 if call.headers().is_none() {
219 return Err(crate::endpoint::validation("Headers is required"));
220 }
221 return Ok(EndpointInput {
222 body,
223 query: call.query().cloned(),
224 });
225 }
226 let query = match call.query() {
227 None => None,
228 Some(JsValue::Object(input)) => Some(JsValue::Object(
229 input
230 .iter()
231 .filter(|(name, _)| {
232 ["disableCookieCache", "disableRefresh"].contains(&name.as_str())
233 })
234 .map(|(name, value)| (name.clone(), JsValue::Bool(value.is_truthy())))
235 .collect(),
236 )),
237 Some(value) => {
238 return Err(crate::endpoint::validation(format!(
239 "[query] Invalid input: expected object, received {}",
240 json_type(Some(value)),
241 )));
242 }
243 };
244 if call.headers().is_none() {
245 return Err(crate::endpoint::validation("Headers is required"));
246 }
247 Ok(EndpointInput {
248 body: call.body().cloned(),
249 query,
250 })
251 }
252
253 async fn on_endpoint(
254 &self,
255 call: &EndpointCall,
256 ctx: &AuthContext<S>,
257 ) -> AuthResult<EndpointResponse> {
258 if call.operation_id() == "updateSession" {
259 return self.update_session_endpoint_response(call, ctx).await;
260 }
261 if call.operation_id() == "signOut" {
262 let body = if call.body().is_some() {
263 call.body_as()?
264 } else {
265 SignOutRequest::default()
266 };
267 let response = self.sign_out_response(call, &body, ctx).await?;
268 let mut result =
269 EndpointResponse::value(alibi_core::utils::json::from_slice(&response.body)?);
270 for (name, value) in response.headers {
271 if !name.eq_ignore_ascii_case("content-type") {
272 result = result.with_header(name, value);
273 }
274 }
275 return Ok(result);
276 }
277 if call.operation_id() == "listSessions" {
278 let (user, session) = ctx
279 .require_cached_session(call)
280 .await
281 .map_err(unauthorized_if_unauthenticated)?;
282 call.record_authenticated_session(
283 match &user {
284 alibi_core::AuthenticatedUser::Stored(user) => ctx.user_view(user),
285 alibi_core::AuthenticatedUser::Cached(user) => (**user).clone(),
286 },
287 session.clone(),
288 );
289 if !ctx.session_manager().is_session_fresh(&session) {
290 return Err(AuthError::Upstream {
291 status: 403,
292 code: codes::SESSION_NOT_FRESH,
293 message: "Session is not fresh",
294 });
295 }
296 let sessions = match list_sessions_core(user.id(), ctx).await {
297 Ok(sessions) => sessions,
298 Err(error) => {
299 tracing::error!(error = %error, "Session listing failed");
300 return Ok(EndpointResponse::error(AuthError::Api {
301 status: 500,
302 code: None,
303 message: String::new(),
304 })
305 .with_error_body(JsValue::Null));
306 }
307 };
308 return EndpointResponse::json(&sessions);
309 }
310 if matches!(
311 call.operation_id(),
312 "revokeSession" | "revokeOtherSessions" | "revokeSessions"
313 ) {
314 let (user, session) = ctx
316 .require_authoritative_session_record(call)
317 .await
318 .map_err(|_| AuthError::Upstream {
319 status: 401,
320 code: codes::UNAUTHORIZED,
321 message: "Unauthorized",
322 })?;
323 call.record_authenticated_session(ctx.user_view(&user), session.clone());
324 let response = if call.operation_id() == "revokeSession" {
325 let input: RevokeSessionRequest = call.body_as()?;
326 revoke_session_core(&user, &input.token, ctx).await?
327 } else if call.operation_id() == "revokeOtherSessions" {
328 revoke_other_sessions_core(user.id(), &session, ctx).await?
329 } else {
330 revoke_sessions_core(user.id(), ctx).await?
331 };
332 return EndpointResponse::json(&response);
333 }
334 let response = match self.get_session_response(call, ctx).await {
335 Ok(value) => EndpointResponse::json(&value)?,
336 Err(error) => EndpointResponse::error(error),
337 };
338 Ok(response
339 .with_header("cache-control", "no-store")
340 .with_header("pragma", "no-cache"))
341 }
342
343 fn routes(&self) -> Vec<AuthRoute> {
344 vec![
345 AuthRoute::get("/get-session", "get_session"),
346 AuthRoute::post("/get-session", "get_session"),
349 AuthRoute::post("/sign-out", "sign_out"),
350 AuthRoute::post("/update-session", "updateSession"),
351 AuthRoute::get("/list-sessions", "list_sessions"),
352 AuthRoute::post("/revoke-session", "revoke_session"),
353 AuthRoute::post("/revoke-sessions", "revoke_sessions"),
354 AuthRoute::post("/revoke-other-sessions", "revoke_other_sessions"),
355 ]
356 }
357
358 async fn on_request(
359 &self,
360 req: &AuthRequest,
361 ctx: &AuthContext<S>,
362 ) -> AuthResult<Option<AuthResponse>> {
363 let response = match (req.method(), req.path()) {
364 (HttpMethod::Get | HttpMethod::Post, "/get-session") => {
365 self.handle_get_session(req, ctx).await?
366 }
367 (HttpMethod::Post, "/sign-out") => self.handle_sign_out(req, ctx).await?,
368 (HttpMethod::Post, "/update-session") => self.handle_update_session(req, ctx).await?,
369 (HttpMethod::Get, "/list-sessions") if self.config.enable_session_listing => {
370 self.handle_list_sessions(req, ctx).await?
371 }
372 (HttpMethod::Post, "/revoke-session") if self.config.enable_session_revocation => {
373 self.handle_revoke_session(req, ctx).await?
374 }
375 (HttpMethod::Post, "/revoke-sessions") if self.config.enable_session_revocation => {
376 self.handle_revoke_sessions(req, ctx).await?
377 }
378 (HttpMethod::Post, "/revoke-other-sessions")
379 if self.config.enable_session_revocation =>
380 {
381 self.handle_revoke_other_sessions(req, ctx).await?
382 }
383 _ => return Ok(None),
384 };
385 Ok(Some(response))
386 }
387}
388
389impl SessionManagementPlugin {
390 #[must_use]
397 pub const fn get_session_endpoint() -> ServerEndpoint<Option<GetSessionResponse>> {
398 ServerEndpoint::new("session-management", "getSession")
399 }
400
401 #[must_use]
405 pub const fn sign_out_endpoint() -> ServerEndpoint<SignOutResponse> {
406 ServerEndpoint::new("session-management", "signOut")
407 }
408
409 #[must_use]
412 pub const fn list_sessions_endpoint() -> ServerEndpoint<Vec<SessionView>> {
413 ServerEndpoint::new("session-management", "listSessions")
414 }
415
416 pub fn revoke_session_endpoint(
424 input: &RevokeSessionRequest,
425 ) -> AuthResult<ServerEndpoint<alibi_core::StatusResponse>> {
426 ServerEndpoint::new("session-management", "revokeSession").with_body(input)
427 }
428
429 #[must_use]
432 pub const fn revoke_other_sessions_endpoint() -> ServerEndpoint<alibi_core::StatusResponse> {
433 ServerEndpoint::new("session-management", "revokeOtherSessions")
434 }
435
436 #[must_use]
439 pub const fn revoke_sessions_endpoint() -> ServerEndpoint<alibi_core::StatusResponse> {
440 ServerEndpoint::new("session-management", "revokeSessions")
441 }
442
443 pub fn update_session_endpoint(
449 input: &impl Serialize,
450 ) -> AuthResult<ServerEndpoint<UpdateSessionResponse>> {
451 ServerEndpoint::new("session-management", "updateSession").with_body(input)
452 }
453
454 async fn update_session_endpoint_response<S: alibi_core::AuthSchema>(
455 &self,
456 call: &EndpointCall,
457 ctx: &AuthContext<S>,
458 ) -> AuthResult<EndpointResponse> {
459 let (user, session) = ctx
460 .require_cached_session(call)
461 .await
462 .map_err(unauthorized_if_unauthenticated)?;
463 call.record_authenticated_session(
464 match &user {
465 alibi_core::AuthenticatedUser::Stored(user) => ctx.user_view(user),
466 alibi_core::AuthenticatedUser::Cached(user) => (**user).clone(),
467 },
468 session.clone(),
469 );
470 let input = call
471 .body()
472 .and_then(JsValue::as_object)
473 .ok_or_else(|| crate::endpoint::validation("[body] Invalid input: expected record"))?;
474 let allowed = parse_session_update_fields(input, ctx).map_err(|error| match error {
475 FieldInputError::Validation { code, message } => AuthError::Api {
476 status: 400,
477 code: Some(code.into()),
478 message,
479 },
480 FieldInputError::Transform(error) => error,
481 })?;
482 let response = self
483 .update_session_response(call, ctx, &user, &session, allowed)
484 .await?;
485 let value: JsValue = alibi_core::utils::json::from_slice(&response.body)?;
486 let mut result = if response.status >= 400 {
487 EndpointResponse::error(AuthError::Api {
488 status: response.status,
489 code: value
490 .get("code")
491 .and_then(JsValue::as_str)
492 .map(str::to_owned),
493 message: value
494 .get("message")
495 .and_then(JsValue::as_str)
496 .unwrap_or_default()
497 .into(),
498 })
499 .with_error_body(value)
500 } else {
501 EndpointResponse::value(value)
502 };
503 for (name, value) in response.headers {
504 if !name.eq_ignore_ascii_case("content-type") {
505 result = result.with_header(name, value);
506 }
507 }
508 Ok(result)
509 }
510
511 async fn handle_update_session(
512 &self,
513 req: &AuthRequest,
514 ctx: &AuthContext<impl alibi_core::AuthSchema>,
515 ) -> AuthResult<AuthResponse> {
516 if req.body.is_some() {
517 let content_type = req
518 .header("content-type")
519 .map(String::as_str)
520 .unwrap_or_default();
521 if !content_type
522 .split(';')
523 .next()
524 .unwrap_or_default()
525 .trim()
526 .to_lowercase()
527 .contains("application/json")
528 {
529 let message = if content_type.is_empty() {
530 "Content-Type is required. Allowed types: application/json".to_owned()
531 } else {
532 format!(
533 "Content-Type \"{content_type}\" is not allowed. Allowed types: application/json"
534 )
535 };
536 return Ok(AuthResponse::json(
537 415,
538 &serde_json::json!({"code":codes::UNSUPPORTED_MEDIA_TYPE,"message":message}),
539 )?);
540 }
541 }
542 let value: Option<JsValue> = match req.body.as_ref() {
543 Some(body) => match alibi_core::utils::json::from_slice(body) {
544 Ok(value) => Some(value),
545 Err(_) => {
546 return Ok(AuthResponse::json(
547 400,
548 &serde_json::json!({"code":codes::BAD_REQUEST,"message":"Invalid JSON in request body"}),
549 )?);
550 }
551 },
552 None => None,
553 };
554 let Some(input) = value.as_ref().and_then(JsValue::as_object) else {
555 return Ok(validation_response(&format!(
556 "[body] Invalid input: expected record, received {}",
557 json_type(value.as_ref())
558 )));
559 };
560 let (user, session) = match super::helpers::ordinary_session(req, ctx).await {
561 Ok(session) => session,
562 Err(AuthError::Unauthenticated | AuthError::SessionNotFound) => {
563 return Ok(AuthResponse::json(
564 401,
565 &serde_json::json!({"code":codes::UNAUTHORIZED,"message":"Unauthorized"}),
566 )?);
567 }
568 Err(error) => return Err(error),
569 };
570 let allowed = match parse_session_update_fields(input, ctx) {
571 Ok(allowed) => allowed,
572 Err(FieldInputError::Validation { code, message }) => {
573 return Ok(AuthResponse::json(
574 400,
575 &serde_json::json!({"code":code,"message":message}),
576 )?);
577 }
578 Err(FieldInputError::Transform(AuthError::Internal(_))) => {
579 return Ok(AuthResponse::new(500));
580 }
581 Err(FieldInputError::Transform(error)) => return Err(error),
582 };
583 self.update_session_response(req, ctx, &user, &session, allowed)
584 .await
585 }
586
587 async fn update_session_response<S: alibi_core::AuthSchema>(
588 &self,
589 req: &impl SessionRequest,
590 ctx: &AuthContext<S>,
591 user: &alibi_core::AuthenticatedUser<S>,
592 session: &SessionView,
593 allowed: FieldValues,
594 ) -> AuthResult<AuthResponse> {
595 if !allowed.has_input_fields() {
596 return Ok(AuthResponse::json(
597 400,
598 &serde_json::json!({"message":"No fields to update"}),
599 )?);
600 }
601 let updated = match ctx
602 .database
603 .update_session_fields_record(session.token(), allowed)
604 .await
605 {
606 Ok(updated) => updated,
607 Err(error @ AuthError::Internal(_)) => {
608 return Err(AuthError::CallbackFailure(Box::new(error)));
609 }
610 Err(error) => return Err(error),
611 };
612 let Some(updated) = updated else {
613 let mut response = AuthResponse::json(
614 401,
615 &serde_json::json!({"code":codes::FAILED_TO_GET_SESSION,"message":"Failed to get session"}),
616 )?;
617 for cookie in delete_session_cookie_headers(&ctx.config)? {
618 response.headers.append("Set-Cookie", cookie);
619 }
620 return Ok(response);
621 };
622
623 alibi_core::session::cookie_cache::runtime::emit_issuance(ctx, user, &updated).await?;
624
625 let preference = related_cookie_name(&ctx.config, "dont_remember");
626 let dont_remember = req.session_headers().get("cookie").is_some_and(|header| {
627 cookie::Cookie::split_parse(header)
628 .flatten()
629 .find(|cookie| cookie.name() == preference)
630 .and_then(|cookie| verify_cookie_value(cookie.value(), ctx.config.current_secret()))
631 .is_some_and(|signed| !signed.is_empty())
632 });
633 with_session_cookies(
634 AuthResponse::json(
635 200,
636 &serde_json::json!({"session":ctx.session_view(&updated)}),
637 )?,
638 &ctx.config,
639 updated.token(),
640 dont_remember,
641 )
642 }
643
644 pub(crate) async fn handle_get_session(
645 &self,
646 req: &AuthRequest,
647 ctx: &AuthContext<impl alibi_core::AuthSchema>,
648 ) -> AuthResult<AuthResponse> {
649 let response = match self.get_session_response(req, ctx).await {
650 Ok(response) => AuthResponse::json(200, &response)?,
651 Err(error) => error.to_auth_response(),
652 };
653 Ok(response
654 .with_header("cache-control", "no-store")
655 .with_header("pragma", "no-cache"))
656 }
657
658 async fn get_session_response(
659 &self,
660 req: &impl SessionRequest,
661 ctx: &AuthContext<impl alibi_core::AuthSchema>,
662 ) -> AuthResult<Option<GetSessionResponse>> {
663 if req.session_method() == &HttpMethod::Post && !ctx.config.session.defer_session_refresh {
664 return Err(AuthError::Upstream {
665 status: 405,
666 code: codes::METHOD_NOT_ALLOWED_DEFER_SESSION_REQUIRED,
667 message: "POST method requires deferSessionRefresh to be enabled in session config",
668 });
669 }
670 let read = alibi_core::session::cookie_cache::runtime::authenticated(ctx, req, true).await;
671 if let Some(call) = req.endpoint_call()
674 && let Some((user, session)) = call.session_hook_snapshot()
675 {
676 call.record_authenticated_session(user, session);
677 }
678 let Some(read) = read.map_err(|error| match error {
679 error @ (AuthError::Upstream { .. } | AuthError::Api { .. }) => error,
680 _ => AuthError::Upstream {
681 status: 500,
682 code: codes::FAILED_TO_GET_SESSION,
683 message: "Failed to get session",
684 },
685 })?
686 else {
687 return Ok(None);
688 };
689 Ok(Some(GetSessionResponse {
690 session: read.session,
691 user: match read.user {
692 alibi_core::AuthenticatedUser::Stored(user) => ctx.user_view(&user),
693 alibi_core::AuthenticatedUser::Cached(user) => *user,
694 },
695 needs_refresh: read.needs_refresh,
696 }))
697 }
698
699 async fn handle_sign_out(
700 &self,
701 req: &AuthRequest,
702 ctx: &AuthContext<impl alibi_core::AuthSchema>,
703 ) -> AuthResult<AuthResponse> {
704 let body = if req.body.is_some() {
705 match parse_body::<SignOutRequest>(req) {
706 Ok(body) => body,
707 Err(response) => return Ok(response),
708 }
709 } else {
710 SignOutRequest::default()
711 };
712 self.sign_out_response(req, &body, ctx).await
713 }
714
715 async fn sign_out_response(
716 &self,
717 req: &impl SessionRequest,
718 body: &SignOutRequest,
719 ctx: &AuthContext<impl alibi_core::AuthSchema>,
720 ) -> AuthResult<AuthResponse> {
721 let mut current_user = None;
722 if let Some(token) = ctx.session_manager().extract_session_token(req) {
723 if let Ok(Some(session)) = ctx.database.get_session(&token).await {
724 if ctx
727 .database
728 .get_user_by_id(&session.user_id())
729 .await
730 .ok()
731 .flatten()
732 .is_some()
733 {
734 current_user = Some(session.user_id().into_owned());
735 }
736 _ = sign_out_core(&session, ctx).await;
737 } else {
738 _ = ctx.database.delete_session(&token).await;
739 }
740 }
741
742 let mut response = AuthResponse::json(
743 200,
744 &SignOutResponse {
745 success: true,
746 url: None,
747 redirect: None,
748 },
749 )?;
750 for cookie in alibi_core::session::cookie_cache::runtime::session_cleanup_headers(
751 &ctx.config,
752 req.session_headers(),
753 false,
754 )? {
755 response.headers.append("Set-Cookie", cookie);
756 }
757 if let Some(user_id) = current_user
758 && let Some(url) = super::oauth::logout::provider_logout_url(&user_id, body, ctx).await
759 {
760 let redirect = body.disable_redirect != Some(true);
761 response.body = AuthResponse::json(
762 200,
763 &serde_json::json!({
764 "success": true, "url": url, "redirect": redirect,
765 }),
766 )?
767 .body;
768 if redirect {
769 _ = response.headers.insert("Location", url);
770 }
771 }
772 Ok(response)
773 }
774
775 async fn handle_list_sessions(
776 &self,
777 req: &AuthRequest,
778 ctx: &AuthContext<impl alibi_core::AuthSchema>,
779 ) -> AuthResult<AuthResponse> {
780 let (user, session) = super::helpers::ordinary_session(req, ctx).await?;
781 if !ctx.session_manager().is_session_fresh(&session) {
782 return Err(AuthError::Upstream {
783 status: 403,
784 code: codes::SESSION_NOT_FRESH,
785 message: "Session is not fresh",
786 });
787 }
788 let Ok(sessions) = list_sessions_core(user.id(), ctx).await else {
789 tracing::error!("Session listing failed");
790 return Ok(AuthResponse::new(500).with_header("content-type", "application/json"));
791 };
792 Ok(AuthResponse::json(200, &sessions)?)
793 }
794
795 async fn handle_revoke_session(
796 &self,
797 req: &AuthRequest,
798 ctx: &AuthContext<impl alibi_core::AuthSchema>,
799 ) -> AuthResult<AuthResponse> {
800 let revoke_req: RevokeSessionRequest = match parse_body(req) {
801 Ok(v) => v,
802 Err(resp) => return Ok(resp),
803 };
804
805 let (user, _) = ctx
806 .require_authoritative_cached_session(req)
807 .await
808 .map_err(unauthorized_if_unauthenticated)?;
809 let response = revoke_session_core(&user, &revoke_req.token, ctx).await?;
810 Ok(AuthResponse::json(200, &response)?)
811 }
812
813 async fn handle_revoke_sessions(
814 &self,
815 req: &AuthRequest,
816 ctx: &AuthContext<impl alibi_core::AuthSchema>,
817 ) -> AuthResult<AuthResponse> {
818 let (user, _) = ctx
819 .require_authoritative_cached_session(req)
820 .await
821 .map_err(unauthorized_if_unauthenticated)?;
822 let response = revoke_sessions_core(user.id(), ctx).await?;
823 Ok(AuthResponse::json(200, &response)?)
824 }
825
826 async fn handle_revoke_other_sessions(
827 &self,
828 req: &AuthRequest,
829 ctx: &AuthContext<impl alibi_core::AuthSchema>,
830 ) -> AuthResult<AuthResponse> {
831 let (user, current_session) = ctx
832 .require_authoritative_cached_session(req)
833 .await
834 .map_err(unauthorized_if_unauthenticated)?;
835 let response = revoke_other_sessions_core(user.id(), ¤t_session, ctx).await?;
836 Ok(AuthResponse::json(200, &response)?)
837 }
838}
839
840impl std::fmt::Debug for SessionManagementPlugin {
841 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
842 f.debug_struct("SessionManagementPlugin")
843 .finish_non_exhaustive()
844 }
845}
846
847pub(crate) async fn sign_out_core(
848 session: &impl AuthSession,
849 ctx: &AuthContext<impl alibi_core::AuthSchema>,
850) -> AuthResult<SuccessResponse> {
851 ctx.database.delete_session(session.token()).await?;
852 Ok(SuccessResponse { success: true })
853}
854
855pub(crate) async fn list_sessions_core(
856 user_id: impl AsRef<str>,
857 ctx: &AuthContext<impl alibi_core::AuthSchema>,
858) -> AuthResult<Vec<SessionView>> {
859 let sessions = ctx
860 .database
861 .get_active_user_sessions_record(user_id.as_ref())
862 .await?;
863 let now = chrono::Utc::now();
864 let mut sessions: Vec<_> = sessions
865 .iter()
866 .filter(|session| session.expires_at() > now)
867 .map(|session| ctx.session_view(session))
868 .collect();
869 if admin_plugin_enabled(ctx) {
870 sessions.retain(|listed| listed.impersonated_by.is_none());
871 }
872 Ok(sessions)
873}
874
875pub(crate) async fn revoke_session_core(
876 user: &impl AuthUser,
877 token: &str,
878 ctx: &AuthContext<impl alibi_core::AuthSchema>,
879) -> AuthResult<StatusResponse> {
880 if let Some(session_to_revoke) = ctx.database.get_session(token).await?
881 && session_to_revoke.user_id() == user.id()
882 {
883 ctx.database
884 .delete_session(token)
885 .await
886 .map_err(revocation_storage_error)?;
887 }
888 Ok(StatusResponse { status: true })
889}
890
891pub(crate) async fn revoke_sessions_core(
892 user_id: impl AsRef<str>,
893 ctx: &AuthContext<impl alibi_core::AuthSchema>,
894) -> AuthResult<StatusResponse> {
895 ctx.database
896 .delete_user_sessions(user_id.as_ref())
897 .await
898 .map_err(revocation_storage_error)?;
899 Ok(StatusResponse { status: true })
900}
901
902pub(crate) async fn revoke_other_sessions_core(
903 user_id: impl AsRef<str>,
904 current_session: &impl AuthSession,
905 ctx: &AuthContext<impl alibi_core::AuthSchema>,
906) -> AuthResult<StatusResponse> {
907 let all_sessions = ctx.session_manager().list_user_sessions(user_id).await?;
908 let tokens: Vec<String> = all_sessions
909 .into_iter()
910 .filter(|session| session.token() != current_session.token())
911 .map(|session| session.token().to_owned())
912 .collect();
913 let count = tokens.len();
914 let database = ctx.database.clone();
915 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
916 crate::helpers::spawn_in_request_context(async move {
918 _ = futures_util::future::join_all(tokens.into_iter().map(|token| {
919 let database = &database;
920 let sender = &sender;
921 async move {
922 let result = database.delete_session(&token).await;
923 _ = sender.send(result);
924 }
925 }))
926 .await;
927 });
928 for _ in 0..count {
929 receiver
930 .recv()
931 .await
932 .ok_or_else(|| AuthError::internal("Session revocation task stopped"))?
933 .map_err(crate::helpers::callback_failure)?;
934 }
935 Ok(StatusResponse { status: true })
936}
937
938#[expect(
939 clippy::needless_pass_by_value,
940 reason = "used by value as a `map_err` callback"
941)]
942fn revocation_storage_error(error: AuthError) -> AuthError {
943 tracing::error!(error = %error, "Session revocation failed");
944 AuthError::Upstream {
945 status: 500,
946 code: codes::INTERNAL_SERVER_ERROR,
947 message: "Internal Server Error",
948 }
949}
950
951fn parse_session_update_fields(
953 input: &indexmap::IndexMap<String, JsValue>,
954 ctx: &AuthContext<impl alibi_core::AuthSchema>,
955) -> Result<FieldValues, FieldInputError> {
956 let fields = ctx.extensions.get::<SessionFields>().map_or_else(
957 || SessionFields(ctx.config.session.additional_fields.clone()),
958 |fields| (*fields).clone(),
959 );
960 fields.parse_update(input)
961}
962
963#[cfg(test)]
965mod tests {
966 use super::*;
967 use crate::test_helpers;
968 use alibi_core::config::AccountConfig;
969 use alibi_core::utils::cookie_utils::related_cookie_name;
970 use alibi_core::wire::SessionView;
971 use alibi_core::{CreateSession, CreateUser};
972 use chrono::{Duration, Utc};
973
974 #[tokio::test]
976 async fn test_get_session_success() {
977 let plugin = SessionManagementPlugin::new();
978 let (ctx, _user, session) = test_helpers::create_test_context_with_user(
979 CreateUser::new()
980 .with_email("test@example.com")
981 .with_name("Test User"),
982 Duration::hours(24),
983 )
984 .await;
985
986 let req = test_helpers::create_auth_request_no_query(
987 HttpMethod::Get,
988 "/get-session",
989 Some(&session.token),
990 None,
991 );
992 let response = plugin.handle_get_session(&req, &ctx).await.unwrap();
993
994 assert_eq!(response.status, 200);
995
996 let body_str = String::from_utf8(response.body).unwrap();
997 let response_data: serde_json::Value = serde_json::from_str(&body_str).unwrap();
998 assert_eq!(
999 (*(*(response_data)
1000 .get("session")
1001 .unwrap_or(&serde_json::Value::Null))
1002 .get("token")
1003 .unwrap_or(&serde_json::Value::Null))
1004 .as_str()
1005 .unwrap(),
1006 session.token
1007 );
1008 assert_eq!(
1009 (*(*(response_data)
1010 .get("user")
1011 .unwrap_or(&serde_json::Value::Null))
1012 .get("email")
1013 .unwrap_or(&serde_json::Value::Null))
1014 .as_str()
1015 .map(ToOwned::to_owned),
1016 Some("test@example.com".to_owned())
1017 );
1018 }
1019
1020 #[tokio::test]
1022 async fn test_get_session_unauthorized() {
1023 let plugin = SessionManagementPlugin::new();
1025 let (ctx, _user, _session) = test_helpers::create_test_context_with_user(
1026 CreateUser::new()
1027 .with_email("test@example.com")
1028 .with_name("Test User"),
1029 Duration::hours(24),
1030 )
1031 .await;
1032
1033 let req =
1034 test_helpers::create_auth_request_no_query(HttpMethod::Get, "/get-session", None, None);
1035 let response = plugin.handle_get_session(&req, &ctx).await.unwrap();
1036 assert_eq!(response.status, 200);
1037 let body: serde_json::Value = serde_json::from_slice(&response.body).expect("valid JSON");
1038 assert!(body.is_null());
1039 }
1040
1041 #[tokio::test]
1043 async fn test_sign_out_success() {
1044 let plugin = SessionManagementPlugin::new();
1045 let (ctx, _user, session) = test_helpers::create_test_context_with_user(
1046 CreateUser::new()
1047 .with_email("test@example.com")
1048 .with_name("Test User"),
1049 Duration::hours(24),
1050 )
1051 .await;
1052
1053 let req = test_helpers::create_auth_request_no_query(
1054 HttpMethod::Post,
1055 "/sign-out",
1056 Some(&session.token),
1057 Some(b"{}".to_vec()),
1058 );
1059 let response = plugin.handle_sign_out(&req, &ctx).await.unwrap();
1060
1061 assert_eq!(response.status, 200);
1062
1063 let body_str = String::from_utf8(response.body).unwrap();
1064 let response_data: SuccessResponse = serde_json::from_str(&body_str).unwrap();
1065 assert!(response_data.success);
1066
1067 let session_check = ctx.database.get_session(&session.token).await.unwrap();
1068 assert!(session_check.is_none());
1069 }
1070
1071 #[tokio::test]
1072 async fn test_sign_out_clears_account_cookie_when_enabled() {
1073 let plugin = SessionManagementPlugin::new();
1074 let config = test_helpers::create_test_config().account(AccountConfig {
1075 store_account_cookie: true,
1076 ..Default::default()
1077 });
1078 let ctx = test_helpers::create_test_context_with_config(config).await;
1079 let (_user, session) = test_helpers::create_user_and_session(
1080 &ctx,
1081 CreateUser::new()
1082 .with_email("test@example.com")
1083 .with_name("Test User"),
1084 Duration::hours(24),
1085 )
1086 .await;
1087
1088 let req = test_helpers::create_auth_request_no_query(
1089 HttpMethod::Post,
1090 "/sign-out",
1091 Some(&session.token),
1092 Some(b"{}".to_vec()),
1093 );
1094 let response = plugin.handle_sign_out(&req, &ctx).await.unwrap();
1095
1096 let account_cookie_name = format!("{}=", related_cookie_name(&ctx.config, "account_data"));
1097 assert!(
1098 response
1099 .headers
1100 .get_all("Set-Cookie")
1101 .any(|cookie| cookie.starts_with(&account_cookie_name)),
1102 "sign-out should clear the account_data cookie when store_account_cookie is enabled"
1103 );
1104 }
1105
1106 #[tokio::test]
1107 async fn test_sign_out_does_not_emit_account_cookie_when_disabled() {
1108 let plugin = SessionManagementPlugin::new();
1109 let (ctx, _user, session) = test_helpers::create_test_context_with_user(
1110 CreateUser::new()
1111 .with_email("test@example.com")
1112 .with_name("Test User"),
1113 Duration::hours(24),
1114 )
1115 .await;
1116
1117 let req = test_helpers::create_auth_request_no_query(
1118 HttpMethod::Post,
1119 "/sign-out",
1120 Some(&session.token),
1121 Some(b"{}".to_vec()),
1122 );
1123 let response = plugin.handle_sign_out(&req, &ctx).await.unwrap();
1124
1125 let account_cookie_name = format!("{}=", related_cookie_name(&ctx.config, "account_data"));
1126 assert!(
1127 !response
1128 .headers
1129 .get_all("Set-Cookie")
1130 .any(|cookie| cookie.starts_with(&account_cookie_name)),
1131 "sign-out should not emit account_data clearing cookies when store_account_cookie is disabled"
1132 );
1133 }
1134
1135 #[tokio::test]
1137 async fn test_list_sessions_success() {
1138 let plugin = SessionManagementPlugin::new();
1139 let (ctx, user, session) = test_helpers::create_test_context_with_user(
1140 CreateUser::new()
1141 .with_email("test@example.com")
1142 .with_name("Test User"),
1143 Duration::hours(24),
1144 )
1145 .await;
1146
1147 let create_session2 = CreateSession {
1148 additional_fields: alibi_core::field_policy::FieldValues::default(),
1149 token: None,
1150 user_id: user.id.clone(),
1151 expires_at: Utc::now() + Duration::hours(24),
1152 ip_address: Some("192.168.1.1".to_owned()),
1153 user_agent: Some("another-agent".to_owned()),
1154 impersonated_by: None,
1155 active_organization_id: None,
1156 active_team_id: None,
1157 };
1158 ctx.database.create_session(create_session2).await.unwrap();
1159
1160 let req = test_helpers::create_auth_request_no_query(
1161 HttpMethod::Get,
1162 "/list-sessions",
1163 Some(&session.token),
1164 None,
1165 );
1166 let response = plugin.handle_list_sessions(&req, &ctx).await.unwrap();
1167
1168 assert_eq!(response.status, 200);
1169
1170 let body_str = String::from_utf8(response.body).unwrap();
1171 let sessions: Vec<SessionView> = serde_json::from_str(&body_str).unwrap();
1172 assert_eq!(sessions.len(), 2);
1173 }
1174
1175 #[tokio::test]
1176 async fn test_list_sessions_filters_impersonated_sessions_when_admin_plugin_is_enabled() {
1177 let plugin = SessionManagementPlugin::new();
1178 let (mut ctx, user, session) = test_helpers::create_test_context_with_user(
1179 CreateUser::new()
1180 .with_email("test@example.com")
1181 .with_name("Test User"),
1182 Duration::hours(24),
1183 )
1184 .await;
1185 ctx.set_metadata("admin.enabled", serde_json::Value::Bool(true));
1186
1187 let direct_session = CreateSession {
1188 additional_fields: alibi_core::field_policy::FieldValues::default(),
1189 token: None,
1190 user_id: user.id.clone(),
1191 expires_at: Utc::now() + Duration::hours(24),
1192 ip_address: Some("192.168.1.1".to_owned()),
1193 user_agent: Some("another-agent".to_owned()),
1194 impersonated_by: None,
1195 active_organization_id: None,
1196 active_team_id: None,
1197 };
1198 ctx.database.create_session(direct_session).await.unwrap();
1199
1200 let impersonated_session = CreateSession {
1201 additional_fields: alibi_core::field_policy::FieldValues::default(),
1202 token: None,
1203 user_id: user.id.clone(),
1204 expires_at: Utc::now() + Duration::hours(24),
1205 ip_address: Some("10.0.0.5".to_owned()),
1206 user_agent: Some("impersonated-agent".to_owned()),
1207 impersonated_by: Some("admin-user".to_owned()),
1208 active_organization_id: None,
1209 active_team_id: None,
1210 };
1211 let impersonated = ctx
1212 .database
1213 .create_session(impersonated_session)
1214 .await
1215 .unwrap();
1216
1217 let req = test_helpers::create_auth_request_no_query(
1218 HttpMethod::Get,
1219 "/list-sessions",
1220 Some(&session.token),
1221 None,
1222 );
1223 let response = plugin.handle_list_sessions(&req, &ctx).await.unwrap();
1224
1225 assert_eq!(response.status, 200);
1226
1227 let body_str = String::from_utf8(response.body).unwrap();
1228 let sessions: Vec<SessionView> = serde_json::from_str(&body_str).unwrap();
1229 assert_eq!(sessions.len(), 2);
1230 assert!(
1231 sessions
1232 .iter()
1233 .all(|candidate| candidate.token != impersonated.token),
1234 "impersonated sessions should not be returned from /list-sessions"
1235 );
1236 }
1237
1238 #[tokio::test]
1240 async fn test_revoke_session_success() {
1241 let plugin = SessionManagementPlugin::new();
1242 let (ctx, user, session) = test_helpers::create_test_context_with_user(
1243 CreateUser::new()
1244 .with_email("test@example.com")
1245 .with_name("Test User"),
1246 Duration::hours(24),
1247 )
1248 .await;
1249
1250 let create_session2 = CreateSession {
1251 additional_fields: alibi_core::field_policy::FieldValues::default(),
1252 token: None,
1253 user_id: user.id.clone(),
1254 expires_at: Utc::now() + Duration::hours(24),
1255 ip_address: Some("192.168.1.1".to_owned()),
1256 user_agent: Some("another-agent".to_owned()),
1257 impersonated_by: None,
1258 active_organization_id: None,
1259 active_team_id: None,
1260 };
1261 let session2 = ctx.database.create_session(create_session2).await.unwrap();
1262
1263 let body = serde_json::json!({ "token": session2.token });
1264 let req = test_helpers::create_auth_request_no_query(
1265 HttpMethod::Post,
1266 "/revoke-session",
1267 Some(&session.token),
1268 Some(body.to_string().into_bytes()),
1269 );
1270
1271 let response = plugin.handle_revoke_session(&req, &ctx).await.unwrap();
1272 assert_eq!(response.status, 200);
1273
1274 let session2_check = ctx.database.get_session(&session2.token).await.unwrap();
1275 assert!(session2_check.is_none());
1276
1277 let session1_check = ctx.database.get_session(&session.token).await.unwrap();
1278 assert!(session1_check.is_some());
1279 }
1280
1281 #[tokio::test]
1283 async fn test_revoke_session_forbidden_different_user() {
1284 let plugin = SessionManagementPlugin::new();
1285 let (ctx, _user1, session1) = test_helpers::create_test_context_with_user(
1286 CreateUser::new()
1287 .with_email("test@example.com")
1288 .with_name("Test User"),
1289 Duration::hours(24),
1290 )
1291 .await;
1292
1293 let create_user2 = CreateUser::new()
1294 .with_email("user2@example.com")
1295 .with_name("User Two");
1296 let user2 = ctx.database.create_user(create_user2).await.unwrap();
1297
1298 let create_session2 = CreateSession {
1299 additional_fields: alibi_core::field_policy::FieldValues::default(),
1300 token: None,
1301 user_id: user2.id,
1302 expires_at: Utc::now() + Duration::hours(24),
1303 ip_address: Some("192.168.1.1".to_owned()),
1304 user_agent: Some("another-agent".to_owned()),
1305 impersonated_by: None,
1306 active_organization_id: None,
1307 active_team_id: None,
1308 };
1309 let session2 = ctx.database.create_session(create_session2).await.unwrap();
1310
1311 let body = serde_json::json!({ "token": session2.token });
1312 let req = test_helpers::create_auth_request_no_query(
1313 HttpMethod::Post,
1314 "/revoke-session",
1315 Some(&session1.token),
1316 Some(body.to_string().into_bytes()),
1317 );
1318
1319 let response = plugin.handle_revoke_session(&req, &ctx).await.unwrap();
1320 assert_eq!(response.status, 200);
1321
1322 let body_2: serde_json::Value = serde_json::from_slice(&response.body).unwrap();
1323 assert_eq!(
1324 (*(body_2).get("status").unwrap_or(&serde_json::Value::Null)),
1325 true
1326 );
1327
1328 let still_exists = ctx.database.get_session(&session2.token).await.unwrap();
1329 assert!(
1330 still_exists.is_some(),
1331 "other user's session must not be revoked"
1332 );
1333 }
1334
1335 #[tokio::test]
1337 async fn test_revoke_sessions_success() {
1338 let plugin = SessionManagementPlugin::new();
1339 let (ctx, user, session1) = test_helpers::create_test_context_with_user(
1340 CreateUser::new()
1341 .with_email("test@example.com")
1342 .with_name("Test User"),
1343 Duration::hours(24),
1344 )
1345 .await;
1346
1347 let create_session2 = CreateSession {
1348 additional_fields: alibi_core::field_policy::FieldValues::default(),
1349 token: None,
1350 user_id: user.id.clone(),
1351 expires_at: Utc::now() + Duration::hours(24),
1352 ip_address: Some("192.168.1.1".to_owned()),
1353 user_agent: Some("another-agent".to_owned()),
1354 impersonated_by: None,
1355 active_organization_id: None,
1356 active_team_id: None,
1357 };
1358 ctx.database.create_session(create_session2).await.unwrap();
1359
1360 let req = test_helpers::create_auth_request_no_query(
1361 HttpMethod::Post,
1362 "/revoke-sessions",
1363 Some(&session1.token),
1364 Some(b"{}".to_vec()),
1365 );
1366 let response = plugin.handle_revoke_sessions(&req, &ctx).await.unwrap();
1367
1368 assert_eq!(response.status, 200);
1369
1370 let user_sessions = ctx.database.get_user_sessions(&user.id).await.unwrap();
1371 assert_eq!(user_sessions.len(), 0);
1372 }
1373
1374 #[tokio::test]
1376 async fn test_plugin_routes() {
1377 let plugin = SessionManagementPlugin::new();
1378 let routes = AuthPlugin::<
1379 alibi_seaorm::store::__private_test_support::bundled_schema::BundledSchema,
1380 >::routes(&plugin);
1381
1382 assert_eq!(routes.len(), 8);
1383 assert!(
1384 routes
1385 .iter()
1386 .any(|r| r.path == "/get-session" && r.method == HttpMethod::Get)
1387 );
1388 assert!(
1390 routes
1391 .iter()
1392 .any(|r| r.path == "/get-session" && r.method == HttpMethod::Post)
1393 );
1394 assert!(
1395 routes
1396 .iter()
1397 .any(|r| r.path == "/sign-out" && r.method == HttpMethod::Post)
1398 );
1399 assert!(
1400 routes
1401 .iter()
1402 .any(|r| r.path == "/list-sessions" && r.method == HttpMethod::Get)
1403 );
1404 assert!(
1405 routes
1406 .iter()
1407 .any(|r| r.path == "/revoke-session" && r.method == HttpMethod::Post)
1408 );
1409 assert!(
1410 routes
1411 .iter()
1412 .any(|r| r.path == "/revoke-sessions" && r.method == HttpMethod::Post)
1413 );
1414 }
1415
1416 #[tokio::test]
1418 async fn test_plugin_on_request_routing() {
1419 let plugin = SessionManagementPlugin::new();
1420 let (ctx, _user, session) = test_helpers::create_test_context_with_user(
1421 CreateUser::new()
1422 .with_email("test@example.com")
1423 .with_name("Test User"),
1424 Duration::hours(24),
1425 )
1426 .await;
1427
1428 let req = test_helpers::create_auth_request_no_query(
1430 HttpMethod::Get,
1431 "/get-session",
1432 Some(&session.token),
1433 None,
1434 );
1435 let response = plugin.on_request(&req, &ctx).await.unwrap();
1436 assert!(response.is_some());
1437 assert_eq!(response.unwrap().status, 200);
1438
1439 let req_2 = test_helpers::create_auth_request_no_query(
1442 HttpMethod::Post,
1443 "/get-session",
1444 Some(&session.token),
1445 Some(b"{}".to_vec()),
1446 );
1447 let response_2 = plugin.on_request(&req_2, &ctx).await.unwrap().unwrap();
1448 assert_eq!(response_2.status, 405);
1449 assert_eq!(
1450 serde_json::from_slice::<serde_json::Value>(&response_2.body).unwrap(),
1451 serde_json::json!({
1452 "code": "METHOD_NOT_ALLOWED_DEFER_SESSION_REQUIRED",
1453 "message": "POST method requires deferSessionRefresh to be enabled in session config",
1454 }),
1455 );
1456 assert_eq!(
1457 response_2.headers.get("cache-control"),
1458 Some(&"no-store".to_owned())
1459 );
1460 assert_eq!(
1461 response_2.headers.get("pragma"),
1462 Some(&"no-cache".to_owned())
1463 );
1464
1465 let req_3 = test_helpers::create_auth_request_no_query(
1467 HttpMethod::Get,
1468 "/invalid-route",
1469 Some(&session.token),
1470 None,
1471 );
1472 let response_3 = plugin.on_request(&req_3, &ctx).await.unwrap();
1473 assert!(response_3.is_none());
1474 }
1475
1476 #[tokio::test]
1478 async fn test_configuration() {
1479 let plugin = SessionManagementPlugin::new()
1480 .enable_session_listing(false)
1481 .enable_session_revocation(false)
1482 .require_authentication(false);
1483
1484 assert!(!plugin.config.enable_session_listing);
1485 assert!(!plugin.config.enable_session_revocation);
1486 assert!(!plugin.config.require_authentication);
1487
1488 let (ctx, _user, session) = test_helpers::create_test_context_with_user(
1489 CreateUser::new()
1490 .with_email("test@example.com")
1491 .with_name("Test User"),
1492 Duration::hours(24),
1493 )
1494 .await;
1495
1496 let req = test_helpers::create_auth_request_no_query(
1497 HttpMethod::Get,
1498 "/list-sessions",
1499 Some(&session.token),
1500 None,
1501 );
1502 let response = plugin.on_request(&req, &ctx).await.unwrap();
1503 assert!(response.is_none());
1504
1505 let req_2 = test_helpers::create_auth_request_no_query(
1506 HttpMethod::Post,
1507 "/revoke-session",
1508 Some(&session.token),
1509 Some(b"{}".to_vec()),
1510 );
1511 let response_2 = plugin.on_request(&req_2, &ctx).await.unwrap();
1512 assert!(response_2.is_none());
1513 }
1514}
1515