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