1mod claims;
6mod jwt;
7
8pub use claims::Claims;
9pub use jwt::validate_token;
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(auth_header: Option<&str>, config: &JwtConfig) -> Result<AuthResult, JwtError> {
100 let token = match auth_header {
102 Some(header) => extract_bearer_token(header)?,
103 None => {
104 return match &config.anon_role {
105 Some(role) => Ok(AuthResult::anonymous(role)),
106 None => Err(JwtError::MissingHeader),
107 };
108 }
109 };
110
111 validate_token(token, config)
113}
114
115fn extract_bearer_token(header: &str) -> Result<&str, JwtError> {
117 let header = header.trim();
118
119 if let Some(token) = header.strip_prefix("Bearer ") {
120 Ok(token.trim())
121 } else if let Some(token) = header.strip_prefix("bearer ") {
122 Ok(token.trim())
123 } else {
124 Err(JwtError::InvalidHeaderFormat)
125 }
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131
132 #[test]
133 fn test_extract_bearer_token() {
134 assert_eq!(extract_bearer_token("Bearer abc123").unwrap(), "abc123");
135 assert_eq!(extract_bearer_token("bearer abc123").unwrap(), "abc123");
136 assert!(extract_bearer_token("Basic abc123").is_err());
137 }
138
139 #[test]
140 fn test_auth_result_anonymous() {
141 let result = AuthResult::anonymous("anon");
142 assert_eq!(result.role, "anon");
143 assert!(result.claims.is_empty());
144 }
145
146 #[test]
147 fn test_authenticate_no_header_with_anon() {
148 let config = JwtConfig {
149 anon_role: Some("web_anon".to_string()),
150 ..Default::default()
151 };
152
153 let result = authenticate(None, &config).unwrap();
154 assert_eq!(result.role, "web_anon");
155 }
156
157 #[test]
158 fn test_authenticate_no_header_no_anon() {
159 let config = JwtConfig::default();
160 assert!(authenticate(None, &config).is_err());
161 }
162}