Skip to main content

alibi_core/plugin/
context.rs

1use super::*;
2/// Context passed to plugin methods.
3pub struct AuthContext<S: AuthSchema> {
4    pub config: Arc<AuthConfig>,
5    pub database: Arc<dyn AuthStore<S>>,
6    pub email_provider: Option<Arc<dyn EmailProvider>>,
7    pub metadata: MetadataMap,
8    pub extensions: ContextExtensions,
9}
10
11impl<S: AuthSchema> AuthContext<S> {
12    /// Parse configured user fields at the endpoint boundary before creation
13    /// validation or adapter hooks. Unknown fields are ignored by this policy.
14    pub fn parse_user_fields(
15        &self,
16        input: &indexmap::IndexMap<String, crate::utils::json::JsValue>,
17        creation: bool,
18    ) -> Result<crate::field_policy::FieldValues, crate::field_policy::FieldInputError> {
19        let registered = self.extensions.get::<crate::field_policy::UserFields>();
20        let configured =
21            crate::field_policy::SessionFields(self.config.user.additional_fields.clone());
22        let fields = registered.as_ref().map_or(&configured, |fields| &fields.0);
23        if creation {
24            fields.parse_create(input)
25        } else {
26            fields.parse_update(input)
27        }
28    }
29
30    /// Hash through initialized policies at the current actual endpoint.
31    ///
32    /// # Errors
33    /// Propagates policy rejections and original hasher errors.
34    pub async fn hash_password(
35        &self,
36        hasher: Option<&Arc<dyn crate::utils::password::PasswordHasher>>,
37        password: &str,
38    ) -> AuthResult<String> {
39        let context = crate::endpoint::current_endpoint_call_context().map_or_else(
40            || {
41                crate::hooks::current_request_hook_context()
42                    .map(crate::utils::password::PasswordHashContext::from_request)
43            },
44            |call| {
45                Some(crate::utils::password::PasswordHashContext {
46                    path: call.path().map(str::to_owned),
47                    request: call
48                        .request()
49                        .map(crate::hooks::RequestHookContext::from_request),
50                })
51            },
52        );
53        self.hash_password_with_context(hasher, password, context.as_ref())
54            .await
55    }
56
57    /// Hash for a trusted endpoint call whose logical context is independent
58    /// of an optional physical request, including server-only APIs.
59    ///
60    /// # Errors
61    /// Propagates policy rejections and original hasher errors.
62    pub async fn hash_password_with_context(
63        &self,
64        hasher: Option<&Arc<dyn crate::utils::password::PasswordHasher>>,
65        password: &str,
66        context: Option<&crate::utils::password::PasswordHashContext>,
67    ) -> AuthResult<String> {
68        if let Some(hooks) = self
69            .extensions
70            .get::<crate::utils::password::PasswordHashHooks>()
71        {
72            for hook in hooks.0.iter().rev() {
73                hook.before_hash(password, context).await?;
74            }
75        }
76        crate::utils::password::hash_password(hasher, password).await
77    }
78
79    #[must_use]
80    pub fn new(config: Arc<AuthConfig>, database: Arc<dyn AuthStore<S>>) -> Self {
81        let email_provider = config.email_provider.clone();
82        Self {
83            config,
84            database,
85            email_provider,
86            metadata: MetadataMap::new(),
87            extensions: ContextExtensions::default(),
88        }
89    }
90
91    #[must_use]
92    pub fn with_metadata(
93        config: Arc<AuthConfig>,
94        database: Arc<dyn AuthStore<S>>,
95        metadata: MetadataMap,
96    ) -> Self {
97        let email_provider = config.email_provider.clone();
98        Self {
99            config,
100            database,
101            email_provider,
102            metadata,
103            extensions: ContextExtensions::default(),
104        }
105    }
106
107    pub fn set_metadata(&mut self, key: impl Into<String>, value: serde_json::Value) {
108        drop(self.metadata.insert(key.into(), value));
109    }
110
111    #[must_use]
112    pub fn get_metadata(&self, key: &str) -> Option<&serde_json::Value> {
113        self.metadata.get(key)
114    }
115
116    #[must_use]
117    pub fn with_extensions(mut self, extensions: ContextExtensions) -> Self {
118        self.extensions = extensions;
119        self
120    }
121
122    #[must_use]
123    pub fn email_verification_override(&self) -> Option<Arc<VerificationEmailOverrideHandle<S>>> {
124        self.extensions.get()
125    }
126
127    pub fn user_view(&self, user: &impl crate::entity::AuthUser) -> crate::wire::UserView {
128        self.project_user_view(user, true)
129    }
130
131    /// Project trusted adapter output without removing declared hidden fields.
132    /// Canonical identity accessors still refer to the physical model.
133    pub fn trusted_user_view(&self, user: &impl crate::entity::AuthUser) -> crate::wire::UserView {
134        self.project_user_view(user, false)
135    }
136
137    pub(in crate::plugin) fn project_user_view(
138        &self,
139        user: &impl crate::entity::AuthUser,
140        public: bool,
141    ) -> crate::wire::UserView {
142        if let Some(view) = user.retained_user_view() {
143            let mut view = view.clone();
144            if public {
145                let registered = self.extensions.get::<crate::field_policy::UserFields>();
146                let fields = registered
147                    .as_ref()
148                    .map_or(&self.config.user.additional_fields, |fields| &fields.0.0);
149                view.extension_fields
150                    .retain(|name, _| fields.get(name).is_none_or(|field| field.returned));
151            }
152            return view;
153        }
154        let mut view = crate::wire::UserView::from(user);
155        if self.feature_enabled("username.enabled") {
156            for (key, absent) in [
157                ("username", view.username.is_none()),
158                ("displayUsername", view.display_username.is_none()),
159            ] {
160                if absent {
161                    drop(
162                        view.extension_fields
163                            .insert(key.into(), serde_json::Value::Null),
164                    );
165                }
166            }
167        } else {
168            view.username = None;
169            view.display_username = None;
170        }
171        if self.feature_enabled("two_factor.enabled") {
172            view.two_factor_enabled = user.two_factor_enabled_value();
173            if view.two_factor_enabled.is_none() {
174                drop(
175                    view.extension_fields
176                        .insert("twoFactorEnabled".into(), serde_json::Value::Null),
177                );
178            }
179        } else {
180            view.two_factor_enabled = None;
181        }
182        if self.feature_enabled("admin.enabled") {
183            view.banned = user.banned_value();
184            for (key, absent) in [
185                ("role", view.role.is_none()),
186                ("banned", view.banned.is_none()),
187                ("banReason", view.ban_reason.is_none()),
188                ("banExpires", view.ban_expires.is_none()),
189            ] {
190                if absent {
191                    drop(
192                        view.extension_fields
193                            .insert(key.into(), serde_json::Value::Null),
194                    );
195                }
196            }
197        } else {
198            view.role = None;
199            view.banned = None;
200            view.ban_reason = None;
201            view.ban_expires = None;
202        }
203
204        if self.feature_enabled("anonymous.enabled") {
205            view.is_anonymous = Some(user.is_anonymous().unwrap_or(false));
206        } else {
207            view.is_anonymous = None;
208        }
209        if self.feature_enabled("phone-number.enabled") {
210            if view.phone_number.is_none() {
211                drop(
212                    view.extension_fields
213                        .insert("phoneNumber".into(), serde_json::Value::Null),
214                );
215            }
216            if view.phone_number_verified.is_none() {
217                drop(
218                    view.extension_fields
219                        .insert("phoneNumberVerified".into(), serde_json::Value::Null),
220                );
221            }
222        } else {
223            view.phone_number = None;
224            view.phone_number_verified = None;
225        }
226        if self.feature_enabled("last-login-method.enabled") {
227            if view.last_login_method.is_none() {
228                drop(
229                    view.extension_fields
230                        .insert("lastLoginMethod".into(), serde_json::Value::Null),
231                );
232            }
233        } else {
234            view.last_login_method = None;
235        }
236        let registered = self.extensions.get::<crate::field_policy::UserFields>();
237        let fields = registered
238            .as_ref()
239            .map_or(&self.config.user.additional_fields, |fields| &fields.0.0);
240        let physical = user.additional_fields();
241        let values = user
242            .adapter_snapshot()
243            .map(crate::AdapterOutput::values)
244            .unwrap_or(&physical);
245        for (name, field) in fields {
246            drop(view.extension_fields.remove(name));
247            if (!public || field.returned)
248                && let Some(value) = values.get(name).or_else(|| {
249                    field
250                        .field_name
251                        .as_ref()
252                        .and_then(|physical| values.get(physical))
253                })
254            {
255                drop(view.extension_fields.insert(name.clone(), value.clone()));
256            }
257        }
258        if let Some(snapshot) = user.adapter_snapshot() {
259            // An ephemeral adapter can omit a property rather than persist SQL
260            // NULL. Output snapshots retain that distinction through caching.
261            for name in [
262                "id",
263                "name",
264                "email",
265                "emailVerified",
266                "image",
267                "createdAt",
268                "updatedAt",
269                "username",
270                "displayUsername",
271                "twoFactorEnabled",
272                "role",
273                "banned",
274                "banReason",
275                "banExpires",
276                "isAnonymous",
277                "phoneNumber",
278                "phoneNumberVerified",
279                "lastLoginMethod",
280            ] {
281                if !snapshot.contains_field(name) || snapshot.field_is_undefined(name) {
282                    let _ = view.omitted_fields.insert(name.into());
283                }
284            }
285        }
286        view
287    }
288
289    /// Retain configured output for an already authenticated physical user.
290    pub(crate) async fn user_adapter_record(
291        &self,
292        user: S::User,
293    ) -> AuthResult<crate::AdapterRecord<S::User>> {
294        use crate::AuthUser;
295        let registered = self
296            .extensions
297            .get::<crate::field_policy::AdapterFieldPolicies>();
298        let fields = registered.as_ref().map_or_else(
299            || {
300                crate::field_policy::SessionAdapterFields(Arc::new(
301                    self.config.user.additional_fields.clone(),
302                ))
303            },
304            |fields| fields.user.clone(),
305        );
306        let output = fields
307            .record_output(
308                serde_json::to_value(
309                    user.retained_user_view()
310                        .cloned()
311                        .unwrap_or_else(|| crate::UserView::from(&user)),
312                )?,
313                user.additional_fields(),
314                serde_json::to_value(self.trusted_user_view(&user))?,
315            )
316            .await?;
317        Ok(crate::AdapterRecord::with_output(user, output))
318    }
319
320    /// Preserve the initialized adapter result's physical authority and declared
321    /// undefined presence while applying the public user field policy once.
322    #[must_use]
323    pub fn filter_user_record(
324        &self,
325        record: crate::AdapterRecord<S::User>,
326    ) -> crate::AdapterRecord<S::User> {
327        let registered = self.extensions.get::<crate::field_policy::UserFields>();
328        let fields = registered
329            .as_ref()
330            .map_or(&self.config.user.additional_fields, |fields| &fields.0.0);
331        let output = record.raw_snapshot().filter_returned(fields);
332        crate::AdapterRecord::with_output(record.into_stored(), output)
333    }
334
335    /// Public account output retains declared adapter projections and always
336    /// removes credentials, including explicitly returned additional fields.
337    ///
338    /// # Errors
339    /// Returns an error when the canonical account view cannot be serialized.
340    pub fn account_view(
341        &self,
342        account: &impl crate::entity::AuthAccount,
343    ) -> AuthResult<serde_json::Map<String, serde_json::Value>> {
344        let serde_json::Value::Object(mut view) =
345            serde_json::to_value(crate::wire::AccountView::from(account))?
346        else {
347            return Err(AuthError::internal("Account view must be an object"));
348        };
349        let registered = self.extensions.get::<crate::field_policy::AccountFields>();
350        let fields = registered
351            .as_ref()
352            .map_or(&self.config.account.additional_fields, |fields| &fields.0.0);
353        let physical = account.additional_fields();
354        let values = account
355            .adapter_snapshot()
356            .map(crate::AdapterOutput::values)
357            .unwrap_or(&physical);
358        for (name, field) in fields {
359            drop(view.remove(name));
360            if field.returned
361                && let Some(value) = values.get(name).or_else(|| {
362                    field
363                        .field_name
364                        .as_ref()
365                        .and_then(|physical| values.get(physical))
366                })
367            {
368                drop(view.insert(name.clone(), value.clone()));
369            }
370        }
371        for credential in [
372            "accessToken",
373            "refreshToken",
374            "idToken",
375            "accessTokenExpiresAt",
376            "refreshTokenExpiresAt",
377            "password",
378        ] {
379            drop(view.remove(credential));
380        }
381        Ok(view)
382    }
383
384    pub fn session_view(&self, session: &impl AuthSession) -> crate::wire::SessionView {
385        self.project_session_view(session, true)
386    }
387
388    pub fn trusted_session_view(&self, session: &impl AuthSession) -> crate::wire::SessionView {
389        self.project_session_view(session, false)
390    }
391
392    pub(in crate::plugin) fn project_session_view(
393        &self,
394        session: &impl AuthSession,
395        public: bool,
396    ) -> crate::wire::SessionView {
397        if let Some(view) = session.retained_session_view() {
398            let mut view = view.clone();
399            if public {
400                let registered = self.extensions.get::<crate::field_policy::SessionFields>();
401                let fields = registered
402                    .as_ref()
403                    .map_or(&self.config.session.additional_fields, |fields| &fields.0);
404                for (name, field) in fields {
405                    if !field.returned {
406                        let _ignored_clone = view.omitted_fields.insert(name.clone());
407                    }
408                }
409            }
410            return view;
411        }
412        let mut view = crate::wire::SessionView::from(session);
413        let registered = self.extensions.get::<crate::field_policy::SessionFields>();
414        let fields = registered
415            .as_ref()
416            .map_or(&self.config.session.additional_fields, |fields| &fields.0);
417        view.extension_fields
418            .retain(|name, _| fields.contains_key(name));
419        if let Some(output) = session.adapter_snapshot().map(crate::AdapterOutput::values) {
420            for name in fields.keys() {
421                drop(view.extension_fields.remove(name));
422                if let Some(value) = output.get(name) {
423                    drop(view.extension_fields.insert(name.clone(), value.clone()));
424                } else {
425                    let _ignored_clone = view.omitted_fields.insert(name.clone());
426                }
427            }
428        }
429        for (name, field) in fields {
430            if public && !field.returned {
431                let _ignored_clone = view.omitted_fields.insert(name.clone());
432            }
433        }
434        let declared = |name: &str| fields.contains_key(name);
435        if self.feature_enabled("admin.enabled") || declared("impersonatedBy") {
436            if view.impersonated_by.is_none() {
437                drop(
438                    view.extension_fields
439                        .insert("impersonatedBy".into(), serde_json::Value::Null),
440                );
441            }
442        } else {
443            view.impersonated_by = None;
444        }
445        if self.feature_enabled("organization.enabled") || declared("activeOrganizationId") {
446            if view.active_organization_id.is_none() {
447                drop(
448                    view.extension_fields
449                        .insert("activeOrganizationId".into(), serde_json::Value::Null),
450                );
451            }
452        } else {
453            view.active_organization_id = None;
454        }
455
456        if self.feature_enabled("organization.teams.enabled") || declared("activeTeamId") {
457            if view.active_team_id.is_none() {
458                drop(
459                    view.extension_fields
460                        .insert("activeTeamId".into(), serde_json::Value::Null),
461                );
462            }
463        } else {
464            view.active_team_id = None;
465        }
466        view
467    }
468
469    pub fn invitation_view(
470        &self,
471        invitation: &impl crate::entity::AuthInvitation,
472    ) -> crate::wire::InvitationView {
473        let mut view = crate::wire::InvitationView::from(invitation);
474        if self.feature_enabled("organization.teams.enabled") {
475            if view.team_id.is_none() {
476                drop(
477                    view.extension_fields
478                        .insert("teamId".into(), serde_json::Value::Null),
479                );
480            }
481        } else {
482            view.team_id = None;
483        }
484        view
485    }
486
487    pub(in crate::plugin) fn feature_enabled(&self, key: &str) -> bool {
488        self.metadata
489            .get(key)
490            .and_then(serde_json::Value::as_bool)
491            .unwrap_or(false)
492    }
493
494    /// Get the email provider, returning an error if none is configured.
495    ///
496    /// # Errors
497    ///
498    /// Returns a configuration error if no email provider is configured.
499    pub fn email_provider(&self) -> AuthResult<&dyn EmailProvider> {
500        self.email_provider
501            .as_deref()
502            .ok_or_else(|| AuthError::config("No email provider configured"))
503    }
504
505    /// Create a `SessionManager` from this context's config and database.
506    #[must_use]
507    pub fn session_manager(&self) -> SessionManager<S> {
508        SessionManager::new(Arc::clone(&self.config), Arc::clone(&self.database))
509    }
510
511    /// Extract a session token from the request, validate the session, and
512    /// return the authenticated `(User, Session)` pair.
513    ///
514    /// This centralises the pattern previously duplicated across many plugins
515    /// (`get_authenticated_user`, `require_session`, etc.).
516    ///
517    /// # Errors
518    ///
519    /// Returns an authentication error for a missing or invalid session, or propagates storage errors.
520    pub async fn require_session(
521        &self,
522        req: &impl crate::session::SessionRequest,
523    ) -> AuthResult<(S::User, crate::wire::SessionView)> {
524        let (user, session, _) = self.require_session_with_refresh_state(req).await?;
525        Ok((user, session))
526    }
527
528    /// Read an ordinary HTTP session from the configured cache or storage.
529    /// Sensitive stateful operations must continue using the physical APIs.
530    /// Nested source get-session errors become an unauthenticated session;
531    /// errors from later application/store operations are not caught here.
532    /// # Errors
533    /// Returns an error if the request has no valid session or session lookup fails.
534    pub async fn require_cached_session(
535        &self,
536        req: &impl crate::session::SessionRequest,
537    ) -> AuthResult<(crate::AuthenticatedUser<S>, crate::wire::SessionView)> {
538        let read = crate::session::cookie_cache::runtime::authenticated(self, req, false)
539            .await
540            .map_err(|_error| AuthError::Unauthenticated)?
541            .ok_or(AuthError::Unauthenticated)?;
542        Ok((read.user, read.session))
543    }
544
545    /// Read an ordinary HTTP session from the configured cache or storage for
546    /// an application route. Unlike [`Self::require_cached_session`], which
547    /// mirrors upstream's nested get-session and answers every failure with
548    /// `Unauthenticated`, only a missing or invalid session (including a
549    /// malformed cache cookie) is `Unauthenticated` here.
550    /// # Errors
551    /// Returns `Unauthenticated` without a valid session, and propagates
552    /// storage and callback errors.
553    pub async fn require_cached_session_strict(
554        &self,
555        req: &impl crate::session::SessionRequest,
556    ) -> AuthResult<(crate::AuthenticatedUser<S>, crate::wire::SessionView)> {
557        let read = crate::session::cookie_cache::runtime::authenticated_with(
558            self,
559            req,
560            false,
561            crate::session::cookie_cache::runtime::CacheDecoding::MalformedIsMiss,
562        )
563        .await
564        .map_err(|error| {
565            if error.status_code() == 401 {
566                AuthError::Unauthenticated
567            } else {
568                error
569            }
570        })?
571        .ok_or(AuthError::Unauthenticated)?;
572        Ok((read.user, read.session))
573    }
574
575    /// Authorize once and retain the optional deferred-refresh response field.
576    /// Payload callbacks can observe the same context as nested session middleware
577    /// without issuing another session read or refresh.
578    ///
579    /// # Errors
580    ///
581    /// Returns an authentication error for a missing or invalid session, or propagates storage errors.
582    pub async fn require_session_with_refresh_state(
583        &self,
584        req: &impl crate::session::SessionRequest,
585    ) -> AuthResult<(S::User, crate::wire::SessionView, Option<bool>)> {
586        self.authenticated_session(req, true).await
587    }
588
589    /// Authoritative session authority without reconstructing an application
590    /// model from a cookie. Stateless deployments authorize the authenticated
591    /// cache snapshot; stateful deployments always bypass it.
592    pub async fn require_authoritative_cached_session(
593        &self,
594        req: &AuthRequest,
595    ) -> AuthResult<(crate::AuthenticatedUser<S>, crate::wire::SessionView)> {
596        crate::session::cookie_cache::runtime::clear_established_session::<S>(req);
597        let mut authoritative = req.clone();
598        authoritative.virtual_session = None;
599        if self.config.session.has_server_session_store() {
600            drop(
601                authoritative
602                    .query
603                    .insert("disableCookieCache".into(), "true".into()),
604            );
605        }
606        self.require_cached_session(&authoritative).await
607    }
608
609    /// Authorize against the persisted signed-cookie session.
610    /// This bypasses hook-provided virtual sessions while preserving normal
611    /// refresh, browser preferences and deferred-read behavior.
612    ///
613    /// # Errors
614    ///
615    /// Returns an authentication error if no valid persisted session exists, or propagates storage errors.
616    pub async fn require_authoritative_session(
617        &self,
618        req: &impl crate::session::SessionRequest,
619    ) -> AuthResult<(S::User, crate::wire::SessionView)> {
620        crate::session::cookie_cache::runtime::clear_established_session::<S>(req);
621        let (user, session, _) = self.authenticated_session(req, false).await?;
622        Ok((user, session))
623    }
624
625    /// Resolve physical signed-cookie authority while retaining the actual
626    /// initialized adapter output for downstream public and callback projections.
627    ///
628    /// # Errors
629    /// Propagates authentication, storage and output callback failures.
630    pub async fn require_authoritative_session_record(
631        &self,
632        req: &AuthRequest,
633    ) -> AuthResult<(crate::AdapterRecord<S::User>, crate::wire::SessionView)> {
634        crate::session::cookie_cache::runtime::clear_established_session::<S>(req);
635        let mut physical = req.clone();
636        physical.virtual_session = None;
637        if self.config.session.has_server_session_store() {
638            drop(
639                physical
640                    .query
641                    .insert("disableCookieCache".into(), "true".into()),
642            );
643        }
644        let read = crate::session::cookie_cache::runtime::authenticated(self, &physical, false)
645            .await?
646            .ok_or(AuthError::Unauthenticated)?;
647        match read.user {
648            crate::AuthenticatedUser::Stored(user) => Ok((user, read.session)),
649            crate::AuthenticatedUser::Cached(user) if self.config.session.stateless => {
650                let user = S::user_from_cookie_cache(*user).ok_or_else(|| {
651                    AuthError::config("This schema requires cache-aware session authority")
652                })?;
653                Ok((crate::AdapterRecord::physical(user)?, read.session))
654            }
655            crate::AuthenticatedUser::Cached(_) => Err(AuthError::Unauthenticated),
656        }
657    }
658
659    /// Resolve the actual owner snapshot belonging to a stored session credential.
660    /// Secondary-backed sessions retain their cached user; combined fallback uses SQL.
661    ///
662    /// # Errors
663    /// Propagates backend failures and rejects mismatched cached ownership.
664    pub async fn session_user(
665        &self,
666        session: &impl crate::AuthSession,
667    ) -> AuthResult<Option<S::User>> {
668        use crate::AuthUser;
669        if let Some(user) = self.database.get_session_user(session.token()).await? {
670            if user.id() != session.user_id() {
671                return Err(AuthError::Unauthenticated);
672            }
673            return Ok(Some(user));
674        }
675        if self.config.session.secondary_storage.is_some()
676            && (!self.config.session.store_in_database || self.config.session.preserve_in_database)
677        {
678            return Ok(None);
679        }
680        self.database
681            .get_user_by_id(session.user_id().as_ref())
682            .await
683    }
684
685    pub(in crate::plugin) async fn authenticated_session(
686        &self,
687        req: &impl crate::session::SessionRequest,
688        allow_virtual: bool,
689    ) -> AuthResult<(S::User, crate::wire::SessionView, Option<bool>)> {
690        if allow_virtual && let Some(session) = req.virtual_session(self) {
691            let user = if let Some(user) = req.authenticated_user::<S>(self) {
692                user
693            } else {
694                self.database
695                    .get_user_by_id(&session.user_id)
696                    .await?
697                    .ok_or(AuthError::Unauthenticated)?
698            };
699            return Ok((user, session.clone(), None));
700        }
701        if self.config.session.stateless
702            && let Some(cache) = crate::session::cookie_cache::runtime::read(self, req).await?
703        {
704            crate::session::cookie_cache::runtime::renew_cache(self, req, &cache).await?;
705            let user = S::user_from_cookie_cache(cache.user).ok_or_else(|| {
706                AuthError::config("This schema requires cache-aware session authority")
707            })?;
708            return Ok((user, cache.session, None));
709        }
710        let session_manager = self.session_manager();
711
712        let suppressed = session_manager.request_disables_refresh(req);
713        let skip = req
714            .extensions()
715            .get::<crate::session::SessionRefreshSuppressed>()
716            .is_some();
717        let options = crate::session::SessionReadOptions {
718            allow_refresh: !suppressed && !self.config.session.defer_session_refresh && !skip,
719            cleanup_expired: !self.config.session.defer_session_refresh,
720        };
721        let Some(token) = session_manager.extract_session_token(req) else {
722            return Err(AuthError::Unauthenticated);
723        };
724        let read = session_manager
725            .read_session(&token, options)
726            .await
727            .map_err(|_error| AuthError::Unauthenticated)?;
728        let Some(session) = read.session else {
729            self.queue_session_cleanup(req)?;
730            return Err(AuthError::Unauthenticated);
731        };
732        let user = self
733            .session_user(&session)
734            .await
735            .map_err(|_error| AuthError::Unauthenticated)?;
736        let Some(user) = user else {
737            self.queue_session_cleanup(req)?;
738            return Err(AuthError::Unauthenticated);
739        };
740        if read.refreshed {
741            req.queue_response_header(
742                "Set-Cookie",
743                crate::utils::cookie_utils::create_session_cookie(session.token(), &self.config)?,
744            );
745        }
746        Ok((
747            user,
748            self.session_view(&session),
749            (self.config.session.defer_session_refresh && !suppressed)
750                .then_some(read.needs_refresh && !skip),
751        ))
752    }
753
754    pub(in crate::plugin) fn queue_session_cleanup(
755        &self,
756        req: &impl crate::session::SessionRequest,
757    ) -> AuthResult<()> {
758        for cookie in crate::utils::cookie_utils::delete_session_cookie_headers(&self.config)? {
759            req.queue_response_header("Set-Cookie", cookie);
760        }
761        Ok(())
762    }
763
764    /// Read the request's established session without extending its lifetime.
765    /// Before hooks can establish a virtual session; otherwise a signed cookie
766    /// must identify the persistent session.
767    ///
768    /// # Errors
769    ///
770    /// Propagates errors from session or user lookups.
771    pub async fn session_without_refresh(
772        &self,
773        req: &impl crate::session::SessionRequest,
774    ) -> AuthResult<Option<(S::User, crate::wire::SessionView)>> {
775        if let Some(session) = req.virtual_session(self) {
776            if let Some(user) = req.authenticated_user::<S>(self) {
777                return Ok(Some((user, session)));
778            }
779            return Ok(self
780                .database
781                .get_user_by_id(&session.user_id)
782                .await?
783                .map(|user| (user, session.clone())));
784        }
785        self.persistent_session(req).await
786    }
787
788    /// Inspect a signed-cookie session without treating a missing login as an error.
789    ///
790    /// # Errors
791    ///
792    /// Propagates errors from session or user lookups.
793    pub async fn persistent_session(
794        &self,
795        req: &impl crate::session::SessionRequest,
796    ) -> AuthResult<Option<(S::User, crate::wire::SessionView)>> {
797        if self.config.session.stateless
798            && let Some(cache) = crate::session::cookie_cache::runtime::read(self, req).await?
799        {
800            let user = S::user_from_cookie_cache(cache.user).ok_or_else(|| {
801                AuthError::config("This schema requires cache-aware session authority")
802            })?;
803            return Ok(Some((user, cache.session)));
804        }
805        let Some(token) = self.session_manager().extract_session_token(req) else {
806            return Ok(None);
807        };
808        let Some(session) = self.database.get_session(&token).await? else {
809            return Ok(None);
810        };
811        if !session.active() || session.expires_at() <= chrono::Utc::now() {
812            return Ok(None);
813        }
814        let user = self.session_user(&session).await?;
815        let Some(user) = user else {
816            return Ok(None);
817        };
818        Ok(Some((user, self.session_view(&session))))
819    }
820}
821
822impl<S: AuthSchema> std::fmt::Debug for AuthContext<S> {
823    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
824        f.debug_struct("AuthContext").finish_non_exhaustive()
825    }
826}