Skip to main content

libfw_server/
auth.rs

1//! Bearer-token extraction and authorization rejections.
2
3use std::sync::Arc;
4
5use axum::extract::{FromRef, FromRequestParts};
6use axum::http::request::Parts;
7use axum::http::{header, StatusCode};
8use axum::response::{IntoResponse, Response};
9use axum::Json;
10use libfw_core::auth::AuthError;
11use libfw_core::claims::TokenClaims;
12
13use crate::ServerState;
14
15/// A request rejection caused by missing/invalid credentials.
16#[derive(Debug, thiserror::Error)]
17pub enum AuthRejection {
18    /// `401 Unauthorized` — missing or unverifiable token.
19    #[error("{0}")]
20    Unauthorized(String),
21    /// `403 Forbidden` — valid token but insufficient rights.
22    #[error("permission denied: {action} on `{path}`")]
23    Forbidden { path: String, action: String },
24}
25
26impl From<AuthError> for AuthRejection {
27    fn from(err: AuthError) -> Self {
28        match err {
29            AuthError::MissingToken => AuthRejection::Unauthorized("missing bearer token".into()),
30            AuthError::Invalid(msg) => AuthRejection::Unauthorized(format!("invalid token: {msg}")),
31            AuthError::Expired => AuthRejection::Unauthorized("token expired".into()),
32            AuthError::Forbidden { path, action } => {
33                AuthRejection::Forbidden { path, action: action.to_string() }
34            }
35        }
36    }
37}
38
39impl IntoResponse for AuthRejection {
40    fn into_response(self) -> Response {
41        let (status, message) = match self {
42            AuthRejection::Unauthorized(msg) => (StatusCode::UNAUTHORIZED, msg),
43            AuthRejection::Forbidden { path, action } => (
44                StatusCode::FORBIDDEN,
45                format!("permission denied: {action} on `{path}`"),
46            ),
47        };
48        (status, Json(serde_json::json!({ "error": message }))).into_response()
49    }
50}
51
52/// Extracts and verifies the `Authorization: Bearer <token>` header.
53///
54/// Verification is delegated to [`ServerState::verifier`]; path-level
55/// authorization happens in the handler via
56/// [`ServerState::authorize`].
57pub struct BearerClaims(pub TokenClaims);
58
59impl<S> FromRequestParts<S> for BearerClaims
60where
61    S: Send + Sync,
62    Arc<ServerState>: axum::extract::FromRef<S>,
63{
64    type Rejection = AuthRejection;
65
66    async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
67        let state = Arc::<ServerState>::from_ref(state);
68        let header_value = parts
69            .headers
70            .get(header::AUTHORIZATION)
71            .and_then(|v| v.to_str().ok())
72            .ok_or_else(|| AuthRejection::Unauthorized("missing bearer token".into()))?;
73        let token = header_value
74            .strip_prefix("Bearer ")
75            .ok_or_else(|| AuthRejection::Unauthorized("malformed authorization header".into()))?
76            .trim();
77        let claims = state
78            .verifier
79            .verify(token)
80            .map_err(AuthRejection::from)?;
81        Ok(BearerClaims(claims))
82    }
83}