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