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
148pub(crate) fn 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 #[cfg(feature = "jwt-asymmetric")]
189 keys: Option<crate::jwt_keys::KeySet>,
190 validation: Validation,
191}
192
193impl JwtAuth {
194 pub fn new(secret: impl Into<Vec<u8>>) -> Self {
197 let secret = secret.into();
198 assert!(
199 secret.len() >= MIN_SECRET_LEN,
200 "JWT secret must be at least {} bytes",
201 MIN_SECRET_LEN
202 );
203 Self {
204 secret,
205 #[cfg(feature = "jwt-asymmetric")]
206 keys: None,
207 validation: Validation::default(),
208 }
209 }
210
211 #[cfg(feature = "jwt-asymmetric")]
213 pub fn with_keys(keys: crate::jwt_keys::KeySet) -> Self {
214 Self {
215 secret: Vec::new(),
216 keys: Some(keys),
217 validation: Validation::default(),
218 }
219 }
220
221 pub fn validation(mut self, validation: Validation) -> Self {
222 self.validation = validation;
223 self
224 }
225}
226
227#[async_trait]
228impl Middleware for JwtAuth {
229 async fn handle(&self, mut req: Request, next: Next) -> Result<Response> {
230 if req.method == http::Method::OPTIONS {
231 return next.run(req).await;
232 }
233 let token = req
234 .headers
235 .get(http::header::AUTHORIZATION)
236 .and_then(|v| v.to_str().ok())
237 .and_then(|v| {
238 let (scheme, token) = v.split_once(' ')?;
239 scheme
240 .eq_ignore_ascii_case("bearer")
241 .then_some(token.trim())
242 })
243 .ok_or_else(|| Error::unauthorized("Missing bearer token"))?;
244
245 #[cfg(feature = "jwt-asymmetric")]
246 let claims: Value = match &self.keys {
247 Some(keys) => keys.decode(token, &self.validation)?,
248 None => decode_with(token, &self.secret, &self.validation)?,
249 };
250 #[cfg(not(feature = "jwt-asymmetric"))]
251 let claims: Value = decode_with(token, &self.secret, &self.validation)?;
252 req.extensions.insert(Claims(claims));
253 next.run(req).await
254 }
255}
256
257#[cfg(test)]
258mod tests {
259 use super::*;
260 use serde_json::json;
261
262 const SECRET: &[u8] = b"0123456789abcdef0123456789abcdef";
263
264 fn tamper(token: &str, part: usize, f: impl Fn(&str) -> String) -> String {
265 let mut p: Vec<String> = token.split('.').map(String::from).collect();
266 p[part] = f(&p[part]);
267 p.join(".")
268 }
269
270 #[test]
271 fn roundtrip_with_typed_claims() {
272 #[derive(Serialize, serde::Deserialize, PartialEq, Debug)]
273 struct C {
274 sub: String,
275 exp: u64,
276 }
277 let c = C {
278 sub: "ann".into(),
279 exp: expires_in(60),
280 };
281 let token = encode(&c, SECRET).unwrap();
282 assert_eq!(token.split('.').count(), 3);
283 assert_eq!(decode::<C>(&token, SECRET).unwrap(), c);
284 }
285
286 #[test]
287 fn matches_the_jwt_io_reference_vector() {
288 let header = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9";
291 let payload = "eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ";
292 assert_eq!(URL_SAFE_NO_PAD.encode(HEADER), header);
293
294 let mut m = HmacSha256::new_from_slice(b"your-256-bit-secret").unwrap();
295 m.update(format!("{}.{}", header, payload).as_bytes());
296 assert_eq!(
297 URL_SAFE_NO_PAD.encode(m.finalize().into_bytes()),
298 "SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
299 );
300 }
301
302 #[test]
303 fn rejects_tampering_wrong_secret_and_malformed() {
304 let token = encode(&json!({ "sub": "ann" }), SECRET).unwrap();
305 let other = b"ffffffffffffffffffffffffffffffff";
306 assert!(decode::<Value>(&token, other).is_err());
307 assert!(decode::<Value>(
308 &tamper(&token, 1, |_| URL_SAFE_NO_PAD.encode(r#"{"sub":"root"}"#)),
309 SECRET
310 )
311 .is_err());
312 assert!(decode::<Value>(&tamper(&token, 2, |s| format!("{}A", s)), SECRET).is_err());
313 for bad in ["", "a", "a.b", "a.b.c", "a.b.c.d", "..", "!!!.!!!.!!!"] {
314 assert_eq!(
315 decode::<Value>(bad, SECRET).unwrap_err().status_code(),
316 401,
317 "{bad}"
318 );
319 }
320 }
321
322 #[test]
323 fn rejects_alg_none_and_other_algs() {
324 let payload = URL_SAFE_NO_PAD.encode(r#"{"sub":"root"}"#);
325 for alg in ["none", "HS512", "RS256", "hs256"] {
326 let header = URL_SAFE_NO_PAD.encode(format!(r#"{{"alg":"{}","typ":"JWT"}}"#, alg));
327 let unsigned = format!("{}.{}.", header, payload);
328 assert!(decode::<Value>(&unsigned, SECRET).is_err(), "{alg}");
329
330 let mut m = HmacSha256::new_from_slice(SECRET).unwrap();
332 m.update(format!("{}.{}", header, payload).as_bytes());
333 let signed = format!(
334 "{}.{}.{}",
335 header,
336 payload,
337 URL_SAFE_NO_PAD.encode(m.finalize().into_bytes())
338 );
339 assert!(decode::<Value>(&signed, SECRET).is_err(), "{alg} signed");
340 }
341 }
342
343 #[test]
344 fn exp_nbf_leeway_issuer_audience() {
345 let v = Validation::default();
346 let n = 1_000_000;
347 assert!(check_claims(&json!({ "exp": n + 1 }), &v, n).is_ok());
348 assert_eq!(
349 check_claims(&json!({ "exp": n }), &v, n)
350 .unwrap_err()
351 .message(),
352 "Token expired"
353 );
354 assert!(check_claims(
355 &json!({ "exp": n }),
356 &Validation {
357 leeway_secs: 5,
358 ..Default::default()
359 },
360 n
361 )
362 .is_ok());
363 assert!(check_claims(&json!({ "nbf": n + 10 }), &v, n).is_err());
364 assert!(check_claims(
365 &json!({ "nbf": n + 10 }),
366 &Validation {
367 leeway_secs: 10,
368 ..Default::default()
369 },
370 n
371 )
372 .is_ok());
373 assert!(check_claims(&json!({ "exp": "soon" }), &v, n).is_err());
374
375 let strict = Validation {
376 issuer: Some("me".into()),
377 audience: Some("api".into()),
378 ..Default::default()
379 };
380 assert!(check_claims(&json!({ "iss": "me", "aud": "api" }), &strict, n).is_ok());
381 assert!(check_claims(&json!({ "iss": "me", "aud": ["x", "api"] }), &strict, n).is_ok());
382 assert!(check_claims(&json!({ "iss": "you", "aud": "api" }), &strict, n).is_err());
383 assert!(check_claims(&json!({ "iss": "me" }), &strict, n).is_err());
384 }
385
386 #[test]
387 fn expired_token_is_rejected_end_to_end() {
388 let token = encode(&json!({ "exp": now() - 10 }), SECRET).unwrap();
389 assert_eq!(
390 decode::<Value>(&token, SECRET).unwrap_err().message(),
391 "Token expired"
392 );
393 }
394
395 #[test]
396 fn short_secrets_are_refused() {
397 assert!(encode(&json!({}), b"short").is_err());
398 let token = encode(&json!({}), SECRET).unwrap();
399 assert!(decode::<Value>(&token, b"short").is_err());
400 }
401
402 #[test]
403 #[should_panic(expected = "at least 32 bytes")]
404 fn middleware_constructor_panics_on_short_secret() {
405 let _ = JwtAuth::new("short");
406 }
407}