Skip to main content

alibi_core/plugin/
context.rs

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