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 | AuthError::Validation(_) => StatusCode::UNPROCESSABLE_ENTITY,
34            AuthError::NotConfirmed | AuthError::AccountLocked => StatusCode::FORBIDDEN,
35            _ => StatusCode::INTERNAL_SERVER_ERROR,
36        };
37        (status, self.to_string()).into_response()
38    }
39}
40
41impl<S, U> FromRequestParts<S> for CurrentUser<U>
42where
43    S: Send + Sync,
44    U: AuthUser,
45{
46    type Rejection = AuthError;
47
48    fn from_request_parts(
49        parts: &mut Parts,
50        _state: &S,
51    ) -> impl Future<Output = Result<Self, Self::Rejection>> + Send {
52        let identity_result = require_identity(parts);
53        let db = global().db.clone();
54        async move {
55            let identity = identity_result?;
56            let id: U::Id = serde_json::from_value(identity.user_id)
57                .map_err(|e| AuthError::Internal(e.to_string()))?;
58            U::find_by_id(&db, id)
59                .await
60                .map_err(|e| AuthError::Internal(e.to_string()))?
61                .ok_or(AuthError::Unauthorized)
62                .map(CurrentUser)
63        }
64    }
65}
66
67impl<S, U> FromRequestParts<S> for MaybeUser<U>
68where
69    S: Send + Sync,
70    U: AuthUser,
71{
72    type Rejection = std::convert::Infallible;
73
74    fn from_request_parts(
75        parts: &mut Parts,
76        _state: &S,
77    ) -> impl Future<Output = Result<Self, Self::Rejection>> + Send {
78        let identity = identity_from_parts(parts);
79        let db = global().db.clone();
80        async move {
81            let user = match identity {
82                Some(identity) => {
83                    let id: U::Id = match serde_json::from_value(identity.user_id) {
84                        Ok(id) => id,
85                        Err(_) => return Ok(MaybeUser(None)),
86                    };
87                    U::find_by_id(&db, id).await.ok().flatten()
88                }
89                None => None,
90            };
91            Ok(MaybeUser(user))
92        }
93    }
94}
95
96impl<S> FromRequestParts<S> for RequireAuth
97where
98    S: Send + Sync,
99{
100    type Rejection = AuthError;
101
102    fn from_request_parts(
103        parts: &mut Parts,
104        _state: &S,
105    ) -> impl Future<Output = Result<Self, Self::Rejection>> + Send {
106        ready(require_identity(parts).map(RequireAuth))
107    }
108}
109
110impl<S> FromRequestParts<S> for AuthToken
111where
112    S: Send + Sync,
113{
114    type Rejection = AuthError;
115
116    fn from_request_parts(
117        parts: &mut Parts,
118        _state: &S,
119    ) -> impl Future<Output = Result<Self, Self::Rejection>> + Send {
120        let token = parts
121            .headers
122            .get(header::AUTHORIZATION)
123            .and_then(|v| v.to_str().ok())
124            .ok_or(AuthError::InvalidToken)
125            .and_then(|value| {
126                value
127                    .strip_prefix("Bearer ")
128                    .map(str::trim)
129                    .filter(|t| !t.is_empty())
130                    .map(str::to_string)
131                    .ok_or(AuthError::InvalidToken)
132            });
133        ready(token.map(AuthToken))
134    }
135}
136
137// Re-export axum for generated apps (mirrors doido-controller pattern).
138pub use axum;
139
140// Silence unused import when compiling with minimal features.
141type _ReadyCheck = Ready<Result<(), AuthError>>;