Skip to main content

alibi_plugins/
anonymous.rs

1//! Anonymous accounts and their cleanup when a proven account replaces them.
2
3use super::helpers::{
4    apply_default_role, completed_response_session, delete_session_cookie_headers, get_cookie,
5    issue_user_session_record, record_completed_session_record, response_has_session_cookie,
6};
7use alibi_core::error::codes;
8use alibi_core::wire::{SessionView, UserView};
9use alibi_core::{
10    AuthContext, AuthInitContext, AuthPlugin, AuthRequest, AuthResponse, AuthResult, AuthRoute,
11    AuthSchema, AuthSession, AuthUser, BeforeRequestAction, CreateUser, HttpMethod, RequestMeta,
12};
13use async_trait::async_trait;
14use rand::distr::{Alphanumeric, SampleString};
15use serde_json::json;
16use std::sync::Arc;
17
18/// Application-owned generation of anonymous display names and email addresses.
19#[async_trait]
20pub trait AnonymousIdentity: Send + Sync {
21    async fn email(&self) -> AuthResult<Option<String>> {
22        Ok(None)
23    }
24    async fn name(&self, _request: Option<&AuthRequest>) -> AuthResult<Option<String>> {
25        Ok(None)
26    }
27}
28
29/// Both authenticated accounts involved in an anonymous account upgrade.
30pub struct AnonymousLink {
31    pub anonymous_user: UserView,
32    pub anonymous_session: SessionView,
33    pub new_user: UserView,
34    pub new_session: SessionView,
35}
36
37/// Transfer application-owned data before the anonymous account is deleted.
38#[async_trait]
39pub trait LinkAnonymousAccount: Send + Sync {
40    async fn link(&self, accounts: &AnonymousLink, request: &AuthRequest) -> AuthResult<()>;
41}
42
43#[derive(Clone, Default)]
44pub struct AnonymousConfig {
45    pub email_domain_name: Option<String>,
46    pub identity: Option<Arc<dyn AnonymousIdentity>>,
47    pub on_link_account: Option<Arc<dyn LinkAnonymousAccount>>,
48    pub disable_delete_anonymous_user: bool,
49}
50
51#[derive(Clone, Default)]
52pub struct AnonymousPlugin {
53    config: AnonymousConfig,
54}
55
56/// Authority created for an anonymous identity by the public server operation.
57#[derive(Debug, serde::Deserialize)]
58pub struct AnonymousSignIn {
59    pub token: String,
60    pub user: Box<UserView>,
61}
62
63/// The authenticated anonymous identity and its sessions were deleted.
64#[derive(Debug, serde::Deserialize)]
65pub struct AnonymousDeleted {
66    pub success: bool,
67}
68
69impl AnonymousPlugin {
70    /// Delete only the authoritative caller's anonymous account.
71    #[must_use]
72    pub const fn delete_anonymous_user_endpoint()
73    -> alibi_core::endpoint::ServerEndpoint<AnonymousDeleted> {
74        alibi_core::endpoint::ServerEndpoint::new("anonymous", "deleteAnonymousUser")
75    }
76
77    /// Create anonymous authority through installed server endpoint hooks.
78    #[must_use]
79    pub const fn sign_in_anonymous_endpoint()
80    -> alibi_core::endpoint::ServerEndpoint<AnonymousSignIn> {
81        alibi_core::endpoint::ServerEndpoint::new("anonymous", "signInAnonymous")
82    }
83
84    #[must_use]
85    pub fn new() -> Self {
86        Self::default()
87    }
88    #[must_use]
89    pub const fn with_config(config: AnonymousConfig) -> Self {
90        Self { config }
91    }
92
93    async fn sign_in<S: AuthSchema>(
94        &self,
95        req: &AuthRequest,
96        ctx: &AuthContext<S>,
97    ) -> AuthResult<AuthResponse> {
98        self.sign_in_core(req, ctx).await
99    }
100
101    #[expect(
102        clippy::too_many_lines,
103        reason = "Keep anonymous session reuse, user creation, and session issuance in request order"
104    )]
105    async fn sign_in_core<S: AuthSchema>(
106        &self,
107        req: &impl alibi_core::session::SessionRequest,
108        ctx: &AuthContext<S>,
109    ) -> AuthResult<AuthResponse> {
110        use alibi_core::utils::cookie_utils::{
111            create_session_cookie_with_max_age, create_session_like_cookie, related_cookie_name,
112            sign_cookie_value, verify_cookie_value,
113        };
114
115        if let Some((current, _)) = anonymous_session(req, ctx).await
116            && current.is_anonymous() == Some(true)
117        {
118            return Ok(error(
119                400,
120                codes::ANONYMOUS_USERS_CANNOT_SIGN_IN_AGAIN_ANONYMOUSLY,
121                "Anonymous users cannot sign in again anonymously",
122            ));
123        }
124        let custom = match &self.config.identity {
125            Some(identity) => identity.email().await.map_err(callback_error)?,
126            None => None,
127        };
128        let email = if let Some(email) = custom.filter(|value| !value.is_empty()) {
129            if !super::authentication_helpers::is_valid_email(&email) {
130                return Ok(error(
131                    400,
132                    codes::INVALID_EMAIL_FORMAT,
133                    "Email was not generated in a valid format",
134                ));
135            }
136            email
137        } else {
138            let id = Alphanumeric.sample_string(&mut rand::rng(), 32);
139            self.config
140                .email_domain_name
141                .as_ref()
142                .filter(|domain| !domain.is_empty())
143                .map_or_else(
144                    || format!("{id}@anonymous.placeholder.invalid"),
145                    |domain| format!("temp-{id}@{domain}"),
146                )
147        };
148        let name = match &self.config.identity {
149            Some(identity) => identity
150                .name(req.physical_request())
151                .await
152                .map_err(callback_error)?,
153            None => None,
154        }
155        .filter(|name| !name.is_empty())
156        .unwrap_or_else(|| "Anonymous".to_owned());
157        let mut create = CreateUser::new().with_email(email).with_name(name);
158        create.is_anonymous = Some(true);
159        create.email_verified = Some(false);
160        apply_default_role(ctx, &mut create);
161        let user = ctx
162            .database
163            .create_user_with_source_record(
164                create,
165                alibi_core::user_validation::UserValidationSource::creation("anonymous"),
166            )
167            .await
168            .map_err(|error| match error {
169                alibi_core::AuthError::UserCreationCancelled => alibi_core::AuthError::Upstream {
170                    status: 500,
171                    code: codes::FAILED_TO_CREATE_USER,
172                    message: "Failed to create user",
173                },
174                error @ (alibi_core::AuthError::Api { .. }
175                | alibi_core::AuthError::Upstream { .. }
176                | alibi_core::AuthError::BadRequest(_)
177                | alibi_core::AuthError::InvalidRequest(_)
178                | alibi_core::AuthError::Validation(_)
179                | alibi_core::AuthError::InvalidCredentials
180                | alibi_core::AuthError::Unauthenticated
181                | alibi_core::AuthError::AuthenticationFailed(_)
182                | alibi_core::AuthError::SessionNotFound
183                | alibi_core::AuthError::Forbidden(_)
184                | alibi_core::AuthError::SessionCreationCancelled
185                | alibi_core::AuthError::BannedUser(_)
186                | alibi_core::AuthError::Unauthorized
187                | alibi_core::AuthError::UserNotFound
188                | alibi_core::AuthError::NotFound(_)
189                | alibi_core::AuthError::Conflict(_)
190                | alibi_core::AuthError::MethodNotAllowed(_)
191                | alibi_core::AuthError::PayloadTooLarge(_)
192                | alibi_core::AuthError::UnprocessableEntity(_)
193                | alibi_core::AuthError::RateLimited { .. }
194                | alibi_core::AuthError::NotImplemented(_)
195                | alibi_core::AuthError::Config(_)
196                | alibi_core::AuthError::Database(_)
197                | alibi_core::AuthError::Serialization(_)
198                | alibi_core::AuthError::Plugin { .. }
199                | alibi_core::AuthError::CallbackFailure(_)
200                | alibi_core::AuthError::Internal(_)
201                | alibi_core::AuthError::Encryption(_)
202                | alibi_core::AuthError::PasswordHash(_)
203                | alibi_core::AuthError::Jwt(_)) => error,
204            })?;
205        let headers = req.session_headers();
206        let meta = if let Some(request) = req.physical_request() {
207            RequestMeta::from_request(request)
208        } else {
209            RequestMeta {
210                ip_address: Some(
211                    ctx.config
212                        .advanced
213                        .ip_address
214                        .resolve_ip(headers)
215                        .unwrap_or_default(),
216                ),
217                user_agent: Some(headers.get("user-agent").cloned().unwrap_or_default()),
218            }
219        };
220        let issued =
221            issue_user_session_record(ctx, user.id().as_ref(), meta.ip_address, meta.user_agent)
222                .await
223                .map_err(|cause| match cause.into_auth_error() {
224                    alibi_core::AuthError::SessionCreationCancelled => {
225                        alibi_core::AuthError::Upstream {
226                            status: 400,
227                            code: codes::COULD_NOT_CREATE_SESSION,
228                            message: "Could not create session",
229                        }
230                    }
231                    cause @ (alibi_core::AuthError::Api { .. }
232                    | alibi_core::AuthError::Upstream { .. }
233                    | alibi_core::AuthError::BadRequest(_)
234                    | alibi_core::AuthError::InvalidRequest(_)
235                    | alibi_core::AuthError::Validation(_)
236                    | alibi_core::AuthError::InvalidCredentials
237                    | alibi_core::AuthError::Unauthenticated
238                    | alibi_core::AuthError::AuthenticationFailed(_)
239                    | alibi_core::AuthError::SessionNotFound
240                    | alibi_core::AuthError::Forbidden(_)
241                    | alibi_core::AuthError::UserCreationCancelled
242                    | alibi_core::AuthError::BannedUser(_)
243                    | alibi_core::AuthError::Unauthorized
244                    | alibi_core::AuthError::UserNotFound
245                    | alibi_core::AuthError::NotFound(_)
246                    | alibi_core::AuthError::Conflict(_)
247                    | alibi_core::AuthError::MethodNotAllowed(_)
248                    | alibi_core::AuthError::PayloadTooLarge(_)
249                    | alibi_core::AuthError::UnprocessableEntity(_)
250                    | alibi_core::AuthError::RateLimited { .. }
251                    | alibi_core::AuthError::NotImplemented(_)
252                    | alibi_core::AuthError::Config(_)
253                    | alibi_core::AuthError::Database(_)
254                    | alibi_core::AuthError::Serialization(_)
255                    | alibi_core::AuthError::Plugin { .. }
256                    | alibi_core::AuthError::CallbackFailure(_)
257                    | alibi_core::AuthError::Internal(_)
258                    | alibi_core::AuthError::Encryption(_)
259                    | alibi_core::AuthError::PasswordHash(_)
260                    | alibi_core::AuthError::Jwt(_)) => cause,
261                })?;
262        // The created row is Source's original new-user snapshot, even if a
263        // lifecycle hook subsequently changes the database during session creation.
264        record_completed_session_record::<S>(&user, &issued.session);
265        let mut response = AuthResponse::json(
266            200,
267            &json!({"token":issued.session.token(),"user":ctx.user_view(&user)}),
268        )?;
269
270        let preference_name = related_cookie_name(&ctx.config, "dont_remember");
271        let dont_remember = get_cookie(req, &preference_name)
272            .and_then(|value| verify_cookie_value(&value, ctx.config.current_secret()))
273            .is_some_and(|value| !value.is_empty());
274        response.headers.append(
275            "set-cookie",
276            create_session_cookie_with_max_age(
277                Some(issued.session.token()),
278                if dont_remember {
279                    None
280                } else {
281                    Some(ctx.config.session.expires_in.num_seconds())
282                },
283                &ctx.config,
284            )?,
285        );
286        if dont_remember {
287            response.headers.append(
288                "set-cookie",
289                create_session_like_cookie(
290                    &preference_name,
291                    &sign_cookie_value("true", ctx.config.current_secret()),
292                    None,
293                    &ctx.config,
294                )?,
295            );
296        }
297        Ok(response)
298    }
299
300    async fn delete(
301        &self,
302        req: &AuthRequest,
303        ctx: &AuthContext<impl AuthSchema>,
304    ) -> AuthResult<AuthResponse> {
305        self.delete_core(req, ctx).await
306    }
307
308    async fn delete_core(
309        &self,
310        req: &impl alibi_core::session::SessionRequest,
311        ctx: &AuthContext<impl AuthSchema>,
312    ) -> AuthResult<AuthResponse> {
313        let (user, _) =
314            ctx.require_authoritative_session(req)
315                .await
316                .map_err(|error| match error {
317                    alibi_core::AuthError::Unauthenticated => alibi_core::AuthError::Upstream {
318                        status: 401,
319                        code: codes::UNAUTHORIZED,
320                        message: "Unauthorized",
321                    },
322                    error @ (alibi_core::AuthError::Api { .. }
323                    | alibi_core::AuthError::Upstream { .. }
324                    | alibi_core::AuthError::BadRequest(_)
325                    | alibi_core::AuthError::InvalidRequest(_)
326                    | alibi_core::AuthError::Validation(_)
327                    | alibi_core::AuthError::InvalidCredentials
328                    | alibi_core::AuthError::AuthenticationFailed(_)
329                    | alibi_core::AuthError::SessionNotFound
330                    | alibi_core::AuthError::Forbidden(_)
331                    | alibi_core::AuthError::SessionCreationCancelled
332                    | alibi_core::AuthError::UserCreationCancelled
333                    | alibi_core::AuthError::BannedUser(_)
334                    | alibi_core::AuthError::Unauthorized
335                    | alibi_core::AuthError::UserNotFound
336                    | alibi_core::AuthError::NotFound(_)
337                    | alibi_core::AuthError::Conflict(_)
338                    | alibi_core::AuthError::MethodNotAllowed(_)
339                    | alibi_core::AuthError::PayloadTooLarge(_)
340                    | alibi_core::AuthError::UnprocessableEntity(_)
341                    | alibi_core::AuthError::RateLimited { .. }
342                    | alibi_core::AuthError::NotImplemented(_)
343                    | alibi_core::AuthError::Config(_)
344                    | alibi_core::AuthError::Database(_)
345                    | alibi_core::AuthError::Serialization(_)
346                    | alibi_core::AuthError::Plugin { .. }
347                    | alibi_core::AuthError::CallbackFailure(_)
348                    | alibi_core::AuthError::Internal(_)
349                    | alibi_core::AuthError::Encryption(_)
350                    | alibi_core::AuthError::PasswordHash(_)
351                    | alibi_core::AuthError::Jwt(_)) => error,
352                })?;
353        if self.config.disable_delete_anonymous_user {
354            return Ok(error(
355                400,
356                codes::DELETE_ANONYMOUS_USER_DISABLED,
357                "Deleting anonymous users is disabled",
358            ));
359        }
360        if user.is_anonymous() != Some(true) {
361            return Ok(error(
362                403,
363                codes::USER_IS_NOT_ANONYMOUS,
364                "User is not anonymous",
365            ));
366        }
367        if let Err(cause) = ctx.database.delete_user_sessions(user.id().as_ref()).await {
368            tracing::error!(%cause, "Failed to delete anonymous user sessions");
369            return Ok(error(
370                500,
371                codes::FAILED_TO_DELETE_ANONYMOUS_USER_SESSIONS,
372                "Failed to delete anonymous user sessions",
373            ));
374        }
375        if let Err(cause) = ctx.database.delete_user(user.id().as_ref()).await {
376            tracing::error!(%cause, "Failed to delete anonymous user");
377            return Ok(error(
378                500,
379                codes::FAILED_TO_DELETE_ANONYMOUS_USER,
380                "Failed to delete anonymous user",
381            ));
382        }
383        let mut response = AuthResponse::json(200, &json!({"success":true}))?;
384        for cookie in delete_session_cookie_headers(&ctx.config)? {
385            response.headers.append("set-cookie", cookie);
386        }
387        Ok(response)
388    }
389}
390
391#[async_trait]
392impl<S: AuthSchema> AuthPlugin<S> for AnonymousPlugin {
393    route_openapi_metadata!(S);
394
395    fn server_endpoints(&self) -> Vec<alibi_core::endpoint::EndpointDefinition> {
396        vec![
397            crate::endpoint::definition(
398                "signInAnonymous",
399                "signInAnonymous",
400                Some("/sign-in/anonymous"),
401                HttpMethod::Post,
402            ),
403            crate::endpoint::definition(
404                "deleteAnonymousUser",
405                "deleteAnonymousUser",
406                Some("/delete-anonymous-user"),
407                HttpMethod::Post,
408            ),
409        ]
410    }
411    async fn on_endpoint(
412        &self,
413        call: &alibi_core::endpoint::EndpointCall,
414        ctx: &AuthContext<S>,
415    ) -> AuthResult<alibi_core::endpoint::EndpointResponse> {
416        let output = if call.operation_id() == "deleteAnonymousUser" {
417            self.delete_core(call, ctx).await?
418        } else {
419            self.sign_in_core(call, ctx).await?
420        };
421        let mut response = if output.status >= 400 {
422            alibi_core::endpoint::EndpointResponse::error(crate::endpoint::error_response(
423                output.clone(),
424            ))
425        } else {
426            alibi_core::endpoint::EndpointResponse::value(alibi_core::utils::json::from_slice(
427                &output.body,
428            )?)
429        };
430        response.set_status(Some(output.status));
431        response.merge_headers(output.headers);
432        Ok(response)
433    }
434    fn name(&self) -> &'static str {
435        "anonymous"
436    }
437    fn routes(&self) -> Vec<AuthRoute> {
438        vec![
439            AuthRoute::post("/sign-in/anonymous", "sign_in_anonymous"),
440            AuthRoute::post("/delete-anonymous-user", "delete_anonymous_user"),
441        ]
442    }
443    async fn on_init(&self, ctx: &mut AuthInitContext<S>) -> AuthResult<()> {
444        ctx.set_metadata("anonymous.enabled", json!(true));
445        let default = |mut input: CreateUser| {
446            if input.is_anonymous.is_none() {
447                input.is_anonymous = Some(false);
448            }
449            Ok(input)
450        };
451        if ctx.config.user_validation.is_some() {
452            ctx.register_user_creation_adapter_default(default);
453        } else {
454            ctx.register_user_create_transform(default);
455        }
456        Ok(())
457    }
458    async fn on_request(
459        &self,
460        req: &AuthRequest,
461        ctx: &AuthContext<S>,
462    ) -> AuthResult<Option<AuthResponse>> {
463        match (req.method(), req.path()) {
464            (HttpMethod::Post, "/sign-in/anonymous") => Ok(Some(self.sign_in(req, ctx).await?)),
465            (HttpMethod::Post, "/delete-anonymous-user") => Ok(Some(self.delete(req, ctx).await?)),
466            _ => Ok(None),
467        }
468    }
469    async fn before_request(
470        &self,
471        req: &AuthRequest,
472        ctx: &AuthContext<S>,
473    ) -> AuthResult<Option<BeforeRequestAction>> {
474        if req.path() == "/sign-in/social"
475            && let Some((user, _)) = anonymous_session(req, ctx).await
476            && user.is_anonymous() == Some(true)
477        {
478            req.extensions()
479                .insert(super::oauth::CapturedOAuthServerContext(
480                    super::oauth::OAuthServerContext {
481                        anonymous_user_id: user.id().into_owned(),
482                    },
483                ));
484        }
485        Ok(None)
486    }
487    async fn after_request(
488        &self,
489        req: &AuthRequest,
490        ctx: &AuthContext<S>,
491        response: AuthResponse,
492    ) -> AuthResult<AuthResponse> {
493        let matches = [
494            "/sign-in",
495            "/sign-up",
496            "/callback",
497            "/magic-link/verify",
498            "/email-otp/verify-email",
499            "/one-tap/callback",
500            "/passkey/verify-authentication",
501            "/phone-number/verify",
502            "/verify-email",
503        ]
504        .iter()
505        .any(|prefix| req.path().starts_with(prefix));
506        if !matches {
507            return Ok(response);
508        }
509        if !response_has_session_cookie(ctx, &response) {
510            return Ok(response);
511        }
512        let Some((old_user, old_session)) = resolve_anonymous_session(req, ctx).await? else {
513            return Ok(response);
514        };
515        if old_user.is_anonymous != Some(true) {
516            return Ok(response);
517        }
518        let Some(issued) = completed_response_session(req, ctx, &response) else {
519            return Ok(response);
520        };
521        if let Some(linker) = &self.config.on_link_account {
522            // The new-user callback receives the real newly issued session,
523            // including adapter fields hidden from public session responses.
524            let mut new_session = issued.callback_session(ctx);
525            new_session.omitted_fields.clear();
526            linker
527                .link(
528                    &AnonymousLink {
529                        anonymous_user: old_user.clone(),
530                        anonymous_session: ctx.session_view(&old_session),
531                        new_user: issued.callback_user(ctx),
532                        new_session,
533                    },
534                    req,
535                )
536                .await
537                .map_err(callback_error)?;
538        }
539        if self.config.disable_delete_anonymous_user
540            || old_user.id == issued.user.id().as_ref()
541            || issued.user.is_anonymous() == Some(true)
542        {
543            return Ok(response);
544        }
545        if let Err(error) = ctx.database.delete_user(&old_user.id).await {
546            tracing::error!(user_id=%old_user.id,error=%error,"Failed to clean up anonymous account");
547        }
548        Ok(response)
549    }
550}
551
552impl std::fmt::Debug for AnonymousConfig {
553    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
554        f.debug_struct("AnonymousConfig").finish_non_exhaustive()
555    }
556}
557
558impl std::fmt::Debug for AnonymousLink {
559    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
560        f.debug_struct("AnonymousLink").finish_non_exhaustive()
561    }
562}
563
564impl std::fmt::Debug for AnonymousPlugin {
565    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
566        f.debug_struct("AnonymousPlugin").finish_non_exhaustive()
567    }
568}
569
570// Source's nested get-session disables refresh, catches failed resolution, and
571// retains its real expiry cleanup/response-cookie effects on the request.
572async fn anonymous_session<S: AuthSchema>(
573    req: &impl alibi_core::session::SessionRequest,
574    ctx: &AuthContext<S>,
575) -> Option<(alibi_core::AuthenticatedUser<S>, SessionView)> {
576    if let Some(call) = req.endpoint_call() {
577        let mut read = call.clone();
578        let mut query = call
579            .query()
580            .and_then(alibi_core::utils::json::JsValue::as_object)
581            .cloned()
582            .unwrap_or_default();
583        _ = query.insert(
584            "disableRefresh".into(),
585            alibi_core::utils::json::JsValue::Bool(true),
586        );
587        alibi_core::endpoint::EndpointContextPatch {
588            query: Some(alibi_core::utils::json::JsValue::Object(query)),
589            ..Default::default()
590        }
591        .apply(&mut read);
592        return ctx.require_cached_session(&read).await.ok();
593    }
594    let mut read = req.physical_request()?.clone();
595    _ = read.query.insert("disableRefresh".into(), "true".into());
596    ctx.require_cached_session(&read).await.ok()
597}
598
599async fn resolve_anonymous_session<S: AuthSchema>(
600    req: &AuthRequest,
601    ctx: &AuthContext<S>,
602) -> AuthResult<Option<(UserView, SessionView)>> {
603    if let Some((user, session)) = anonymous_session(req, ctx).await
604        && user.is_anonymous() == Some(true)
605    {
606        let user = match user {
607            alibi_core::AuthenticatedUser::Stored(user) => ctx.user_view(&user),
608            alibi_core::AuthenticatedUser::Cached(user) => *user,
609        };
610        return Ok(Some((user, session)));
611    }
612    let Some(context) = req
613        .extensions()
614        .get::<super::oauth::RecoveredOAuthServerContext>()
615    else {
616        return Ok(None);
617    };
618    let Some(user) = ctx
619        .database
620        .get_user_by_id_record(&context.0.anonymous_user_id)
621        .await?
622    else {
623        return Ok(None);
624    };
625    if user.is_anonymous() != Some(true) {
626        return Ok(None);
627    }
628    let session = ctx
629        .database
630        .get_user_sessions(&context.0.anonymous_user_id)
631        .await?
632        .into_iter()
633        .find(|session| session.expires_at() > chrono::Utc::now());
634    Ok(session.map(|session| (ctx.trusted_user_view(&user), ctx.session_view(&session))))
635}
636
637// An ordinary application exception has an empty 500 wire. Explicit typed
638// HTTP errors retain their status/body, including coded and uncoded API errors.
639fn callback_error(error: alibi_core::AuthError) -> alibi_core::AuthError {
640    match error {
641        alibi_core::AuthError::Internal(_) => {
642            alibi_core::AuthError::CallbackFailure(Box::new(error))
643        }
644        other => other,
645    }
646}
647
648fn error(status: u16, code: &'static str, message: &'static str) -> AuthResponse {
649    alibi_core::AuthError::Upstream {
650        status,
651        code,
652        message,
653    }
654    .to_auth_response()
655}