allframe_core/auth/
mod.rs1use std::fmt;
57
58#[cfg(feature = "auth-jwt")]
59pub mod jwt;
60
61#[cfg(feature = "auth-axum")]
62pub mod axum;
63
64#[cfg(feature = "auth-tonic")]
65pub mod tonic;
66
67#[cfg(feature = "auth-jwt")]
69pub use jwt::{JwtAlgorithm, JwtConfig, JwtValidator};
70
71#[cfg(feature = "auth-axum")]
72pub use self::axum::{AuthLayer, AuthenticatedUser};
73
74#[cfg(feature = "auth-tonic")]
75pub use self::tonic::AuthInterceptor;
76
77#[derive(Debug, Clone)]
79pub enum AuthError {
80 MissingToken,
82 InvalidToken(String),
84 TokenExpired,
86 InvalidSignature,
88 InvalidIssuer,
90 InvalidAudience,
92 ValidationFailed(String),
94 Internal(String),
96}
97
98impl fmt::Display for AuthError {
99 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
100 match self {
101 AuthError::MissingToken => write!(f, "missing authentication token"),
102 AuthError::InvalidToken(msg) => write!(f, "invalid token: {}", msg),
103 AuthError::TokenExpired => write!(f, "token has expired"),
104 AuthError::InvalidSignature => write!(f, "invalid token signature"),
105 AuthError::InvalidIssuer => write!(f, "invalid token issuer"),
106 AuthError::InvalidAudience => write!(f, "invalid token audience"),
107 AuthError::ValidationFailed(msg) => write!(f, "validation failed: {}", msg),
108 AuthError::Internal(msg) => write!(f, "internal auth error: {}", msg),
109 }
110 }
111}
112
113impl std::error::Error for AuthError {}
114
115impl AuthError {
116 pub fn is_missing(&self) -> bool {
118 matches!(self, AuthError::MissingToken)
119 }
120
121 pub fn is_expired(&self) -> bool {
123 matches!(self, AuthError::TokenExpired)
124 }
125
126 pub fn status_code(&self) -> u16 {
128 match self {
129 AuthError::MissingToken => 401,
130 AuthError::InvalidToken(_) => 401,
131 AuthError::TokenExpired => 401,
132 AuthError::InvalidSignature => 401,
133 AuthError::InvalidIssuer => 401,
134 AuthError::InvalidAudience => 401,
135 AuthError::ValidationFailed(_) => 403,
136 AuthError::Internal(_) => 500,
137 }
138 }
139}
140
141#[async_trait::async_trait]
169pub trait Authenticator: Send + Sync {
170 type Claims: Clone + Send + Sync + 'static;
172
173 async fn authenticate(&self, token: &str) -> Result<Self::Claims, AuthError>;
182}
183
184#[derive(Clone, Debug)]
189pub struct AuthContext<C> {
190 pub claims: C,
192 pub token: String,
194}
195
196impl<C: Clone> AuthContext<C> {
197 pub fn new(claims: C, token: impl Into<String>) -> Self {
199 Self {
200 claims,
201 token: token.into(),
202 }
203 }
204
205 pub fn claims(&self) -> &C {
207 &self.claims
208 }
209
210 pub fn token(&self) -> &str {
212 &self.token
213 }
214
215 pub fn get<T>(&self, f: impl FnOnce(&C) -> T) -> T {
217 f(&self.claims)
218 }
219}
220
221pub fn extract_bearer_token(header_value: &str) -> Option<&str> {
234 let header = header_value.trim();
235 if header.len() > 7 && header[..7].eq_ignore_ascii_case("bearer ") {
236 Some(header[7..].trim())
237 } else {
238 None
239 }
240}
241
242pub trait HasSubject {
244 fn subject(&self) -> &str;
246}
247
248pub trait HasExpiration {
250 fn expiration(&self) -> Option<i64>;
252
253 fn is_expired(&self) -> bool {
255 if let Some(exp) = self.expiration() {
256 let now = std::time::SystemTime::now()
257 .duration_since(std::time::UNIX_EPOCH)
258 .unwrap()
259 .as_secs() as i64;
260 exp < now
261 } else {
262 false
263 }
264 }
265}
266
267#[cfg(test)]
268mod tests {
269 use super::*;
270
271 #[test]
272 fn test_extract_bearer_token() {
273 assert_eq!(extract_bearer_token("Bearer abc123"), Some("abc123"));
274 assert_eq!(extract_bearer_token("bearer ABC"), Some("ABC"));
275 assert_eq!(extract_bearer_token("BEARER token"), Some("token"));
276 assert_eq!(extract_bearer_token("Bearer spaced"), Some("spaced"));
277 assert_eq!(extract_bearer_token("Basic xyz"), None);
278 assert_eq!(extract_bearer_token("abc123"), None);
279 assert_eq!(extract_bearer_token(""), None);
280 assert_eq!(extract_bearer_token("Bearer"), None);
281 assert_eq!(extract_bearer_token("Bearer "), None);
283 }
284
285 #[test]
286 fn test_auth_error_display() {
287 assert_eq!(
288 AuthError::MissingToken.to_string(),
289 "missing authentication token"
290 );
291 assert_eq!(
292 AuthError::TokenExpired.to_string(),
293 "token has expired"
294 );
295 assert_eq!(
296 AuthError::InvalidToken("bad".into()).to_string(),
297 "invalid token: bad"
298 );
299 }
300
301 #[test]
302 fn test_auth_error_status_codes() {
303 assert_eq!(AuthError::MissingToken.status_code(), 401);
304 assert_eq!(AuthError::TokenExpired.status_code(), 401);
305 assert_eq!(AuthError::ValidationFailed("".into()).status_code(), 403);
306 assert_eq!(AuthError::Internal("".into()).status_code(), 500);
307 }
308
309 #[test]
310 fn test_auth_context() {
311 #[derive(Clone, Debug)]
312 struct TestClaims {
313 sub: String,
314 role: String,
315 }
316
317 let ctx = AuthContext::new(
318 TestClaims {
319 sub: "user123".into(),
320 role: "admin".into(),
321 },
322 "token123",
323 );
324
325 assert_eq!(ctx.claims().sub, "user123");
326 assert_eq!(ctx.token(), "token123");
327 assert_eq!(ctx.get(|c| c.role.clone()), "admin");
328 }
329
330 #[test]
331 fn test_auth_error_predicates() {
332 assert!(AuthError::MissingToken.is_missing());
333 assert!(!AuthError::TokenExpired.is_missing());
334 assert!(AuthError::TokenExpired.is_expired());
335 assert!(!AuthError::MissingToken.is_expired());
336 }
337
338 #[derive(Clone)]
339 struct MockClaims {
340 exp: Option<i64>,
341 }
342
343 impl HasExpiration for MockClaims {
344 fn expiration(&self) -> Option<i64> {
345 self.exp
346 }
347 }
348
349 #[test]
350 fn test_has_expiration() {
351 let past = MockClaims { exp: Some(0) };
352 assert!(past.is_expired());
353
354 let future = MockClaims {
355 exp: Some(i64::MAX),
356 };
357 assert!(!future.is_expired());
358
359 let no_exp = MockClaims { exp: None };
360 assert!(!no_exp.is_expired());
361 }
362}