1#![warn(missing_docs)]
6
7#[cfg(any(target_arch = "wasm32", feature = "jwt-rust-crypto"))]
11extern crate jsonwebtoken10 as jsonwebtoken;
12
13mod cache;
14mod claims;
15pub mod hasura;
16pub mod jwks;
17mod jwt;
18mod keys;
19
20pub use cache::{CacheOutcome, CacheStats, JwtCache};
21pub use claims::Claims;
22pub use hasura::{HasuraAuthConfig, HasuraIdentity, SecretOutcome, TokenRole};
23pub use jwks::Jwks;
24pub use jwt::validate_token;
25pub use keys::{JwtKeyError, JwtKeys, KeySet};
26
27use serde::{Deserialize, Serialize};
28use std::collections::HashMap;
29
30#[derive(Clone, Debug, Serialize, Deserialize)]
32pub struct AuthResult {
33 pub role: String,
35 pub claims: HashMap<String, serde_json::Value>,
37}
38
39impl AuthResult {
40 pub fn anonymous(anon_role: &str) -> Self {
42 Self {
43 role: anon_role.to_string(),
44 claims: HashMap::new(),
45 }
46 }
47
48 pub fn get_claim(&self, key: &str) -> Option<&serde_json::Value> {
50 self.claims.get(key)
51 }
52
53 pub fn claims_json(&self) -> String {
55 serde_json::to_string(&self.claims).unwrap_or_else(|_| "{}".to_string())
56 }
57
58 pub fn claim_settings(&self) -> Vec<(String, String)> {
64 let mut claims = Claims::from(self.claims.clone());
65 claims.set("role", serde_json::Value::String(self.role.clone()));
66 let mut settings = vec![("request.jwt.claims".to_string(), claims.to_json())];
67 settings.extend(claims.prefixed_entries("request.jwt.claims."));
68 settings
69 }
70}
71
72#[derive(Clone, Debug)]
78#[non_exhaustive]
79pub struct JwtConfig {
80 pub keys: Option<JwtKeys>,
85 pub audience: Option<String>,
87 pub issuer: Option<String>,
90 pub role_claim_key: String,
92 pub anon_role: Option<String>,
94 pub jwks: Option<Jwks>,
99}
100
101impl Default for JwtConfig {
102 fn default() -> Self {
103 Self {
104 keys: None,
105 audience: None,
106 issuer: None,
107 role_claim_key: "role".to_string(),
108 anon_role: None,
109 jwks: None,
110 }
111 }
112}
113
114impl JwtConfig {
115 pub fn with_jwks(mut self, jwks: Jwks) -> Self {
118 self.keys = Some(jwks.keys().clone());
119 self.jwks = Some(jwks);
120 self
121 }
122}
123
124pub async fn ensure_keys(auth_header: Option<&str>, config: &JwtConfig) {
137 let Some(jwks) = &config.jwks else {
138 return;
139 };
140 let Some(Ok(token)) = auth_header.map(extract_bearer_token) else {
141 return;
142 };
143 jwks.ensure_for_token(token).await;
144}
145
146#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
154pub enum ClaimFault {
155 #[error("JWT expired")]
157 Expired,
158
159 #[error("JWT not yet valid")]
161 NotYetValid,
162
163 #[error("JWT issued at future")]
166 IssuedInFuture,
167
168 #[error("JWT not in audience")]
170 NotInAudience,
171
172 #[error("JWT issuer is not the expected one")]
174 UnexpectedIssuer,
175
176 #[error("Parsing claims failed")]
178 Unparsable,
179
180 #[error("The JWT '{0}' claim must be a number")]
182 NotANumber(&'static str),
183
184 #[error("The JWT 'aud' claim must be a string or an array of strings")]
186 AudienceNotStrings,
187}
188
189#[derive(Debug, thiserror::Error)]
191pub enum JwtError {
192 #[error("Anonymous access is disabled")]
194 NoIdentity,
195
196 #[error("Unsupported token type")]
198 InvalidHeaderFormat,
199
200 #[error("Server lacks JWT secret")]
202 SecretMissing,
203
204 #[error("JWT cryptographic operation failed")]
206 InvalidSignature,
207
208 #[error("No suitable key or wrong key type")]
210 NoSuitableKey,
211
212 #[error("{0}")]
214 Claim(#[from] ClaimFault),
215}
216
217pub fn authenticate(auth_header: Option<&str>, config: &JwtConfig) -> Result<AuthResult, JwtError> {
219 let token = match auth_header {
221 Some(header) => extract_bearer_token(header)?,
222 None => {
223 return match &config.anon_role {
224 Some(role) => Ok(AuthResult::anonymous(role)),
225 None => Err(JwtError::NoIdentity),
226 };
227 }
228 };
229
230 validate_token(token, config)
232}
233
234fn extract_bearer_token(header: &str) -> Result<&str, JwtError> {
236 let header = header.trim();
237
238 if let Some(token) = header.strip_prefix("Bearer ") {
239 Ok(token.trim())
240 } else if let Some(token) = header.strip_prefix("bearer ") {
241 Ok(token.trim())
242 } else {
243 Err(JwtError::InvalidHeaderFormat)
244 }
245}
246
247#[cfg(test)]
248mod tests {
249 use super::*;
250
251 #[test]
252 fn test_extract_bearer_token() {
253 assert_eq!(extract_bearer_token("Bearer abc123").unwrap(), "abc123");
254 assert_eq!(extract_bearer_token("bearer abc123").unwrap(), "abc123");
255 assert!(extract_bearer_token("Basic abc123").is_err());
256 }
257
258 #[test]
259 fn test_auth_result_anonymous() {
260 let result = AuthResult::anonymous("anon");
261 assert_eq!(result.role, "anon");
262 assert!(result.claims.is_empty());
263 }
264
265 #[test]
266 fn auth_result_claim_settings_include_json_and_individual_claims() {
267 let result = AuthResult {
268 role: "authenticated_user".to_string(),
269 claims: HashMap::from([
270 ("role".to_string(), serde_json::json!("untrusted_role")),
271 ("user_id".to_string(), serde_json::json!("user-1")),
272 ("org_class".to_string(), serde_json::json!(["defense"])),
273 ]),
274 };
275
276 let settings = result.claim_settings();
277 let setting = |name: &str| {
278 settings
279 .iter()
280 .find(|(key, _)| key == name)
281 .map(|(_, value)| value.as_str())
282 };
283 let claims: serde_json::Value =
284 serde_json::from_str(setting("request.jwt.claims").unwrap()).unwrap();
285
286 assert_eq!(claims["role"], serde_json::json!("authenticated_user"));
287 assert_eq!(
288 setting("request.jwt.claims.role"),
289 Some("authenticated_user")
290 );
291 assert_eq!(setting("request.jwt.claims.user_id"), Some("user-1"));
292 assert_eq!(
293 setting("request.jwt.claims.org_class"),
294 Some(r#"["defense"]"#)
295 );
296 }
297
298 #[test]
299 fn test_authenticate_no_header_with_anon() {
300 let config = JwtConfig {
301 anon_role: Some("web_anon".to_string()),
302 ..Default::default()
303 };
304
305 let result = authenticate(None, &config).unwrap();
306 assert_eq!(result.role, "web_anon");
307 }
308
309 #[test]
310 fn test_authenticate_no_header_no_anon() {
311 let config = JwtConfig::default();
312 assert!(authenticate(None, &config).is_err());
313 }
314}