manta_shared/common/
jwt_ops.rs1use base64::prelude::*;
29use serde_json::Value;
30
31use crate::common::error::MantaError;
32
33pub static PA_ADMIN: &str = "pa_admin";
37
38fn get_claims_from_jwt_token(token: &str) -> Result<Value, MantaError> {
39 let jwt_body = token.split(' ').nth(1).unwrap_or(token);
41
42 let base64_claims = jwt_body.split('.').nth(1).ok_or_else(|| {
43 MantaError::JwtMalformed(
44 "expected header.payload.signature format".to_string(),
45 )
46 })?;
47
48 let claims_u8 = BASE64_URL_SAFE_NO_PAD
49 .decode(base64_claims)
50 .or_else(|_| BASE64_STANDARD.decode(base64_claims))
51 .map_err(|e| {
52 MantaError::JwtMalformed(format!("could not decode claims: {e}"))
53 })?;
54
55 let claims_str = std::str::from_utf8(&claims_u8).map_err(|e| {
56 MantaError::JwtMalformed(format!("claims are not valid UTF-8: {e}"))
57 })?;
58
59 Ok(serde_json::from_str::<Value>(claims_str)?)
60}
61
62pub fn get_name(token: &str) -> Result<String, MantaError> {
72 let jwt_claims = get_claims_from_jwt_token(token)?;
73
74 let jwt_name = jwt_claims.get("name").and_then(Value::as_str);
75
76 match jwt_name {
77 Some(name) => Ok(name.to_string()),
78 None => Ok("MISSING".to_string()),
79 }
80}
81
82pub fn get_preferred_username(token: &str) -> Result<String, MantaError> {
92 let jwt_claims = get_claims_from_jwt_token(token)?;
93
94 let jwt_preferred_username =
95 jwt_claims.get("preferred_username").and_then(Value::as_str);
96
97 match jwt_preferred_username {
98 Some(name) => Ok(name.to_string()),
99 None => Ok("MISSING".to_string()),
100 }
101}
102
103pub fn get_roles(token: &str) -> Result<Vec<String>, MantaError> {
116 Ok(
118 get_claims_from_jwt_token(token)?
119 .pointer("/realm_access/roles")
120 .unwrap_or(&serde_json::json!([]))
121 .as_array()
122 .cloned()
123 .unwrap_or_default()
124 .iter()
125 .filter_map(|role_value| role_value.as_str().map(str::to_string))
126 .collect(),
127 )
128}
129
130pub fn has_role(token: &str, role: &str) -> bool {
137 get_roles(token).is_ok_and(|roles| roles.iter().any(|r| r == role))
138}
139
140pub fn is_user_admin(token: &str) -> bool {
148 has_role(token, PA_ADMIN)
149}
150
151pub fn get_string_claim(
163 token: &str,
164 claim: &str,
165) -> Result<Option<String>, MantaError> {
166 Ok(
167 get_claims_from_jwt_token(token)?
168 .get(claim)
169 .and_then(Value::as_str)
170 .map(str::to_string),
171 )
172}
173
174#[cfg(test)]
175mod tests {
176 use super::*;
177
178 fn make_jwt(payload: &serde_json::Value) -> String {
180 let header = BASE64_URL_SAFE_NO_PAD.encode(r#"{"alg":"none","typ":"JWT"}"#);
181 let body = BASE64_URL_SAFE_NO_PAD.encode(payload.to_string());
182 format!("{header}.{body}.sig")
183 }
184
185 #[test]
188 fn get_name_present() {
189 let token = make_jwt(&serde_json::json!({
190 "name": "Alice Smith",
191 "preferred_username": "alice"
192 }));
193 assert_eq!(get_name(&token).unwrap(), "Alice Smith");
194 }
195
196 #[test]
197 fn get_name_missing_returns_missing() {
198 let token = make_jwt(&serde_json::json!({
199 "preferred_username": "alice"
200 }));
201 assert_eq!(get_name(&token).unwrap(), "MISSING");
202 }
203
204 #[test]
205 fn get_name_with_bearer_prefix() {
206 let token = make_jwt(&serde_json::json!({
207 "name": "Bob Jones"
208 }));
209 let bearer_token = format!("Bearer {token}");
210 assert_eq!(get_name(&bearer_token).unwrap(), "Bob Jones");
211 }
212
213 #[test]
216 fn get_preferred_username_present() {
217 let token = make_jwt(&serde_json::json!({
218 "name": "Alice",
219 "preferred_username": "alice123"
220 }));
221 assert_eq!(get_preferred_username(&token).unwrap(), "alice123");
222 }
223
224 #[test]
225 fn get_preferred_username_missing_returns_missing() {
226 let token = make_jwt(&serde_json::json!({"name": "Alice"}));
227 assert_eq!(get_preferred_username(&token).unwrap(), "MISSING");
228 }
229
230 #[test]
233 fn malformed_jwt_no_dots() {
234 assert!(get_claims_from_jwt_token("nodots").is_err());
235 }
236
237 #[test]
238 fn malformed_jwt_invalid_base64() {
239 assert!(get_claims_from_jwt_token("header.!!!invalid.sig").is_err());
240 }
241
242 #[test]
243 fn jwt_with_standard_base64_padding() {
244 let payload = serde_json::json!({"name": "Test"});
246 let header = BASE64_STANDARD.encode(r#"{"alg":"none"}"#);
247 let body = BASE64_STANDARD.encode(payload.to_string());
248 let token = format!("{header}.{body}.sig");
249 assert_eq!(get_name(&token).unwrap(), "Test");
250 }
251
252 #[test]
253 fn empty_token_string_is_err() {
254 assert!(get_claims_from_jwt_token("").is_err());
255 }
256
257 #[test]
258 fn jwt_with_valid_base64_but_invalid_json() {
259 let body = BASE64_URL_SAFE_NO_PAD.encode("not json at all");
261 let token = format!("header.{body}.sig");
262 assert!(get_claims_from_jwt_token(&token).is_err());
263 }
264
265 #[test]
266 fn jwt_with_valid_base64_but_invalid_utf8() {
267 let body = BASE64_URL_SAFE_NO_PAD.encode([0xFF, 0xFE, 0xFD]);
269 let token = format!("header.{body}.sig");
270 assert!(get_claims_from_jwt_token(&token).is_err());
271 }
272
273 #[test]
274 fn get_name_with_empty_string_name() {
275 let token = make_jwt(&serde_json::json!({"name": ""}));
276 assert_eq!(get_name(&token).unwrap(), "");
277 }
278
279 #[test]
280 fn bearer_prefix_with_extra_spaces() {
281 let token = make_jwt(&serde_json::json!({"name": "Test"}));
283 let bad_bearer = format!("Bearer {token}");
284 assert!(get_name(&bad_bearer).is_err());
286 }
287
288 #[test]
291 fn has_role_finds_role_when_present() {
292 let token = make_jwt(&serde_json::json!({
293 "realm_access": { "roles": ["other-role", "sample-role"] }
294 }));
295 assert!(has_role(&token, "sample-role"));
296 }
297
298 #[test]
299 fn has_role_returns_false_when_role_absent() {
300 let token = make_jwt(&serde_json::json!({
301 "realm_access": { "roles": ["other-role"] }
302 }));
303 assert!(!has_role(&token, "sample-role"));
304 }
305
306 #[test]
307 fn has_role_returns_false_on_missing_realm_access() {
308 let token = make_jwt(&serde_json::json!({ "name": "Alice" }));
309 assert!(!has_role(&token, "sample-role"));
310 }
311
312 #[test]
313 fn has_role_returns_false_on_empty_roles_array() {
314 let token = make_jwt(&serde_json::json!({
315 "realm_access": { "roles": [] }
316 }));
317 assert!(!has_role(&token, "sample-role"));
318 }
319
320 #[test]
321 fn has_role_returns_false_on_malformed_jwt() {
322 assert!(!has_role("not.a.jwt", "sample-role"));
323 assert!(!has_role("only-two.dots", "sample-role"));
324 assert!(!has_role("", "sample-role"));
325 }
326
327 #[test]
328 fn has_role_with_bearer_prefix() {
329 let token = make_jwt(&serde_json::json!({
330 "realm_access": { "roles": ["sample-role"] }
331 }));
332 let bearer_token = format!("Bearer {token}");
333 assert!(has_role(&bearer_token, "sample-role"));
334 }
335
336 #[test]
337 fn has_role_can_check_any_role_string() {
338 let token = make_jwt(&serde_json::json!({
341 "realm_access": { "roles": ["pa_admin"] }
342 }));
343 assert!(has_role(&token, "pa_admin"));
344 assert!(!has_role(&token, "sample-role"));
345 }
346
347 #[test]
350 fn get_string_claim_present() {
351 let token = make_jwt(&serde_json::json!({"sub": "f3a9-uuid"}));
352 assert_eq!(
353 get_string_claim(&token, "sub").unwrap().as_deref(),
354 Some("f3a9-uuid")
355 );
356 }
357
358 #[test]
359 fn get_string_claim_missing_or_non_string_is_none() {
360 let token = make_jwt(&serde_json::json!({"sub": 42}));
361 assert_eq!(get_string_claim(&token, "sub").unwrap(), None);
362 assert_eq!(get_string_claim(&token, "name").unwrap(), None);
363 }
364
365 #[test]
366 fn get_string_claim_malformed_token_errors() {
367 assert!(get_string_claim("not-a-jwt", "sub").is_err());
368 }
369}