Skip to main content

jerrycan_auth/
guard.rs

1//! Guards are dependencies (spec §4.3): `Session<T>`/`Bearer<T>` are extractors
2//! returning 401; `require_role` returns 403. No auth middleware.
3
4use jerrycan_core::{Error, FromRequest, Headers, RequestCtx, Result};
5use serde::de::DeserializeOwned;
6
7/// Session extractor: decrypts the `jerrycan_session` cookie into `T`.
8/// Absent/invalid cookie → 401. Requires the `Auth` extension to be registered.
9pub struct Session<T>(pub T);
10
11impl<T: DeserializeOwned + Send> FromRequest for Session<T> {
12    async fn from_request(ctx: &mut RequestCtx) -> Result<Self> {
13        let auth = ctx.resolve::<crate::Auth>().await?;
14        let headers = Headers::from_request(ctx).await?;
15        let cookie_header = headers.get("cookie").ok_or_else(Error::unauthorized)?;
16        let token = auth
17            .sessions()
18            .read_cookie(cookie_header)
19            .ok_or_else(Error::unauthorized)?;
20        auth.sessions().decode::<T>(&token).map(Session)
21    }
22}
23
24/// Bearer JWT extractor: verifies the `Authorization: Bearer <jwt>` token into `T`.
25pub struct Bearer<T>(pub T);
26
27impl<T: DeserializeOwned + Send> FromRequest for Bearer<T> {
28    async fn from_request(ctx: &mut RequestCtx) -> Result<Self> {
29        let auth = ctx.resolve::<crate::Auth>().await?;
30        let headers = Headers::from_request(ctx).await?;
31        let value = headers
32            .get("authorization")
33            .ok_or_else(Error::unauthorized)?;
34        let token = value
35            .strip_prefix("Bearer ")
36            .ok_or_else(Error::unauthorized)?;
37        crate::jwt::decode::<T>(token, auth.jwt_key()).map(Bearer)
38    }
39}
40
41/// Role check helper for generated guards: `403` when the role doesn't match.
42pub fn require_role(actual: &str, required: &str) -> Result<()> {
43    if actual == required {
44        Ok(())
45    } else {
46        Err(Error::forbidden())
47    }
48}
49
50#[cfg(test)]
51mod tests {
52    use super::*;
53    use crate::Auth;
54    use jerrycan_core::{App, Dep, Json, get, post};
55    use serde::{Deserialize, Serialize};
56
57    #[derive(Serialize, Deserialize, Clone)]
58    struct User {
59        id: i64,
60        role: String,
61    }
62
63    async fn login(auth: Dep<Auth>) -> Result<jerrycan_core::Response> {
64        // Issue a session cookie for a fixed user (test login).
65        let cookie = auth.sessions().set_cookie(&User {
66            id: 1,
67            role: "admin".into(),
68        })?;
69        let mut res = jerrycan_core::IntoResponse::into_response("ok");
70        res.headers_mut().insert(
71            jerrycan_core::http::header::SET_COOKIE,
72            jerrycan_core::http::HeaderValue::from_str(&cookie).unwrap(),
73        );
74        Ok(res)
75    }
76
77    async fn whoami(Session(user): Session<User>) -> Json<i64> {
78        Json(user.id)
79    }
80
81    fn app() -> App {
82        App::new()
83            .extend(Auth::with_secret("a-very-long-development-secret-string!!"))
84            .route("/login", post(login))
85            .route("/me", get(whoami))
86    }
87
88    #[tokio::test]
89    async fn no_cookie_is_401() {
90        let t = app().into_test();
91        assert_eq!(
92            t.get("/me").await.status(),
93            jerrycan_core::http::StatusCode::UNAUTHORIZED
94        );
95    }
96
97    #[tokio::test]
98    async fn login_then_authenticated_request_succeeds() {
99        let t = app().into_test();
100        let login = t.post_json("/login", &()).await;
101        let set_cookie = login.headers()["set-cookie"].to_str().unwrap().to_string();
102        let cookie = set_cookie.split(';').next().unwrap().to_string(); // jerrycan_session=...
103        let res = t.get_with("/me", &[("cookie", &cookie)]).await;
104        assert_eq!(res.status(), jerrycan_core::http::StatusCode::OK);
105        assert_eq!(res.json::<i64>(), 1);
106    }
107
108    #[tokio::test]
109    async fn require_role_rejects_wrong_role_with_403() {
110        async fn admin_only(Session(user): Session<User>) -> Result<&'static str> {
111            require_role(&user.role, "superadmin")?;
112            Ok("secret")
113        }
114        let t = App::new()
115            .extend(Auth::with_secret("a-very-long-development-secret-string!!"))
116            .route("/login", post(login))
117            .route("/admin", get(admin_only))
118            .into_test();
119        let login = t.post_json("/login", &()).await;
120        let cookie = login.headers()["set-cookie"]
121            .to_str()
122            .unwrap()
123            .split(';')
124            .next()
125            .unwrap()
126            .to_string();
127        let res = t.get_with("/admin", &[("cookie", &cookie)]).await;
128        assert_eq!(res.status(), jerrycan_core::http::StatusCode::FORBIDDEN);
129    }
130}