1mod claims;
6pub mod hasura;
7mod jwt;
8
9pub use claims::Claims;
10pub use hasura::{HasuraAuthConfig, HasuraIdentity, SecretOutcome, TokenRole};
11pub use jwt::validate_token;
12
13use serde::{Deserialize, Serialize};
14use std::collections::HashMap;
15
16#[derive(Clone, Debug, Serialize, Deserialize)]
18pub struct AuthResult {
19 pub role: String,
21 pub claims: HashMap<String, serde_json::Value>,
23}
24
25impl AuthResult {
26 pub fn anonymous(anon_role: &str) -> Self {
28 Self {
29 role: anon_role.to_string(),
30 claims: HashMap::new(),
31 }
32 }
33
34 pub fn get_claim(&self, key: &str) -> Option<&serde_json::Value> {
36 self.claims.get(key)
37 }
38
39 pub fn claims_json(&self) -> String {
41 serde_json::to_string(&self.claims).unwrap_or_else(|_| "{}".to_string())
42 }
43}
44
45#[derive(Clone, Debug)]
47pub struct JwtConfig {
48 pub secret: Option<String>,
50 pub secret_is_base64: bool,
52 pub audience: Option<String>,
54 pub role_claim_key: String,
56 pub anon_role: Option<String>,
58}
59
60impl Default for JwtConfig {
61 fn default() -> Self {
62 Self {
63 secret: None,
64 secret_is_base64: false,
65 audience: None,
66 role_claim_key: "role".to_string(),
67 anon_role: None,
68 }
69 }
70}
71
72#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
80pub enum ClaimFault {
81 #[error("JWT expired")]
82 Expired,
83
84 #[error("JWT not yet valid")]
85 NotYetValid,
86
87 #[error("JWT issued at future")]
88 IssuedInFuture,
89
90 #[error("JWT not in audience")]
91 NotInAudience,
92
93 #[error("Parsing claims failed")]
94 Unparsable,
95
96 #[error("The JWT '{0}' claim must be a number")]
98 NotANumber(&'static str),
99
100 #[error("The JWT 'aud' claim must be a string or an array of strings")]
101 AudienceNotStrings,
102}
103
104#[derive(Debug, thiserror::Error)]
106pub enum JwtError {
107 #[error("Anonymous access is disabled")]
109 NoIdentity,
110
111 #[error("Unsupported token type")]
112 InvalidHeaderFormat,
113
114 #[error("Server lacks JWT secret")]
115 SecretMissing,
116
117 #[error("JWT cryptographic operation failed")]
118 InvalidSignature,
119
120 #[error("No suitable key or wrong key type")]
122 NoSuitableKey,
123
124 #[error("{0}")]
126 Claim(#[from] ClaimFault),
127}
128
129pub fn authenticate(auth_header: Option<&str>, config: &JwtConfig) -> Result<AuthResult, JwtError> {
131 let token = match auth_header {
133 Some(header) => extract_bearer_token(header)?,
134 None => {
135 return match &config.anon_role {
136 Some(role) => Ok(AuthResult::anonymous(role)),
137 None => Err(JwtError::NoIdentity),
138 };
139 }
140 };
141
142 validate_token(token, config)
144}
145
146fn extract_bearer_token(header: &str) -> Result<&str, JwtError> {
148 let header = header.trim();
149
150 if let Some(token) = header.strip_prefix("Bearer ") {
151 Ok(token.trim())
152 } else if let Some(token) = header.strip_prefix("bearer ") {
153 Ok(token.trim())
154 } else {
155 Err(JwtError::InvalidHeaderFormat)
156 }
157}
158
159#[cfg(test)]
160mod tests {
161 use super::*;
162
163 #[test]
164 fn test_extract_bearer_token() {
165 assert_eq!(extract_bearer_token("Bearer abc123").unwrap(), "abc123");
166 assert_eq!(extract_bearer_token("bearer abc123").unwrap(), "abc123");
167 assert!(extract_bearer_token("Basic abc123").is_err());
168 }
169
170 #[test]
171 fn test_auth_result_anonymous() {
172 let result = AuthResult::anonymous("anon");
173 assert_eq!(result.role, "anon");
174 assert!(result.claims.is_empty());
175 }
176
177 #[test]
178 fn test_authenticate_no_header_with_anon() {
179 let config = JwtConfig {
180 anon_role: Some("web_anon".to_string()),
181 ..Default::default()
182 };
183
184 let result = authenticate(None, &config).unwrap();
185 assert_eq!(result.role, "web_anon");
186 }
187
188 #[test]
189 fn test_authenticate_no_header_no_anon() {
190 let config = JwtConfig::default();
191 assert!(authenticate(None, &config).is_err());
192 }
193}