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;
16mod jwt;
17
18pub use cache::JwtCache;
19pub use claims::Claims;
20pub use hasura::{HasuraAuthConfig, HasuraIdentity, SecretOutcome, TokenRole};
21pub use jwt::validate_token;
22
23use serde::{Deserialize, Serialize};
24use std::collections::HashMap;
25
26#[derive(Clone, Debug, Serialize, Deserialize)]
28pub struct AuthResult {
29 pub role: String,
31 pub claims: HashMap<String, serde_json::Value>,
33}
34
35impl AuthResult {
36 pub fn anonymous(anon_role: &str) -> Self {
38 Self {
39 role: anon_role.to_string(),
40 claims: HashMap::new(),
41 }
42 }
43
44 pub fn get_claim(&self, key: &str) -> Option<&serde_json::Value> {
46 self.claims.get(key)
47 }
48
49 pub fn claims_json(&self) -> String {
51 serde_json::to_string(&self.claims).unwrap_or_else(|_| "{}".to_string())
52 }
53
54 pub fn claim_settings(&self) -> Vec<(String, String)> {
60 let mut claims = Claims::from(self.claims.clone());
61 claims.set("role", serde_json::Value::String(self.role.clone()));
62 let mut settings = vec![("request.jwt.claims".to_string(), claims.to_json())];
63 settings.extend(claims.prefixed_entries("request.jwt.claims."));
64 settings
65 }
66}
67
68#[derive(Clone, Debug)]
70pub struct JwtConfig {
71 pub secret: Option<String>,
73 pub secret_is_base64: bool,
75 pub audience: Option<String>,
77 pub role_claim_key: String,
79 pub anon_role: Option<String>,
81}
82
83impl Default for JwtConfig {
84 fn default() -> Self {
85 Self {
86 secret: None,
87 secret_is_base64: false,
88 audience: None,
89 role_claim_key: "role".to_string(),
90 anon_role: None,
91 }
92 }
93}
94
95#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
103pub enum ClaimFault {
104 #[error("JWT expired")]
106 Expired,
107
108 #[error("JWT not yet valid")]
110 NotYetValid,
111
112 #[error("JWT issued at future")]
115 IssuedInFuture,
116
117 #[error("JWT not in audience")]
119 NotInAudience,
120
121 #[error("Parsing claims failed")]
123 Unparsable,
124
125 #[error("The JWT '{0}' claim must be a number")]
127 NotANumber(&'static str),
128
129 #[error("The JWT 'aud' claim must be a string or an array of strings")]
131 AudienceNotStrings,
132}
133
134#[derive(Debug, thiserror::Error)]
136pub enum JwtError {
137 #[error("Anonymous access is disabled")]
139 NoIdentity,
140
141 #[error("Unsupported token type")]
143 InvalidHeaderFormat,
144
145 #[error("Server lacks JWT secret")]
147 SecretMissing,
148
149 #[error("JWT cryptographic operation failed")]
151 InvalidSignature,
152
153 #[error("No suitable key or wrong key type")]
155 NoSuitableKey,
156
157 #[error("{0}")]
159 Claim(#[from] ClaimFault),
160}
161
162pub fn authenticate(auth_header: Option<&str>, config: &JwtConfig) -> Result<AuthResult, JwtError> {
164 let token = match auth_header {
166 Some(header) => extract_bearer_token(header)?,
167 None => {
168 return match &config.anon_role {
169 Some(role) => Ok(AuthResult::anonymous(role)),
170 None => Err(JwtError::NoIdentity),
171 };
172 }
173 };
174
175 validate_token(token, config)
177}
178
179fn extract_bearer_token(header: &str) -> Result<&str, JwtError> {
181 let header = header.trim();
182
183 if let Some(token) = header.strip_prefix("Bearer ") {
184 Ok(token.trim())
185 } else if let Some(token) = header.strip_prefix("bearer ") {
186 Ok(token.trim())
187 } else {
188 Err(JwtError::InvalidHeaderFormat)
189 }
190}
191
192#[cfg(test)]
193mod tests {
194 use super::*;
195
196 #[test]
197 fn test_extract_bearer_token() {
198 assert_eq!(extract_bearer_token("Bearer abc123").unwrap(), "abc123");
199 assert_eq!(extract_bearer_token("bearer abc123").unwrap(), "abc123");
200 assert!(extract_bearer_token("Basic abc123").is_err());
201 }
202
203 #[test]
204 fn test_auth_result_anonymous() {
205 let result = AuthResult::anonymous("anon");
206 assert_eq!(result.role, "anon");
207 assert!(result.claims.is_empty());
208 }
209
210 #[test]
211 fn auth_result_claim_settings_include_json_and_individual_claims() {
212 let result = AuthResult {
213 role: "authenticated_user".to_string(),
214 claims: HashMap::from([
215 ("role".to_string(), serde_json::json!("untrusted_role")),
216 ("user_id".to_string(), serde_json::json!("user-1")),
217 ("org_class".to_string(), serde_json::json!(["defense"])),
218 ]),
219 };
220
221 let settings = result.claim_settings();
222 let setting = |name: &str| {
223 settings
224 .iter()
225 .find(|(key, _)| key == name)
226 .map(|(_, value)| value.as_str())
227 };
228 let claims: serde_json::Value =
229 serde_json::from_str(setting("request.jwt.claims").unwrap()).unwrap();
230
231 assert_eq!(claims["role"], serde_json::json!("authenticated_user"));
232 assert_eq!(
233 setting("request.jwt.claims.role"),
234 Some("authenticated_user")
235 );
236 assert_eq!(setting("request.jwt.claims.user_id"), Some("user-1"));
237 assert_eq!(
238 setting("request.jwt.claims.org_class"),
239 Some(r#"["defense"]"#)
240 );
241 }
242
243 #[test]
244 fn test_authenticate_no_header_with_anon() {
245 let config = JwtConfig {
246 anon_role: Some("web_anon".to_string()),
247 ..Default::default()
248 };
249
250 let result = authenticate(None, &config).unwrap();
251 assert_eq!(result.role, "web_anon");
252 }
253
254 #[test]
255 fn test_authenticate_no_header_no_anon() {
256 let config = JwtConfig::default();
257 assert!(authenticate(None, &config).is_err());
258 }
259}