rustkernel_core/security/
auth.rs

1//! Authentication
2//!
3//! JWT and OAuth authentication for kernel access.
4
5use serde::{Deserialize, Serialize};
6use std::time::Duration;
7
8/// Authentication configuration
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct AuthConfig {
11    /// Authentication provider type
12    pub provider: AuthProviderType,
13    /// Token expiration time
14    pub token_expiration: Duration,
15    /// Allow anonymous access
16    pub allow_anonymous: bool,
17    /// Required claims for valid tokens
18    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    /// Create JWT authentication config
38    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    /// Set token expiration
50    pub fn with_expiration(mut self, duration: Duration) -> Self {
51        self.token_expiration = duration;
52        self
53    }
54
55    /// Disable anonymous access
56    pub fn require_auth(mut self) -> Self {
57        self.allow_anonymous = false;
58        self
59    }
60
61    /// Add required claim
62    pub fn require_claim(mut self, claim: impl Into<String>) -> Self {
63        self.required_claims.push(claim.into());
64        self
65    }
66}
67
68/// Authentication provider type
69#[derive(Debug, Clone, Serialize, Deserialize)]
70#[serde(tag = "type", rename_all = "snake_case")]
71pub enum AuthProviderType {
72    /// JWT authentication
73    Jwt {
74        /// Secret key for HS256
75        secret: String,
76        /// Expected issuer
77        issuer: Option<String>,
78        /// Expected audience
79        audience: Option<String>,
80    },
81    /// OAuth2/OIDC
82    OAuth {
83        /// Discovery URL
84        discovery_url: String,
85        /// Client ID
86        client_id: String,
87    },
88    /// API Key
89    ApiKey {
90        /// Header name
91        header: String,
92    },
93    /// No authentication (development only)
94    None,
95}
96
97/// Authentication provider trait
98pub trait AuthProvider: Send + Sync {
99    /// Validate a token and extract claims
100    fn validate(&self, token: &str) -> Result<TokenClaims, super::SecurityError>;
101
102    /// Generate a new token for a user
103    fn generate_token(&self, claims: &TokenClaims) -> Result<String, super::SecurityError>;
104}
105
106/// Authentication token
107#[derive(Debug, Clone)]
108pub struct AuthToken {
109    /// Raw token string
110    pub raw: String,
111    /// Parsed claims
112    pub claims: TokenClaims,
113}
114
115impl AuthToken {
116    /// Create a new auth token
117    pub fn new(raw: impl Into<String>, claims: TokenClaims) -> Self {
118        Self {
119            raw: raw.into(),
120            claims,
121        }
122    }
123
124    /// Get the user ID from claims
125    pub fn user_id(&self) -> Option<&str> {
126        self.claims.sub.as_deref()
127    }
128
129    /// Get the tenant ID from claims
130    pub fn tenant_id(&self) -> Option<&str> {
131        self.claims.tenant_id.as_deref()
132    }
133
134    /// Check if token is expired
135    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/// JWT token claims
145#[derive(Debug, Clone, Serialize, Deserialize)]
146pub struct TokenClaims {
147    /// Subject (user ID)
148    pub sub: Option<String>,
149    /// Issuer
150    pub iss: Option<String>,
151    /// Audience
152    pub aud: Option<String>,
153    /// Expiration time (Unix timestamp)
154    pub exp: Option<u64>,
155    /// Issued at (Unix timestamp)
156    pub iat: Option<u64>,
157    /// Not before (Unix timestamp)
158    pub nbf: Option<u64>,
159    /// JWT ID
160    pub jti: Option<String>,
161    /// Tenant ID (custom claim)
162    pub tenant_id: Option<String>,
163    /// Roles (custom claim)
164    pub roles: Vec<String>,
165    /// Permissions (custom claim)
166    pub permissions: Vec<String>,
167    /// Additional custom claims
168    #[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    /// Create new claims for a user
192    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    /// Set expiration
200    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    /// Set tenant
207    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    /// Add role
213    pub fn with_role(mut self, role: impl Into<String>) -> Self {
214        self.roles.push(role.into());
215        self
216    }
217
218    /// Add permission
219    pub fn with_permission(mut self, permission: impl Into<String>) -> Self {
220        self.permissions.push(permission.into());
221        self
222    }
223
224    /// Add custom claim
225    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}