Skip to main content

miryad_core/auth/
middleware.rs

1use axum::extract::{FromRef, FromRequestParts};
2use axum::http::request::Parts;
3
4use crate::auth::cookie::extract_session;
5use crate::auth::error::AuthError;
6use crate::auth::state::MiryadAuthState;
7
8/// Identité de la requête courante, extraite du cookie de session — pas d'évaluation RBAC ici,
9/// juste "qui fait la requête" (cf. feature 3 pour le "a le droit de quoi").
10pub struct AuthUser {
11    pub subject: String,
12    pub email: Option<String>,
13    pub id_token: String,
14}
15
16impl<S> FromRequestParts<S> for AuthUser
17where
18    S: Send + Sync,
19    MiryadAuthState: FromRef<S>,
20{
21    type Rejection = AuthError;
22
23    async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
24        let auth_state = MiryadAuthState::from_ref(state);
25
26        let cookie_header = parts
27            .headers
28            .get("Cookie")
29            .and_then(|v| v.to_str().ok())
30            .map(std::string::ToString::to_string);
31
32        let identity = extract_session(cookie_header.as_deref(), &auth_state.cookie_key)
33            .inspect_err(|e| tracing::debug!("auth rejected: {}", e))?;
34
35        tracing::debug!(subject = %identity.subject, "auth ok");
36        Ok(Self {
37            subject: identity.subject,
38            email: identity.email,
39            id_token: identity.id_token,
40        })
41    }
42}
43
44#[cfg(test)]
45mod tests {
46    use super::*;
47    use crate::auth::cookie::build_set_cookie;
48    use crate::auth::oidc::{MockOidcClient, OidcIdentity};
49    use ::cookie::Key;
50    use axum::{
51        Router,
52        body::Body,
53        http::{Request, StatusCode},
54        routing::get,
55    };
56    use tower::ServiceExt;
57
58    fn mock_db() -> sea_orm::DatabaseConnection {
59        sea_orm::MockDatabase::new(sea_orm::DatabaseBackend::Sqlite).into_connection()
60    }
61
62    fn test_state() -> MiryadAuthState {
63        MiryadAuthState {
64            oidc_client: std::sync::Arc::new(MockOidcClient),
65            cookie_key: Key::from(&[0u8; 64]),
66            post_login_redirect: "/".to_string(),
67            post_logout_redirect: "/".to_string(),
68            db: mock_db(),
69        }
70    }
71
72    async fn protected_handler(user: AuthUser) -> String {
73        user.subject
74    }
75
76    fn make_app() -> Router {
77        Router::new()
78            .route("/protected", get(protected_handler))
79            .with_state(test_state())
80    }
81
82    #[tokio::test]
83    async fn protected_without_cookie_returns_401() {
84        let app = make_app();
85        let req = Request::builder()
86            .uri("/protected")
87            .body(Body::empty())
88            .expect("valid request");
89        let resp = app.oneshot(req).await.expect("router does not fail");
90        assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
91    }
92
93    #[tokio::test]
94    async fn protected_with_valid_session_passes_and_exposes_subject() {
95        let key = Key::from(&[0u8; 64]);
96        let exp = std::time::SystemTime::now()
97            .duration_since(std::time::UNIX_EPOCH)
98            .expect("system clock is after epoch")
99            .as_secs()
100            + 3600;
101        let jwt = format!("header.{}.sig", {
102            use base64::Engine;
103            base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(format!(r#"{{"exp":{exp}}}"#))
104        });
105        let identity = OidcIdentity {
106            id_token: jwt,
107            subject: "user-123".to_string(),
108            email: Some("test@example.com".to_string()),
109        };
110        let set_cookie = build_set_cookie(&identity, &key);
111        let cookie_value = set_cookie
112            .split(';')
113            .next()
114            .expect("cookie pair present")
115            .to_string();
116
117        let app = Router::new()
118            .route("/protected", get(protected_handler))
119            .with_state(MiryadAuthState {
120                oidc_client: std::sync::Arc::new(MockOidcClient),
121                cookie_key: key,
122                post_login_redirect: "/".to_string(),
123                post_logout_redirect: "/".to_string(),
124                db: mock_db(),
125            });
126        let req = Request::builder()
127            .uri("/protected")
128            .header("Cookie", cookie_value)
129            .body(Body::empty())
130            .expect("valid request");
131        let resp = app.oneshot(req).await.expect("router does not fail");
132        assert_eq!(resp.status(), StatusCode::OK);
133        let body = axum::body::to_bytes(resp.into_body(), usize::MAX)
134            .await
135            .expect("readable body");
136        assert_eq!(&body[..], b"user-123");
137    }
138}