1mod builder;
30mod service;
31mod session;
32
33use std::{borrow::Cow, convert::Infallible, marker::PhantomData, sync::Arc};
34
35use axum::extract::{FromRef, FromRequestParts};
36pub use builder::{JwtBuilderError, JwtContextBuilder};
37#[cfg(feature = "cookie")]
38use cookie_monster::{Cookie, CookieBuilder};
39use http::{HeaderMap, HeaderName, request::Parts};
40use jsonwebtoken::{TokenData, decode, encode};
41use serde::{Serialize, de::DeserializeOwned};
42pub use session::Jwt;
43
44pub use jsonwebtoken::{
45 DecodingKey, EncodingKey, Header, Validation,
46 errors::{Error as JwtError, ErrorKind as JwtErrorKind},
47 get_current_timestamp,
48};
49
50pub struct JwtContext<T>(Arc<JwtContextInner<T>>);
55
56struct JwtContextInner<T> {
57 encoding_key: EncodingKey,
58 decoding_key: DecodingKey,
59 jwt_header: Header,
60 validation: Validation,
61 data: PhantomData<T>,
62 extract: ExtractFrom,
63}
64
65pub(crate) enum ExtractFrom {
66 #[cfg(feature = "cookie")]
67 Cookie(Box<CookieBuilder>),
68 Header {
69 header: HeaderName,
70 prefix: Cow<'static, str>,
71 },
72}
73impl JwtContext<()> {
74 pub fn builder() -> JwtContextBuilder {
76 JwtContextBuilder::new()
77 }
78}
79
80impl<T: Serialize> JwtContext<T> {
81 pub fn encode_token(&self, data: &T) -> jsonwebtoken::errors::Result<String> {
83 encode(&self.0.jwt_header, data, &self.0.encoding_key)
84 }
85
86 #[cfg(feature = "cookie")]
87 pub fn encode_token_to_cookie(&self, data: &T) -> jsonwebtoken::errors::Result<Cookie> {
90 let token = encode(&self.0.jwt_header, data, &self.0.encoding_key)?;
91 match &self.0.extract {
92 ExtractFrom::Cookie(cookie_builder) => Ok(cookie_builder.clone().value(token).build()),
93 ExtractFrom::Header { .. } => Err(jsonwebtoken::errors::ErrorKind::InvalidToken.into()),
94 }
95 }
96
97 #[cfg(feature = "cookie")]
98 pub fn logout_cookie(&self) -> Cookie {
100 match &self.0.extract {
101 ExtractFrom::Cookie(cookie_builder) => {
102 use cookie_monster::Expires;
103
104 cookie_builder
105 .clone()
106 .expires(Expires::remove())
107 .max_age_secs(0)
108 .value("")
109 .build()
110 }
111 ExtractFrom::Header { .. } => panic!("no cookie config set"),
112 }
113 }
114}
115
116impl<T: DeserializeOwned> JwtContext<T> {
117 pub fn decode(&self, jwt: impl AsRef<[u8]>) -> Result<TokenData<T>, JwtError> {
119 decode(jwt.as_ref(), &self.0.decoding_key, &self.0.validation)
120 }
121
122 pub(crate) fn decode_from_headers(&self, headers: &HeaderMap) -> Option<T> {
123 let result = match &self.0.extract {
124 #[cfg(feature = "cookie")]
125 ExtractFrom::Cookie(cookie) => {
126 let jar = cookie_monster::CookieJar::from_headers(headers);
127 let cookie = jar.get(cookie.get_name())?;
128
129 self.decode(cookie.value())
130 }
131 ExtractFrom::Header { header, prefix } => {
132 let authorization_header = headers.get(header)?.to_str().ok()?;
133
134 let jwt = jwt_from_header_value(authorization_header, prefix)?;
135 self.decode(jwt)
136 }
137 };
138
139 result.ok().map(|t| t.claims)
140 }
141}
142
143fn jwt_from_header_value<'a>(header: &'a str, prefix: &str) -> Option<&'a str> {
144 let prefix_len = prefix.len();
145
146 if header.len() < prefix_len {
147 return None;
148 }
149
150 if !header[..prefix_len].eq_ignore_ascii_case(prefix) {
151 return None;
152 }
153
154 Some(&header[prefix_len..])
155}
156
157impl<S, U> FromRequestParts<S> for JwtContext<U>
158where
159 JwtContext<U>: FromRef<S>,
160 S: Send + Sync,
161{
162 type Rejection = Infallible;
163
164 async fn from_request_parts(_parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
165 Ok(Self::from_ref(state))
166 }
167}
168
169impl<T> Clone for JwtContext<T> {
170 fn clone(&self) -> Self {
171 JwtContext(self.0.clone())
172 }
173}
174
175#[cfg(all(test, feature = "cookie"))]
176mod jwt_test {
177 use std::error::Error;
178
179 use http::StatusCode;
180 use serde::Deserialize;
181
182 #[tokio::test]
183 async fn test_cookie() -> Result<(), Box<dyn Error>> {
184 use axum::{Router, body::Body, routing::get};
185 use http::Request;
186 use serde::Serialize;
187 use tower::ServiceExt;
188
189 use crate::{
190 jwt::{Jwt, JwtContext},
191 utils::utc_now_secs,
192 };
193
194 #[derive(Serialize, Deserialize, Clone)]
195 struct AT {
196 exp: u64,
197 }
198
199 let jwt_context = JwtContext::builder()
200 .extract_cookie("session")
201 .jwt_secret("test-secret")
202 .build::<AT>();
203
204 let valid_access_token = AT {
205 exp: utc_now_secs() + 10000,
206 };
207
208 let invalid_access_token = AT {
209 exp: utc_now_secs() - 10000,
210 };
211
212 let valid_cookie = jwt_context.encode_token_to_cookie(&valid_access_token)?;
213 let invalid_cookie = jwt_context.encode_token_to_cookie(&invalid_access_token)?;
214
215 let router = Router::<()>::new()
216 .route("/", get(move |_: Jwt<AT>| async { StatusCode::CREATED }))
217 .layer(jwt_context);
218
219 let mut header = format!("{}=", valid_cookie.name());
220 header.push_str(valid_cookie.value());
221
222 let request = Request::get("/")
223 .header("cookie", header)
224 .body(Body::empty())?;
225
226 let resp = router.clone().oneshot(request).await.unwrap();
227 assert!(resp.status() == StatusCode::CREATED);
228
229 let mut header = format!("{}=", invalid_cookie.name());
230 header.push_str(invalid_cookie.value());
231
232 let request = Request::get("/")
233 .header("cookie", header)
234 .body(Body::empty())?;
235
236 let resp = router.clone().oneshot(request).await.unwrap();
237 assert!(resp.status() == StatusCode::UNAUTHORIZED);
238 Ok(())
239 }
240}