Skip to main content

manta_shared/common/
jwt_ops.rs

1//! JWT claim extractors used by the audit and authorization paths.
2//!
3//! Decodes a bearer token (with or without the `Bearer ` prefix),
4//! tolerates both URL-safe and standard Base64 encodings, and
5//! returns named claims as `String`. All failures map to
6//! [`MantaError::JwtMalformed`] with a structured message; the
7//! HTTP layer maps that to a 401.
8
9use base64::prelude::*;
10use serde_json::Value;
11
12use crate::common::error::MantaError;
13
14fn get_claims_from_jwt_token(token: &str) -> Result<Value, MantaError> {
15  // Handle both "Bearer <token>" and bare "<token>" formats
16  let jwt_body = token.split(' ').nth(1).unwrap_or(token);
17
18  let base64_claims = jwt_body.split('.').nth(1).ok_or_else(|| {
19    MantaError::JwtMalformed(
20      "expected header.payload.signature format".to_string(),
21    )
22  })?;
23
24  let claims_u8 = BASE64_URL_SAFE_NO_PAD
25    .decode(base64_claims)
26    .or_else(|_| BASE64_STANDARD.decode(base64_claims))
27    .map_err(|e| {
28      MantaError::JwtMalformed(format!("could not decode claims: {e}"))
29    })?;
30
31  let claims_str = std::str::from_utf8(&claims_u8).map_err(|e| {
32    MantaError::JwtMalformed(format!("claims are not valid UTF-8: {e}"))
33  })?;
34
35  Ok(serde_json::from_str::<Value>(claims_str)?)
36}
37
38/// Extract the `name` claim from a JWT token.
39///
40/// Returns `"MISSING"` if the claim is absent.
41///
42/// # Examples
43///
44/// ```
45/// use base64::prelude::*;
46/// use manta_shared::common::jwt_ops::get_name;
47///
48/// // Build a minimal three-part JWT whose payload carries a `name` claim.
49/// let payload = BASE64_URL_SAFE_NO_PAD.encode(r#"{"name":"Alice"}"#);
50/// let token = format!("header.{}.sig", payload);
51///
52/// assert_eq!(get_name(&token).unwrap(), "Alice");
53///
54/// // Bearer prefix is tolerated.
55/// let with_prefix = format!("Bearer {}", token);
56/// assert_eq!(get_name(&with_prefix).unwrap(), "Alice");
57/// ```
58pub fn get_name(token: &str) -> Result<String, MantaError> {
59  let jwt_claims = get_claims_from_jwt_token(token)?;
60
61  let jwt_name = jwt_claims.get("name").and_then(Value::as_str);
62
63  match jwt_name {
64    Some(name) => Ok(name.to_string()),
65    None => Ok("MISSING".to_string()),
66  }
67}
68
69/// Extract the `preferred_username` claim from a JWT token.
70///
71/// Returns `"MISSING"` if the claim is absent.
72pub fn get_preferred_username(token: &str) -> Result<String, MantaError> {
73  let jwt_claims = get_claims_from_jwt_token(token)?;
74
75  let jwt_preferred_username =
76    jwt_claims.get("preferred_username").and_then(Value::as_str);
77
78  match jwt_preferred_username {
79    Some(name) => Ok(name.to_string()),
80    None => Ok("MISSING".to_string()),
81  }
82}
83
84#[cfg(test)]
85mod tests {
86  use super::*;
87
88  /// Build a fake JWT with the given JSON payload.
89  fn make_jwt(payload: &serde_json::Value) -> String {
90    let header = BASE64_URL_SAFE_NO_PAD.encode(r#"{"alg":"none","typ":"JWT"}"#);
91    let body = BASE64_URL_SAFE_NO_PAD.encode(payload.to_string());
92    format!("{header}.{body}.sig")
93  }
94
95  // ---- get_name ----
96
97  #[test]
98  fn get_name_present() {
99    let token = make_jwt(&serde_json::json!({
100      "name": "Alice Smith",
101      "preferred_username": "alice"
102    }));
103    assert_eq!(get_name(&token).unwrap(), "Alice Smith");
104  }
105
106  #[test]
107  fn get_name_missing_returns_missing() {
108    let token = make_jwt(&serde_json::json!({
109      "preferred_username": "alice"
110    }));
111    assert_eq!(get_name(&token).unwrap(), "MISSING");
112  }
113
114  #[test]
115  fn get_name_with_bearer_prefix() {
116    let token = make_jwt(&serde_json::json!({
117      "name": "Bob Jones"
118    }));
119    let bearer_token = format!("Bearer {token}");
120    assert_eq!(get_name(&bearer_token).unwrap(), "Bob Jones");
121  }
122
123  // ---- get_preferred_username ----
124
125  #[test]
126  fn get_preferred_username_present() {
127    let token = make_jwt(&serde_json::json!({
128      "name": "Alice",
129      "preferred_username": "alice123"
130    }));
131    assert_eq!(get_preferred_username(&token).unwrap(), "alice123");
132  }
133
134  #[test]
135  fn get_preferred_username_missing_returns_missing() {
136    let token = make_jwt(&serde_json::json!({"name": "Alice"}));
137    assert_eq!(get_preferred_username(&token).unwrap(), "MISSING");
138  }
139
140  // ---- get_claims_from_jwt_token ----
141
142  #[test]
143  fn malformed_jwt_no_dots() {
144    assert!(get_claims_from_jwt_token("nodots").is_err());
145  }
146
147  #[test]
148  fn malformed_jwt_invalid_base64() {
149    assert!(get_claims_from_jwt_token("header.!!!invalid.sig").is_err());
150  }
151
152  #[test]
153  fn jwt_with_standard_base64_padding() {
154    // Some JWTs use standard base64 with padding
155    let payload = serde_json::json!({"name": "Test"});
156    let header = BASE64_STANDARD.encode(r#"{"alg":"none"}"#);
157    let body = BASE64_STANDARD.encode(payload.to_string());
158    let token = format!("{header}.{body}.sig");
159    assert_eq!(get_name(&token).unwrap(), "Test");
160  }
161
162  #[test]
163  fn empty_token_string_is_err() {
164    assert!(get_claims_from_jwt_token("").is_err());
165  }
166
167  #[test]
168  fn jwt_with_valid_base64_but_invalid_json() {
169    // base64 of "not json at all"
170    let body = BASE64_URL_SAFE_NO_PAD.encode("not json at all");
171    let token = format!("header.{body}.sig");
172    assert!(get_claims_from_jwt_token(&token).is_err());
173  }
174
175  #[test]
176  fn jwt_with_valid_base64_but_invalid_utf8() {
177    // Raw bytes that aren't valid UTF-8
178    let body = BASE64_URL_SAFE_NO_PAD.encode([0xFF, 0xFE, 0xFD]);
179    let token = format!("header.{body}.sig");
180    assert!(get_claims_from_jwt_token(&token).is_err());
181  }
182
183  #[test]
184  fn get_name_with_empty_string_name() {
185    let token = make_jwt(&serde_json::json!({"name": ""}));
186    assert_eq!(get_name(&token).unwrap(), "");
187  }
188
189  #[test]
190  fn bearer_prefix_with_extra_spaces() {
191    // "Bearer  token" - the split(' ').nth(1) would get empty string
192    let token = make_jwt(&serde_json::json!({"name": "Test"}));
193    let bad_bearer = format!("Bearer  {token}");
194    // nth(1) returns empty string, which has no dots -> error
195    assert!(get_name(&bad_bearer).is_err());
196  }
197}