1mod jwt;
6mod claims;
7
8pub use jwt::validate_token;
9pub use claims::Claims;
10
11use serde::{Deserialize, Serialize};
12use std::collections::HashMap;
13
14#[derive(Clone, Debug, Serialize, Deserialize)]
16pub struct AuthResult {
17 pub role: String,
19 pub claims: HashMap<String, serde_json::Value>,
21}
22
23impl AuthResult {
24 pub fn anonymous(anon_role: &str) -> Self {
26 Self {
27 role: anon_role.to_string(),
28 claims: HashMap::new(),
29 }
30 }
31
32 pub fn get_claim(&self, key: &str) -> Option<&serde_json::Value> {
34 self.claims.get(key)
35 }
36
37 pub fn claims_json(&self) -> String {
39 serde_json::to_string(&self.claims).unwrap_or_else(|_| "{}".to_string())
40 }
41}
42
43#[derive(Clone, Debug)]
45pub struct JwtConfig {
46 pub secret: Option<String>,
48 pub secret_is_base64: bool,
50 pub audience: Option<String>,
52 pub role_claim_key: String,
54 pub anon_role: Option<String>,
56}
57
58impl Default for JwtConfig {
59 fn default() -> Self {
60 Self {
61 secret: None,
62 secret_is_base64: false,
63 audience: None,
64 role_claim_key: "role".to_string(),
65 anon_role: None,
66 }
67 }
68}
69
70#[derive(Debug, thiserror::Error)]
72pub enum JwtError {
73 #[error("Missing authorization header")]
74 MissingHeader,
75
76 #[error("Invalid authorization header format")]
77 InvalidHeaderFormat,
78
79 #[error("Token expired")]
80 Expired,
81
82 #[error("Token not yet valid")]
83 NotYetValid,
84
85 #[error("Invalid signature")]
86 InvalidSignature,
87
88 #[error("Invalid token: {0}")]
89 InvalidToken(String),
90
91 #[error("Missing role claim")]
92 MissingRole,
93
94 #[error("Invalid audience")]
95 InvalidAudience,
96}
97
98pub fn authenticate(
100 auth_header: Option<&str>,
101 config: &JwtConfig,
102) -> Result<AuthResult, JwtError> {
103 let token = match auth_header {
105 Some(header) => extract_bearer_token(header)?,
106 None => {
107 return match &config.anon_role {
108 Some(role) => Ok(AuthResult::anonymous(role)),
109 None => Err(JwtError::MissingHeader),
110 };
111 }
112 };
113
114 validate_token(token, config)
116}
117
118fn extract_bearer_token(header: &str) -> Result<&str, JwtError> {
120 let header = header.trim();
121
122 if let Some(token) = header.strip_prefix("Bearer ") {
123 Ok(token.trim())
124 } else if let Some(token) = header.strip_prefix("bearer ") {
125 Ok(token.trim())
126 } else {
127 Err(JwtError::InvalidHeaderFormat)
128 }
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134
135 #[test]
136 fn test_extract_bearer_token() {
137 assert_eq!(
138 extract_bearer_token("Bearer abc123").unwrap(),
139 "abc123"
140 );
141 assert_eq!(
142 extract_bearer_token("bearer abc123").unwrap(),
143 "abc123"
144 );
145 assert!(extract_bearer_token("Basic abc123").is_err());
146 }
147
148 #[test]
149 fn test_auth_result_anonymous() {
150 let result = AuthResult::anonymous("anon");
151 assert_eq!(result.role, "anon");
152 assert!(result.claims.is_empty());
153 }
154
155 #[test]
156 fn test_authenticate_no_header_with_anon() {
157 let config = JwtConfig {
158 anon_role: Some("web_anon".to_string()),
159 ..Default::default()
160 };
161
162 let result = authenticate(None, &config).unwrap();
163 assert_eq!(result.role, "web_anon");
164 }
165
166 #[test]
167 fn test_authenticate_no_header_no_anon() {
168 let config = JwtConfig::default();
169 assert!(authenticate(None, &config).is_err());
170 }
171}