Skip to main content

doido_auth/
extractors.rs

1//! Axum extractors for authenticated requests.
2
3use crate::error::AuthError;
4use crate::identity::AuthIdentity;
5use crate::state::global;
6use crate::strategy::{identity_from_parts, require_identity};
7use crate::user::AuthUser;
8use axum::extract::FromRequestParts;
9use axum::http::request::Parts;
10use axum::http::StatusCode;
11use axum::response::{IntoResponse, Response};
12use http::header;
13use std::future::{ready, Future, Ready};
14
15/// Requires an authenticated user loaded from the database.
16pub struct CurrentUser<U>(pub U);
17
18/// Optional authenticated user — never fails the request.
19pub struct MaybeUser<U>(pub Option<U>);
20
21/// Ensures some identity is present without loading the full user model.
22pub struct RequireAuth(pub AuthIdentity);
23
24/// Raw bearer token string from the `Authorization` header.
25pub struct AuthToken(pub String);
26
27impl IntoResponse for AuthError {
28    fn into_response(self) -> Response {
29        let status = match self {
30            AuthError::Unauthorized | AuthError::InvalidCredentials | AuthError::InvalidToken => {
31                StatusCode::UNAUTHORIZED
32            }
33            AuthError::EmailTaken => StatusCode::UNPROCESSABLE_ENTITY,
34            _ => StatusCode::INTERNAL_SERVER_ERROR,
35        };
36        (status, self.to_string()).into_response()
37    }
38}
39
40impl<S, U> FromRequestParts<S> for CurrentUser<U>
41where
42    S: Send + Sync,
43    U: AuthUser,
44{
45    type Rejection = AuthError;
46
47    fn from_request_parts(
48        parts: &mut Parts,
49        _state: &S,
50    ) -> impl Future<Output = Result<Self, Self::Rejection>> + Send {
51        let identity_result = require_identity(parts);
52        let db = global().db.clone();
53        async move {
54            let identity = identity_result?;
55            let id: U::Id = serde_json::from_value(identity.user_id)
56                .map_err(|e| AuthError::Internal(e.to_string()))?;
57            U::find_by_id(&db, id)
58                .await
59                .map_err(|e| AuthError::Internal(e.to_string()))?
60                .ok_or(AuthError::Unauthorized)
61                .map(CurrentUser)
62        }
63    }
64}
65
66impl<S, U> FromRequestParts<S> for MaybeUser<U>
67where
68    S: Send + Sync,
69    U: AuthUser,
70{
71    type Rejection = std::convert::Infallible;
72
73    fn from_request_parts(
74        parts: &mut Parts,
75        _state: &S,
76    ) -> impl Future<Output = Result<Self, Self::Rejection>> + Send {
77        let identity = identity_from_parts(parts);
78        let db = global().db.clone();
79        async move {
80            let user = match identity {
81                Some(identity) => {
82                    let id: U::Id = match serde_json::from_value(identity.user_id) {
83                        Ok(id) => id,
84                        Err(_) => return Ok(MaybeUser(None)),
85                    };
86                    U::find_by_id(&db, id).await.ok().flatten()
87                }
88                None => None,
89            };
90            Ok(MaybeUser(user))
91        }
92    }
93}
94
95impl<S> FromRequestParts<S> for RequireAuth
96where
97    S: Send + Sync,
98{
99    type Rejection = AuthError;
100
101    fn from_request_parts(
102        parts: &mut Parts,
103        _state: &S,
104    ) -> impl Future<Output = Result<Self, Self::Rejection>> + Send {
105        ready(require_identity(parts).map(RequireAuth))
106    }
107}
108
109impl<S> FromRequestParts<S> for AuthToken
110where
111    S: Send + Sync,
112{
113    type Rejection = AuthError;
114
115    fn from_request_parts(
116        parts: &mut Parts,
117        _state: &S,
118    ) -> impl Future<Output = Result<Self, Self::Rejection>> + Send {
119        let token = parts
120            .headers
121            .get(header::AUTHORIZATION)
122            .and_then(|v| v.to_str().ok())
123            .ok_or(AuthError::InvalidToken)
124            .and_then(|value| {
125                value
126                    .strip_prefix("Bearer ")
127                    .map(str::trim)
128                    .filter(|t| !t.is_empty())
129                    .map(str::to_string)
130                    .ok_or(AuthError::InvalidToken)
131            });
132        ready(token.map(AuthToken))
133    }
134}
135
136// Re-export axum for generated apps (mirrors doido-controller pattern).
137pub use axum;
138
139// Silence unused import when compiling with minimal features.
140type _ReadyCheck = Ready<Result<(), AuthError>>;