1#![warn(missing_docs)]
6
7mod cache;
8mod claims;
9pub mod hasura;
10mod jwt;
11
12pub use cache::JwtCache;
13pub use claims::Claims;
14pub use hasura::{HasuraAuthConfig, HasuraIdentity, SecretOutcome, TokenRole};
15pub use jwt::validate_token;
16
17use serde::{Deserialize, Serialize};
18use std::collections::HashMap;
19
20#[derive(Clone, Debug, Serialize, Deserialize)]
22pub struct AuthResult {
23 pub role: String,
25 pub claims: HashMap<String, serde_json::Value>,
27}
28
29impl AuthResult {
30 pub fn anonymous(anon_role: &str) -> Self {
32 Self {
33 role: anon_role.to_string(),
34 claims: HashMap::new(),
35 }
36 }
37
38 pub fn get_claim(&self, key: &str) -> Option<&serde_json::Value> {
40 self.claims.get(key)
41 }
42
43 pub fn claims_json(&self) -> String {
45 serde_json::to_string(&self.claims).unwrap_or_else(|_| "{}".to_string())
46 }
47
48 pub fn claim_settings(&self) -> Vec<(String, String)> {
54 let mut claims = Claims::from(self.claims.clone());
55 claims.set("role", serde_json::Value::String(self.role.clone()));
56 let mut settings = vec![("request.jwt.claims".to_string(), claims.to_json())];
57 settings.extend(claims.prefixed_entries("request.jwt.claims."));
58 settings
59 }
60}
61
62#[derive(Clone, Debug)]
64pub struct JwtConfig {
65 pub secret: Option<String>,
67 pub secret_is_base64: bool,
69 pub audience: Option<String>,
71 pub role_claim_key: String,
73 pub anon_role: Option<String>,
75}
76
77impl Default for JwtConfig {
78 fn default() -> Self {
79 Self {
80 secret: None,
81 secret_is_base64: false,
82 audience: None,
83 role_claim_key: "role".to_string(),
84 anon_role: None,
85 }
86 }
87}
88
89#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
97pub enum ClaimFault {
98 #[error("JWT expired")]
100 Expired,
101
102 #[error("JWT not yet valid")]
104 NotYetValid,
105
106 #[error("JWT issued at future")]
109 IssuedInFuture,
110
111 #[error("JWT not in audience")]
113 NotInAudience,
114
115 #[error("Parsing claims failed")]
117 Unparsable,
118
119 #[error("The JWT '{0}' claim must be a number")]
121 NotANumber(&'static str),
122
123 #[error("The JWT 'aud' claim must be a string or an array of strings")]
125 AudienceNotStrings,
126}
127
128#[derive(Debug, thiserror::Error)]
130pub enum JwtError {
131 #[error("Anonymous access is disabled")]
133 NoIdentity,
134
135 #[error("Unsupported token type")]
137 InvalidHeaderFormat,
138
139 #[error("Server lacks JWT secret")]
141 SecretMissing,
142
143 #[error("JWT cryptographic operation failed")]
145 InvalidSignature,
146
147 #[error("No suitable key or wrong key type")]
149 NoSuitableKey,
150
151 #[error("{0}")]
153 Claim(#[from] ClaimFault),
154}
155
156pub fn authenticate(auth_header: Option<&str>, config: &JwtConfig) -> Result<AuthResult, JwtError> {
158 let token = match auth_header {
160 Some(header) => extract_bearer_token(header)?,
161 None => {
162 return match &config.anon_role {
163 Some(role) => Ok(AuthResult::anonymous(role)),
164 None => Err(JwtError::NoIdentity),
165 };
166 }
167 };
168
169 validate_token(token, config)
171}
172
173fn extract_bearer_token(header: &str) -> Result<&str, JwtError> {
175 let header = header.trim();
176
177 if let Some(token) = header.strip_prefix("Bearer ") {
178 Ok(token.trim())
179 } else if let Some(token) = header.strip_prefix("bearer ") {
180 Ok(token.trim())
181 } else {
182 Err(JwtError::InvalidHeaderFormat)
183 }
184}
185
186#[cfg(test)]
187mod tests {
188 use super::*;
189
190 #[test]
191 fn test_extract_bearer_token() {
192 assert_eq!(extract_bearer_token("Bearer abc123").unwrap(), "abc123");
193 assert_eq!(extract_bearer_token("bearer abc123").unwrap(), "abc123");
194 assert!(extract_bearer_token("Basic abc123").is_err());
195 }
196
197 #[test]
198 fn test_auth_result_anonymous() {
199 let result = AuthResult::anonymous("anon");
200 assert_eq!(result.role, "anon");
201 assert!(result.claims.is_empty());
202 }
203
204 #[test]
205 fn auth_result_claim_settings_include_json_and_individual_claims() {
206 let result = AuthResult {
207 role: "authenticated_user".to_string(),
208 claims: HashMap::from([
209 ("role".to_string(), serde_json::json!("untrusted_role")),
210 ("user_id".to_string(), serde_json::json!("user-1")),
211 ("org_class".to_string(), serde_json::json!(["defense"])),
212 ]),
213 };
214
215 let settings = result.claim_settings();
216 let setting = |name: &str| {
217 settings
218 .iter()
219 .find(|(key, _)| key == name)
220 .map(|(_, value)| value.as_str())
221 };
222 let claims: serde_json::Value =
223 serde_json::from_str(setting("request.jwt.claims").unwrap()).unwrap();
224
225 assert_eq!(claims["role"], serde_json::json!("authenticated_user"));
226 assert_eq!(
227 setting("request.jwt.claims.role"),
228 Some("authenticated_user")
229 );
230 assert_eq!(setting("request.jwt.claims.user_id"), Some("user-1"));
231 assert_eq!(
232 setting("request.jwt.claims.org_class"),
233 Some(r#"["defense"]"#)
234 );
235 }
236
237 #[test]
238 fn test_authenticate_no_header_with_anon() {
239 let config = JwtConfig {
240 anon_role: Some("web_anon".to_string()),
241 ..Default::default()
242 };
243
244 let result = authenticate(None, &config).unwrap();
245 assert_eq!(result.role, "web_anon");
246 }
247
248 #[test]
249 fn test_authenticate_no_header_no_anon() {
250 let config = JwtConfig::default();
251 assert!(authenticate(None, &config).is_err());
252 }
253}