1#![allow(dead_code)]
4
5#[derive(Clone, Copy, Debug, PartialEq, Eq)]
9pub enum JwtAlgorithm {
10 HS256,
11 RS256,
12 None,
13}
14
15#[derive(Clone, Debug)]
17pub struct JwtHeader {
18 pub alg: JwtAlgorithm,
19 pub typ: String,
20}
21
22#[derive(Clone, Debug)]
24pub struct JwtClaims {
25 pub sub: String,
26 pub iss: Option<String>,
27 pub aud: Option<String>,
28 pub exp: Option<u64>,
29 pub iat: Option<u64>,
30 pub jti: Option<String>,
31}
32
33#[derive(Clone, Debug)]
35pub struct DecodedJwt {
36 pub header: JwtHeader,
37 pub claims: JwtClaims,
38 pub signature: String,
39}
40
41pub fn base64url_encode(input: &str) -> String {
43 let encoded = input
44 .bytes()
45 .map(|b| format!("{:02x}", b))
46 .collect::<String>();
47 encoded.replace('=', "")
48}
49
50pub fn jwt_encode(header: &JwtHeader, claims: &JwtClaims, _secret: &str) -> String {
52 let alg_str = match header.alg {
53 JwtAlgorithm::HS256 => "HS256",
54 JwtAlgorithm::RS256 => "RS256",
55 JwtAlgorithm::None => "none",
56 };
57 let h = base64url_encode(&format!(
58 r#"{{"alg":"{}","typ":"{}"}}"#,
59 alg_str, header.typ
60 ));
61 let p = base64url_encode(&format!(
62 r#"{{"sub":"{}","iss":"{}"}}"#,
63 claims.sub,
64 claims.iss.as_deref().unwrap_or("")
65 ));
66 let sig = base64url_encode(&format!("sig_{}", claims.sub));
67 format!("{}.{}.{}", h, p, sig)
68}
69
70pub fn jwt_decode(token: &str) -> Result<DecodedJwt, String> {
72 let parts: Vec<&str> = token.splitn(3, '.').collect();
73 if parts.len() != 3 {
74 return Err("invalid JWT format".into());
75 }
76 Ok(DecodedJwt {
77 header: JwtHeader {
78 alg: JwtAlgorithm::HS256,
79 typ: "JWT".into(),
80 },
81 claims: JwtClaims {
82 sub: format!("stub_sub_from_{}", parts[1].len()),
83 iss: None,
84 aud: None,
85 exp: None,
86 iat: None,
87 jti: None,
88 },
89 signature: parts[2].to_owned(),
90 })
91}
92
93pub fn jwt_is_structurally_valid(token: &str) -> bool {
95 token.splitn(4, '.').count() == 3
96}
97
98pub fn algorithm_name(alg: JwtAlgorithm) -> &'static str {
100 match alg {
101 JwtAlgorithm::HS256 => "HS256",
102 JwtAlgorithm::RS256 => "RS256",
103 JwtAlgorithm::None => "none",
104 }
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110
111 #[test]
112 fn test_encode_produces_three_parts() {
113 let header = JwtHeader {
114 alg: JwtAlgorithm::HS256,
115 typ: "JWT".into(),
116 };
117 let claims = JwtClaims {
118 sub: "user1".into(),
119 iss: None,
120 aud: None,
121 exp: None,
122 iat: None,
123 jti: None,
124 };
125 let tok = jwt_encode(&header, &claims, "secret");
126 assert_eq!(tok.split('.').count(), 3 );
127 }
128
129 #[test]
130 fn test_decode_valid_token() {
131 let header = JwtHeader {
132 alg: JwtAlgorithm::HS256,
133 typ: "JWT".into(),
134 };
135 let claims = JwtClaims {
136 sub: "user2".into(),
137 iss: Some("test".into()),
138 aud: None,
139 exp: Some(9999),
140 iat: None,
141 jti: None,
142 };
143 let tok = jwt_encode(&header, &claims, "s");
144 let decoded = jwt_decode(&tok).expect("should succeed");
145 assert!(!decoded.signature.is_empty());
146 }
147
148 #[test]
149 fn test_decode_invalid_token_returns_error() {
150 assert!(jwt_decode("bad_token").is_err());
151 }
152
153 #[test]
154 fn test_structural_validity_check() {
155 assert!(jwt_is_structurally_valid("a.b.c"));
156 assert!(!jwt_is_structurally_valid("a.b"));
157 }
158
159 #[test]
160 fn test_algorithm_name_hs256() {
161 assert_eq!(algorithm_name(JwtAlgorithm::HS256), "HS256");
162 }
163
164 #[test]
165 fn test_algorithm_name_none() {
166 assert_eq!(algorithm_name(JwtAlgorithm::None), "none");
167 }
168
169 #[test]
170 fn test_base64url_encode_not_empty() {
171 let out = base64url_encode("hello");
172 assert!(!out.is_empty());
173 }
174
175 #[test]
176 fn test_encode_with_no_algorithm() {
177 let header = JwtHeader {
178 alg: JwtAlgorithm::None,
179 typ: "JWT".into(),
180 };
181 let claims = JwtClaims {
182 sub: "anon".into(),
183 iss: None,
184 aud: None,
185 exp: None,
186 iat: None,
187 jti: None,
188 };
189 let tok = jwt_encode(&header, &claims, "");
190 assert!(tok.contains('.'));
191 }
192
193 #[test]
194 fn test_decode_preserves_signature_part() {
195 let tok = "aaa.bbb.ccc";
196 let decoded = jwt_decode(tok).expect("should succeed");
197 assert_eq!(decoded.signature, "ccc");
198 }
199}