1use serde::{Deserialize, Serialize};
4
5#[derive(Debug, thiserror::Error)]
7pub enum JwtError {
8 #[error("token expired")]
10 Expired,
11
12 #[error("invalid token: {0}")]
14 Invalid(String),
15
16 #[error("missing claim: {0}")]
18 MissingClaim(String),
19}
20
21pub fn encode_hs256(claims: &impl Serialize, secret: &str) -> Result<String, JwtError> {
23 let header = jsonwebtoken::Header::new(jsonwebtoken::Algorithm::HS256);
24 jsonwebtoken::encode(
25 &header,
26 claims,
27 &jsonwebtoken::EncodingKey::from_secret(secret.as_bytes()),
28 )
29 .map_err(|e| JwtError::Invalid(e.to_string()))
30}
31
32pub fn decode_hs256<T: for<'de> Deserialize<'de>>(
34 token: &str,
35 secret: &str,
36 issuer: Option<&str>,
37 audience: Option<&str>,
38) -> Result<T, JwtError> {
39 let mut validation = jsonwebtoken::Validation::new(jsonwebtoken::Algorithm::HS256);
40
41 if let Some(iss) = issuer {
42 validation.set_issuer(&[iss]);
43 }
44 if let Some(aud) = audience {
45 validation.set_audience(&[aud]);
46 }
47
48 jsonwebtoken::decode::<T>(
49 token,
50 &jsonwebtoken::DecodingKey::from_secret(secret.as_bytes()),
51 &validation,
52 )
53 .map(|data| data.claims)
54 .map_err(|e| match *e.kind() {
55 jsonwebtoken::errors::ErrorKind::ExpiredSignature => JwtError::Expired,
56 _ => JwtError::Invalid(e.to_string()),
57 })
58}
59
60pub fn extract_bearer_token(header_value: &str) -> Option<&str> {
62 header_value.strip_prefix("Bearer ").map(|t| t.trim())
63}
64
65pub fn build_auth_cookie(cookie_name: &str, token: &str, max_age_secs: i64) -> String {
67 format!("{cookie_name}={token}; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age={max_age_secs}")
68}
69
70#[cfg(test)]
71#[allow(clippy::unwrap_used, clippy::expect_used)]
73mod tests {
74 use super::*;
75 use serde::{Deserialize, Serialize};
76
77 #[derive(Debug, Serialize, Deserialize, PartialEq)]
78 struct TestClaims {
79 sub: String,
80 exp: usize,
81 iat: usize,
82 }
83
84 #[test]
85 fn encode_decode_roundtrip() {
86 let claims = TestClaims {
87 sub: "user123".into(),
88 exp: 9999999999,
89 iat: 1000000000,
90 };
91 let token = encode_hs256(&claims, "secret-key-1234567890123456").unwrap();
92 let decoded: TestClaims =
93 decode_hs256(&token, "secret-key-1234567890123456", None, None).unwrap();
94 assert_eq!(decoded, claims);
95 }
96
97 #[test]
98 fn decode_wrong_secret_fails() {
99 let claims = TestClaims {
100 sub: "user123".into(),
101 exp: 9999999999,
102 iat: 1000000000,
103 };
104 let token = encode_hs256(&claims, "secret1").unwrap();
105 assert!(decode_hs256::<TestClaims>(&token, "secret2", None, None).is_err());
106 }
107
108 #[test]
109 fn extract_bearer_token_valid() {
110 assert_eq!(extract_bearer_token("Bearer abc123"), Some("abc123"));
111 }
112
113 #[test]
114 fn extract_bearer_token_missing() {
115 assert!(extract_bearer_token("Basic abc123").is_none());
116 }
117
118 #[test]
119 fn build_auth_cookie_format() {
120 let cookie = build_auth_cookie("session", "tok123", 3600);
121 assert!(cookie.contains("session=tok123"));
122 assert!(cookie.contains("HttpOnly"));
123 assert!(cookie.contains("Secure"));
124 assert!(cookie.contains("Max-Age=3600"));
125 }
126}