rustkernel_core/security/
auth.rs1use serde::{Deserialize, Serialize};
6use std::time::Duration;
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct AuthConfig {
11 pub provider: AuthProviderType,
13 pub token_expiration: Duration,
15 pub allow_anonymous: bool,
17 pub required_claims: Vec<String>,
19}
20
21impl Default for AuthConfig {
22 fn default() -> Self {
23 Self {
24 provider: AuthProviderType::Jwt {
25 secret: String::new(),
26 issuer: None,
27 audience: None,
28 },
29 token_expiration: Duration::from_secs(3600),
30 allow_anonymous: true,
31 required_claims: Vec::new(),
32 }
33 }
34}
35
36impl AuthConfig {
37 pub fn jwt(secret: impl Into<String>) -> Self {
39 Self {
40 provider: AuthProviderType::Jwt {
41 secret: secret.into(),
42 issuer: None,
43 audience: None,
44 },
45 ..Default::default()
46 }
47 }
48
49 pub fn with_expiration(mut self, duration: Duration) -> Self {
51 self.token_expiration = duration;
52 self
53 }
54
55 pub fn require_auth(mut self) -> Self {
57 self.allow_anonymous = false;
58 self
59 }
60
61 pub fn require_claim(mut self, claim: impl Into<String>) -> Self {
63 self.required_claims.push(claim.into());
64 self
65 }
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize)]
70#[serde(tag = "type", rename_all = "snake_case")]
71pub enum AuthProviderType {
72 Jwt {
74 secret: String,
76 issuer: Option<String>,
78 audience: Option<String>,
80 },
81 OAuth {
83 discovery_url: String,
85 client_id: String,
87 },
88 ApiKey {
90 header: String,
92 },
93 None,
95}
96
97pub trait AuthProvider: Send + Sync {
99 fn validate(&self, token: &str) -> Result<TokenClaims, super::SecurityError>;
101
102 fn generate_token(&self, claims: &TokenClaims) -> Result<String, super::SecurityError>;
104}
105
106#[derive(Debug, Clone)]
108pub struct AuthToken {
109 pub raw: String,
111 pub claims: TokenClaims,
113}
114
115impl AuthToken {
116 pub fn new(raw: impl Into<String>, claims: TokenClaims) -> Self {
118 Self {
119 raw: raw.into(),
120 claims,
121 }
122 }
123
124 pub fn user_id(&self) -> Option<&str> {
126 self.claims.sub.as_deref()
127 }
128
129 pub fn tenant_id(&self) -> Option<&str> {
131 self.claims.tenant_id.as_deref()
132 }
133
134 pub fn is_expired(&self) -> bool {
136 if let Some(exp) = self.claims.exp {
137 chrono::Utc::now().timestamp() as u64 > exp
138 } else {
139 false
140 }
141 }
142}
143
144#[derive(Debug, Clone, Serialize, Deserialize)]
146pub struct TokenClaims {
147 pub sub: Option<String>,
149 pub iss: Option<String>,
151 pub aud: Option<String>,
153 pub exp: Option<u64>,
155 pub iat: Option<u64>,
157 pub nbf: Option<u64>,
159 pub jti: Option<String>,
161 pub tenant_id: Option<String>,
163 pub roles: Vec<String>,
165 pub permissions: Vec<String>,
167 #[serde(flatten)]
169 pub extra: std::collections::HashMap<String, serde_json::Value>,
170}
171
172impl Default for TokenClaims {
173 fn default() -> Self {
174 Self {
175 sub: None,
176 iss: None,
177 aud: None,
178 exp: None,
179 iat: Some(chrono::Utc::now().timestamp() as u64),
180 nbf: None,
181 jti: None,
182 tenant_id: None,
183 roles: Vec::new(),
184 permissions: Vec::new(),
185 extra: std::collections::HashMap::new(),
186 }
187 }
188}
189
190impl TokenClaims {
191 pub fn for_user(user_id: impl Into<String>) -> Self {
193 Self {
194 sub: Some(user_id.into()),
195 ..Default::default()
196 }
197 }
198
199 pub fn expires_in(mut self, duration: Duration) -> Self {
201 let now = chrono::Utc::now().timestamp() as u64;
202 self.exp = Some(now + duration.as_secs());
203 self
204 }
205
206 pub fn for_tenant(mut self, tenant_id: impl Into<String>) -> Self {
208 self.tenant_id = Some(tenant_id.into());
209 self
210 }
211
212 pub fn with_role(mut self, role: impl Into<String>) -> Self {
214 self.roles.push(role.into());
215 self
216 }
217
218 pub fn with_permission(mut self, permission: impl Into<String>) -> Self {
220 self.permissions.push(permission.into());
221 self
222 }
223
224 pub fn with_claim(mut self, key: impl Into<String>, value: impl Serialize) -> Self {
226 if let Ok(json_value) = serde_json::to_value(value) {
227 self.extra.insert(key.into(), json_value);
228 }
229 self
230 }
231}
232
233#[cfg(test)]
234mod tests {
235 use super::*;
236
237 #[test]
238 fn test_auth_config() {
239 let config = AuthConfig::jwt("my-secret")
240 .with_expiration(Duration::from_secs(7200))
241 .require_auth();
242
243 assert!(!config.allow_anonymous);
244 assert_eq!(config.token_expiration, Duration::from_secs(7200));
245 }
246
247 #[test]
248 fn test_token_claims() {
249 let claims = TokenClaims::for_user("user-123")
250 .for_tenant("tenant-456")
251 .with_role("admin")
252 .expires_in(Duration::from_secs(3600));
253
254 assert_eq!(claims.sub.as_deref(), Some("user-123"));
255 assert_eq!(claims.tenant_id.as_deref(), Some("tenant-456"));
256 assert!(claims.roles.contains(&"admin".to_string()));
257 assert!(claims.exp.is_some());
258 }
259
260 #[test]
261 fn test_auth_token() {
262 let claims = TokenClaims::for_user("user-123");
263 let token = AuthToken::new("raw-token-string", claims);
264
265 assert_eq!(token.user_id(), Some("user-123"));
266 assert!(!token.is_expired());
267 }
268}