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
49#[derive(Clone, Debug)]
51pub struct JwtConfig {
52 pub secret: Option<String>,
54 pub secret_is_base64: bool,
56 pub audience: Option<String>,
58 pub role_claim_key: String,
60 pub anon_role: Option<String>,
62}
63
64impl Default for JwtConfig {
65 fn default() -> Self {
66 Self {
67 secret: None,
68 secret_is_base64: false,
69 audience: None,
70 role_claim_key: "role".to_string(),
71 anon_role: None,
72 }
73 }
74}
75
76#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
84pub enum ClaimFault {
85 #[error("JWT expired")]
87 Expired,
88
89 #[error("JWT not yet valid")]
91 NotYetValid,
92
93 #[error("JWT issued at future")]
96 IssuedInFuture,
97
98 #[error("JWT not in audience")]
100 NotInAudience,
101
102 #[error("Parsing claims failed")]
104 Unparsable,
105
106 #[error("The JWT '{0}' claim must be a number")]
108 NotANumber(&'static str),
109
110 #[error("The JWT 'aud' claim must be a string or an array of strings")]
112 AudienceNotStrings,
113}
114
115#[derive(Debug, thiserror::Error)]
117pub enum JwtError {
118 #[error("Anonymous access is disabled")]
120 NoIdentity,
121
122 #[error("Unsupported token type")]
124 InvalidHeaderFormat,
125
126 #[error("Server lacks JWT secret")]
128 SecretMissing,
129
130 #[error("JWT cryptographic operation failed")]
132 InvalidSignature,
133
134 #[error("No suitable key or wrong key type")]
136 NoSuitableKey,
137
138 #[error("{0}")]
140 Claim(#[from] ClaimFault),
141}
142
143pub fn authenticate(auth_header: Option<&str>, config: &JwtConfig) -> Result<AuthResult, JwtError> {
145 let token = match auth_header {
147 Some(header) => extract_bearer_token(header)?,
148 None => {
149 return match &config.anon_role {
150 Some(role) => Ok(AuthResult::anonymous(role)),
151 None => Err(JwtError::NoIdentity),
152 };
153 }
154 };
155
156 validate_token(token, config)
158}
159
160fn extract_bearer_token(header: &str) -> Result<&str, JwtError> {
162 let header = header.trim();
163
164 if let Some(token) = header.strip_prefix("Bearer ") {
165 Ok(token.trim())
166 } else if let Some(token) = header.strip_prefix("bearer ") {
167 Ok(token.trim())
168 } else {
169 Err(JwtError::InvalidHeaderFormat)
170 }
171}
172
173#[cfg(test)]
174mod tests {
175 use super::*;
176
177 #[test]
178 fn test_extract_bearer_token() {
179 assert_eq!(extract_bearer_token("Bearer abc123").unwrap(), "abc123");
180 assert_eq!(extract_bearer_token("bearer abc123").unwrap(), "abc123");
181 assert!(extract_bearer_token("Basic abc123").is_err());
182 }
183
184 #[test]
185 fn test_auth_result_anonymous() {
186 let result = AuthResult::anonymous("anon");
187 assert_eq!(result.role, "anon");
188 assert!(result.claims.is_empty());
189 }
190
191 #[test]
192 fn test_authenticate_no_header_with_anon() {
193 let config = JwtConfig {
194 anon_role: Some("web_anon".to_string()),
195 ..Default::default()
196 };
197
198 let result = authenticate(None, &config).unwrap();
199 assert_eq!(result.role, "web_anon");
200 }
201
202 #[test]
203 fn test_authenticate_no_header_no_anon() {
204 let config = JwtConfig::default();
205 assert!(authenticate(None, &config).is_err());
206 }
207}