1use jerrycan_core::{Error, FromRequest, Headers, RequestCtx, Result};
5use serde::de::DeserializeOwned;
6
7pub 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
24pub 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
41pub 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 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(); 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}