1use crate::error::{Error, Result};
38use crate::middleware::{Middleware, Next};
39use crate::request::Request;
40use crate::response::Response;
41use async_trait::async_trait;
42use base64::engine::general_purpose::URL_SAFE_NO_PAD;
43use base64::Engine;
44use hmac::{Hmac, KeyInit, Mac};
45use serde::de::DeserializeOwned;
46use serde::Serialize;
47use serde_json::Value;
48use sha2::Sha256;
49use std::time::{SystemTime, UNIX_EPOCH};
50
51type HmacSha256 = Hmac<Sha256>;
52
53const MIN_SECRET_LEN: usize = 32;
54const HEADER: &str = r#"{"alg":"HS256","typ":"JWT"}"#;
55
56#[derive(Debug, Clone, PartialEq)]
58pub struct Claims(pub Value);
59
60#[derive(Debug, Clone, Default)]
62pub struct Validation {
63 pub leeway_secs: u64,
65 pub issuer: Option<String>,
67 pub audience: Option<String>,
69}
70
71pub fn now() -> u64 {
73 SystemTime::now()
74 .duration_since(UNIX_EPOCH)
75 .map(|d| d.as_secs())
76 .unwrap_or(0)
77}
78
79pub fn expires_in(secs: u64) -> u64 {
81 now() + secs
82}
83
84fn mac(secret: &[u8]) -> Result<HmacSha256> {
85 if secret.len() < MIN_SECRET_LEN {
86 return Err(Error::internal(format!(
87 "JWT secret must be at least {} bytes",
88 MIN_SECRET_LEN
89 )));
90 }
91 HmacSha256::new_from_slice(secret).map_err(|_| Error::internal("Invalid JWT secret"))
92}
93
94pub fn encode<T: Serialize>(claims: &T, secret: &[u8]) -> Result<String> {
96 let payload =
97 serde_json::to_vec(claims).map_err(|e| Error::internal(format!("JWT claims: {}", e)))?;
98 let signing_input = format!(
99 "{}.{}",
100 URL_SAFE_NO_PAD.encode(HEADER),
101 URL_SAFE_NO_PAD.encode(payload)
102 );
103 let mut m = mac(secret)?;
104 m.update(signing_input.as_bytes());
105 let signature = URL_SAFE_NO_PAD.encode(m.finalize().into_bytes());
106 Ok(format!("{}.{}", signing_input, signature))
107}
108
109pub fn decode<T: DeserializeOwned>(token: &str, secret: &[u8]) -> Result<T> {
111 decode_with(token, secret, &Validation::default())
112}
113
114pub fn decode_with<T: DeserializeOwned>(
116 token: &str,
117 secret: &[u8],
118 validation: &Validation,
119) -> Result<T> {
120 let invalid = || Error::unauthorized("Invalid token");
121
122 let mut parts = token.split('.');
123 let (Some(h), Some(p), Some(s), None) =
124 (parts.next(), parts.next(), parts.next(), parts.next())
125 else {
126 return Err(invalid());
127 };
128
129 let header: Value = serde_json::from_slice(&URL_SAFE_NO_PAD.decode(h).map_err(|_| invalid())?)
130 .map_err(|_| invalid())?;
131 if header.get("alg").and_then(Value::as_str) != Some("HS256") {
132 return Err(invalid());
133 }
134
135 let signature = URL_SAFE_NO_PAD.decode(s).map_err(|_| invalid())?;
136 let mut m = mac(secret)?;
137 m.update(h.as_bytes());
138 m.update(b".");
139 m.update(p.as_bytes());
140 m.verify_slice(&signature).map_err(|_| invalid())?;
141
142 let claims: Value = serde_json::from_slice(&URL_SAFE_NO_PAD.decode(p).map_err(|_| invalid())?)
143 .map_err(|_| invalid())?;
144 check_claims(&claims, validation, now())?;
145 serde_json::from_value(claims).map_err(|_| invalid())
146}
147
148fn check_claims(claims: &Value, v: &Validation, now: u64) -> Result<()> {
149 if let Some(exp) = claims.get("exp") {
150 let exp = exp
151 .as_u64()
152 .ok_or_else(|| Error::unauthorized("Invalid token"))?;
153 if now >= exp.saturating_add(v.leeway_secs) {
154 return Err(Error::unauthorized("Token expired"));
155 }
156 }
157 if let Some(nbf) = claims.get("nbf") {
158 let nbf = nbf
159 .as_u64()
160 .ok_or_else(|| Error::unauthorized("Invalid token"))?;
161 if now.saturating_add(v.leeway_secs) < nbf {
162 return Err(Error::unauthorized("Token not yet valid"));
163 }
164 }
165 if let Some(iss) = &v.issuer {
166 if claims.get("iss").and_then(Value::as_str) != Some(iss) {
167 return Err(Error::unauthorized("Invalid token"));
168 }
169 }
170 if let Some(aud) = &v.audience {
171 let ok = match claims.get("aud") {
172 Some(Value::String(a)) => a == aud,
173 Some(Value::Array(list)) => list.iter().any(|a| a.as_str() == Some(aud)),
174 _ => false,
175 };
176 if !ok {
177 return Err(Error::unauthorized("Invalid token"));
178 }
179 }
180 Ok(())
181}
182
183pub struct JwtAuth {
187 secret: Vec<u8>,
188 validation: Validation,
189}
190
191impl JwtAuth {
192 pub fn new(secret: impl Into<Vec<u8>>) -> Self {
195 let secret = secret.into();
196 assert!(
197 secret.len() >= MIN_SECRET_LEN,
198 "JWT secret must be at least {} bytes",
199 MIN_SECRET_LEN
200 );
201 Self {
202 secret,
203 validation: Validation::default(),
204 }
205 }
206
207 pub fn validation(mut self, validation: Validation) -> Self {
208 self.validation = validation;
209 self
210 }
211}
212
213#[async_trait]
214impl Middleware for JwtAuth {
215 async fn handle(&self, mut req: Request, next: Next) -> Result<Response> {
216 if req.method == http::Method::OPTIONS {
217 return next.run(req).await;
218 }
219 let token = req
220 .headers
221 .get(http::header::AUTHORIZATION)
222 .and_then(|v| v.to_str().ok())
223 .and_then(|v| {
224 let (scheme, token) = v.split_once(' ')?;
225 scheme
226 .eq_ignore_ascii_case("bearer")
227 .then_some(token.trim())
228 })
229 .ok_or_else(|| Error::unauthorized("Missing bearer token"))?;
230
231 let claims: Value = decode_with(token, &self.secret, &self.validation)?;
232 req.extensions.insert(Claims(claims));
233 next.run(req).await
234 }
235}
236
237#[cfg(test)]
238mod tests {
239 use super::*;
240 use serde_json::json;
241
242 const SECRET: &[u8] = b"0123456789abcdef0123456789abcdef";
243
244 fn tamper(token: &str, part: usize, f: impl Fn(&str) -> String) -> String {
245 let mut p: Vec<String> = token.split('.').map(String::from).collect();
246 p[part] = f(&p[part]);
247 p.join(".")
248 }
249
250 #[test]
251 fn roundtrip_with_typed_claims() {
252 #[derive(Serialize, serde::Deserialize, PartialEq, Debug)]
253 struct C {
254 sub: String,
255 exp: u64,
256 }
257 let c = C {
258 sub: "ann".into(),
259 exp: expires_in(60),
260 };
261 let token = encode(&c, SECRET).unwrap();
262 assert_eq!(token.split('.').count(), 3);
263 assert_eq!(decode::<C>(&token, SECRET).unwrap(), c);
264 }
265
266 #[test]
267 fn matches_the_jwt_io_reference_vector() {
268 let header = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9";
271 let payload = "eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ";
272 assert_eq!(URL_SAFE_NO_PAD.encode(HEADER), header);
273
274 let mut m = HmacSha256::new_from_slice(b"your-256-bit-secret").unwrap();
275 m.update(format!("{}.{}", header, payload).as_bytes());
276 assert_eq!(
277 URL_SAFE_NO_PAD.encode(m.finalize().into_bytes()),
278 "SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
279 );
280 }
281
282 #[test]
283 fn rejects_tampering_wrong_secret_and_malformed() {
284 let token = encode(&json!({ "sub": "ann" }), SECRET).unwrap();
285 let other = b"ffffffffffffffffffffffffffffffff";
286 assert!(decode::<Value>(&token, other).is_err());
287 assert!(decode::<Value>(
288 &tamper(&token, 1, |_| URL_SAFE_NO_PAD.encode(r#"{"sub":"root"}"#)),
289 SECRET
290 )
291 .is_err());
292 assert!(decode::<Value>(&tamper(&token, 2, |s| format!("{}A", s)), SECRET).is_err());
293 for bad in ["", "a", "a.b", "a.b.c", "a.b.c.d", "..", "!!!.!!!.!!!"] {
294 assert_eq!(
295 decode::<Value>(bad, SECRET).unwrap_err().status_code(),
296 401,
297 "{bad}"
298 );
299 }
300 }
301
302 #[test]
303 fn rejects_alg_none_and_other_algs() {
304 let payload = URL_SAFE_NO_PAD.encode(r#"{"sub":"root"}"#);
305 for alg in ["none", "HS512", "RS256", "hs256"] {
306 let header = URL_SAFE_NO_PAD.encode(format!(r#"{{"alg":"{}","typ":"JWT"}}"#, alg));
307 let unsigned = format!("{}.{}.", header, payload);
308 assert!(decode::<Value>(&unsigned, SECRET).is_err(), "{alg}");
309
310 let mut m = HmacSha256::new_from_slice(SECRET).unwrap();
312 m.update(format!("{}.{}", header, payload).as_bytes());
313 let signed = format!(
314 "{}.{}.{}",
315 header,
316 payload,
317 URL_SAFE_NO_PAD.encode(m.finalize().into_bytes())
318 );
319 assert!(decode::<Value>(&signed, SECRET).is_err(), "{alg} signed");
320 }
321 }
322
323 #[test]
324 fn exp_nbf_leeway_issuer_audience() {
325 let v = Validation::default();
326 let n = 1_000_000;
327 assert!(check_claims(&json!({ "exp": n + 1 }), &v, n).is_ok());
328 assert_eq!(
329 check_claims(&json!({ "exp": n }), &v, n)
330 .unwrap_err()
331 .message(),
332 "Token expired"
333 );
334 assert!(check_claims(
335 &json!({ "exp": n }),
336 &Validation {
337 leeway_secs: 5,
338 ..Default::default()
339 },
340 n
341 )
342 .is_ok());
343 assert!(check_claims(&json!({ "nbf": n + 10 }), &v, n).is_err());
344 assert!(check_claims(
345 &json!({ "nbf": n + 10 }),
346 &Validation {
347 leeway_secs: 10,
348 ..Default::default()
349 },
350 n
351 )
352 .is_ok());
353 assert!(check_claims(&json!({ "exp": "soon" }), &v, n).is_err());
354
355 let strict = Validation {
356 issuer: Some("me".into()),
357 audience: Some("api".into()),
358 ..Default::default()
359 };
360 assert!(check_claims(&json!({ "iss": "me", "aud": "api" }), &strict, n).is_ok());
361 assert!(check_claims(&json!({ "iss": "me", "aud": ["x", "api"] }), &strict, n).is_ok());
362 assert!(check_claims(&json!({ "iss": "you", "aud": "api" }), &strict, n).is_err());
363 assert!(check_claims(&json!({ "iss": "me" }), &strict, n).is_err());
364 }
365
366 #[test]
367 fn expired_token_is_rejected_end_to_end() {
368 let token = encode(&json!({ "exp": now() - 10 }), SECRET).unwrap();
369 assert_eq!(
370 decode::<Value>(&token, SECRET).unwrap_err().message(),
371 "Token expired"
372 );
373 }
374
375 #[test]
376 fn short_secrets_are_refused() {
377 assert!(encode(&json!({}), b"short").is_err());
378 let token = encode(&json!({}), SECRET).unwrap();
379 assert!(decode::<Value>(&token, b"short").is_err());
380 }
381
382 #[test]
383 #[should_panic(expected = "at least 32 bytes")]
384 fn middleware_constructor_panics_on_short_secret() {
385 let _ = JwtAuth::new("short");
386 }
387}