1use super::{AuthResult, ClaimFault, JwtConfig, JwtError};
10use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation};
11use std::collections::HashMap;
12
13const ALLOWED_SKEW: i64 = 30;
26
27pub fn validate_token(token: &str, config: &JwtConfig) -> Result<AuthResult, JwtError> {
29 let secret = config.secret.as_ref().ok_or(JwtError::SecretMissing)?;
30
31 let key_bytes = if config.secret_is_base64 {
33 base64_decode(secret)?
34 } else {
35 secret.as_bytes().to_vec()
36 };
37
38 let key = DecodingKey::from_secret(&key_bytes);
39
40 let mut validation = Validation::new(Algorithm::HS256);
45 validation.validate_exp = false;
46 validation.validate_nbf = false;
47 validation.validate_aud = false;
48 validation.required_spec_claims.clear();
49
50 let token_data = decode::<HashMap<String, serde_json::Value>>(token, &key, &validation)
51 .map_err(map_jwt_error)?;
52 let claims = token_data.claims;
53
54 check_claims(&claims, config, now())?;
55
56 let role = role_of(&claims, config).ok_or(JwtError::NoIdentity)?;
59
60 Ok(AuthResult { role, claims })
61}
62
63fn role_of(claims: &HashMap<String, serde_json::Value>, config: &JwtConfig) -> Option<String> {
71 claims
72 .get(&config.role_claim_key)
73 .and_then(serde_json::Value::as_str)
74 .map(str::to_string)
75 .or_else(|| config.anon_role.clone())
76}
77
78fn check_claims(
84 claims: &HashMap<String, serde_json::Value>,
85 config: &JwtConfig,
86 now: i64,
87) -> Result<(), JwtError> {
88 let number = |name: &'static str| -> Result<Option<i64>, JwtError> {
91 match claims.get(name) {
92 None | Some(serde_json::Value::Null) => Ok(None),
93 Some(value) => value
94 .as_f64()
95 .map(|seconds| seconds as i64)
96 .map(Some)
97 .ok_or(JwtError::Claim(ClaimFault::NotANumber(name))),
98 }
99 };
100
101 if let Some(exp) = number("exp")? {
102 if now > exp {
103 return Err(JwtError::Claim(ClaimFault::Expired));
104 }
105 }
106 if let Some(nbf) = number("nbf")? {
107 if now + ALLOWED_SKEW < nbf {
108 return Err(JwtError::Claim(ClaimFault::NotYetValid));
109 }
110 }
111 if let Some(iat) = number("iat")? {
112 if now + ALLOWED_SKEW < iat {
113 return Err(JwtError::Claim(ClaimFault::IssuedInFuture));
114 }
115 }
116
117 let matches = |aud: &str| match &config.audience {
123 Some(wanted) => aud == wanted,
124 None => true,
125 };
126 match claims.get("aud") {
127 None | Some(serde_json::Value::Null) => {}
128 Some(serde_json::Value::String(aud)) => {
129 if !matches(aud) {
130 return Err(JwtError::Claim(ClaimFault::NotInAudience));
131 }
132 }
133 Some(serde_json::Value::Array(auds)) => {
134 if !auds.iter().all(serde_json::Value::is_string) {
135 return Err(JwtError::Claim(ClaimFault::AudienceNotStrings));
136 }
137 let admitted = auds
139 .iter()
140 .filter_map(serde_json::Value::as_str)
141 .any(matches);
142 if !auds.is_empty() && !admitted {
143 return Err(JwtError::Claim(ClaimFault::NotInAudience));
144 }
145 }
146 Some(_) => return Err(JwtError::Claim(ClaimFault::AudienceNotStrings)),
147 }
148
149 Ok(())
150}
151
152fn now() -> i64 {
154 std::time::SystemTime::now()
155 .duration_since(std::time::UNIX_EPOCH)
156 .map(|elapsed| elapsed.as_secs() as i64)
157 .unwrap_or_default()
158}
159
160fn base64_decode(s: &str) -> Result<Vec<u8>, JwtError> {
162 use base64::{engine::general_purpose::STANDARD, Engine};
163 STANDARD.decode(s).map_err(|_| JwtError::NoSuitableKey)
164}
165
166fn map_jwt_error(e: jsonwebtoken::errors::Error) -> JwtError {
168 use jsonwebtoken::errors::ErrorKind;
169
170 match e.kind() {
171 ErrorKind::InvalidSignature => JwtError::InvalidSignature,
172 ErrorKind::Json(_) => JwtError::Claim(ClaimFault::Unparsable),
175 _ => JwtError::NoSuitableKey,
176 }
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182
183 fn config() -> JwtConfig {
184 JwtConfig {
185 secret: Some("reallyreallyreallyreallyverysafe".into()),
186 ..JwtConfig::default()
187 }
188 }
189
190 fn claims(json: serde_json::Value) -> HashMap<String, serde_json::Value> {
191 match json {
192 serde_json::Value::Object(map) => map.into_iter().collect(),
193 _ => panic!("claims must be an object"),
194 }
195 }
196
197 #[test]
200 fn an_audience_that_is_not_a_string_is_a_malformed_token() {
201 assert!(matches!(
202 check_claims(
203 &claims(serde_json::json!({"aud": [{"invalid": "value"}, "test"]})),
204 &config(),
205 0
206 ),
207 Err(JwtError::Claim(ClaimFault::AudienceNotStrings))
208 ));
209 }
210
211 #[test]
213 fn any_audience_is_accepted_when_none_is_configured() {
214 assert!(check_claims(
215 &claims(serde_json::json!({"aud": ["someone", "else"]})),
216 &config(),
217 0
218 )
219 .is_ok());
220 }
221
222 #[test]
224 fn an_empty_audience_list_excludes_no_one() {
225 let configured = JwtConfig {
226 audience: Some("us".into()),
227 ..config()
228 };
229 assert!(check_claims(&claims(serde_json::json!({"aud": []})), &configured, 0).is_ok());
230 assert!(matches!(
231 check_claims(
232 &claims(serde_json::json!({"aud": ["them"]})),
233 &configured,
234 0
235 ),
236 Err(JwtError::Claim(ClaimFault::NotInAudience))
237 ));
238 }
239
240 #[test]
242 fn a_token_without_exp_is_not_rejected_for_having_none() {
243 assert!(check_claims(&claims(serde_json::json!({"role": "x"})), &config(), 0).is_ok());
244 }
245
246 #[test]
248 fn a_timestamp_claim_that_is_not_a_number_is_reported_as_such() {
249 assert!(matches!(
250 check_claims(&claims(serde_json::json!({"exp": "soon"})), &config(), 0),
251 Err(JwtError::Claim(ClaimFault::NotANumber("exp")))
252 ));
253 }
254
255 #[test]
257 fn a_token_that_is_not_valid_yet_is_given_the_benefit_of_the_clock() {
258 let issued_moments_hence = claims(serde_json::json!({"iat": 1_000}));
259 assert!(check_claims(&issued_moments_hence, &config(), 990).is_ok());
260 assert!(matches!(
261 check_claims(&issued_moments_hence, &config(), 900),
262 Err(JwtError::Claim(ClaimFault::IssuedInFuture))
263 ));
264
265 let valid_moments_hence = claims(serde_json::json!({"nbf": 1_000}));
266 assert!(check_claims(&valid_moments_hence, &config(), 990).is_ok());
267 assert!(matches!(
268 check_claims(&valid_moments_hence, &config(), 900),
269 Err(JwtError::Claim(ClaimFault::NotYetValid))
270 ));
271 }
272
273 #[test]
276 fn an_expiry_is_honoured_to_the_second() {
277 let expires_at = claims(serde_json::json!({"exp": 1_000}));
278 assert!(check_claims(&expires_at, &config(), 1_000).is_ok());
279 assert!(matches!(
280 check_claims(&expires_at, &config(), 1_001),
281 Err(JwtError::Claim(ClaimFault::Expired))
282 ));
283 }
284
285 #[test]
287 fn a_role_claim_that_is_not_a_string_names_no_role() {
288 let configured = JwtConfig {
289 anon_role: Some("anon".into()),
290 ..config()
291 };
292 for value in [
293 serde_json::json!(42),
294 serde_json::json!({"a": 1}),
295 serde_json::json!(["admin"]),
296 ] {
297 let mut claims = HashMap::new();
298 claims.insert("role".to_string(), value.clone());
299 assert_eq!(
300 role_of(&claims, &configured),
301 Some("anon".to_string()),
302 "{value}"
303 );
304 }
305
306 let mut named = HashMap::new();
307 named.insert("role".to_string(), serde_json::json!("web_user"));
308 assert_eq!(role_of(&named, &configured), Some("web_user".to_string()));
309 }
310
311 #[test]
313 fn an_expired_token_is_reported_as_expired_whatever_else_is_wrong() {
314 assert!(matches!(
315 check_claims(
316 &claims(serde_json::json!({"exp": 1_000, "aud": [{"bad": 1}]})),
317 &config(),
318 9_000
319 ),
320 Err(JwtError::Claim(ClaimFault::Expired))
321 ));
322 }
323}