Skip to main content

reinhardt_auth/
lib.rs

1//! # Reinhardt Auth
2//!
3//! Authentication and authorization system for Reinhardt framework.
4//!
5//! ## Features
6//!
7//! - **DjangoModelPermissions**: Django-style model permissions with `app_label.action_model` format
8//! - **DjangoModelPermissionsOrAnonReadOnly**: Anonymous read access for unauthenticated users
9//! - **Object-Level Permissions**: Fine-grained access control on individual objects
10//! - **User Management**: CRUD operations for users with password hashing
11//! - **Group Management**: User groups and permission assignment
12//! - **REST API Authentication**: Multiple authentication backends (JWT, Token, Session, OAuth2)
13//! - **Standard Permissions**: Permission classes for common authorization scenarios
14//! - **createsuperuser Command**: CLI tool for creating admin users
15//!
16//! ## Security Note: Client-Side vs Server-Side Checks
17//!
18//! Authentication state exposed via [`reinhardt_http::AuthState`] (e.g.,
19//! `is_authenticated()`, `is_admin()`) is populated by server-side
20//! middleware and stored in request extensions. When this state is
21//! forwarded to client-side code (e.g., via WASM or JSON responses),
22//! **it must only be used for UI display purposes** (showing/hiding
23//! elements). All authorization decisions must be enforced server-side
24//! through middleware and permission classes provided by this crate.
25
26pub mod sessions;
27
28// Core authentication types and traits (migrated from reinhardt-core-auth)
29pub mod core;
30
31// CurrentUser injectable for dependency injection
32pub mod current_user;
33pub use current_user::CurrentUser;
34
35// Re-export core authentication types
36pub use core::{
37	AllowAny, AnonymousUser, AuthBackend, BaseUser, CompositeAuthBackend, FullUser, IsActiveUser,
38	IsAdminUser, IsAuthenticated, IsAuthenticatedOrReadOnly, PasswordHasher, Permission,
39	PermissionContext, PermissionsMixin, SimpleUser, User,
40};
41
42#[cfg(feature = "argon2-hasher")]
43pub use core::Argon2Hasher;
44
45// Re-export permission operators from core
46pub use core::permission_operators;
47
48// Implementation-specific modules (kept in contrib)
49pub mod advanced_permissions;
50pub mod base_user_manager;
51pub mod basic;
52pub mod default_user;
53pub mod default_user_manager;
54pub mod group_management;
55#[cfg(feature = "sessions")]
56pub mod handlers;
57pub mod ip_permission;
58#[cfg(feature = "jwt")]
59pub mod jwt;
60pub mod mfa;
61pub mod model_permissions;
62#[cfg(feature = "oauth")]
63pub mod oauth2;
64pub mod object_permissions;
65#[cfg(feature = "rate-limit")]
66pub mod rate_limit_permission;
67pub mod remote_user;
68pub mod rest_authentication;
69#[cfg(feature = "sessions")]
70pub mod session;
71#[cfg(feature = "social")]
72pub mod social;
73pub mod time_based_permission;
74#[cfg(any(feature = "jwt", feature = "token"))]
75pub mod token_blacklist;
76#[cfg(any(feature = "jwt", feature = "token"))]
77pub mod token_rotation;
78#[cfg(any(feature = "jwt", feature = "token"))]
79pub mod token_storage;
80pub mod user_management;
81
82pub use advanced_permissions::{ObjectPermission as AdvancedObjectPermission, RoleBasedPermission};
83pub use base_user_manager::BaseUserManager;
84pub use basic::BasicAuthentication as HttpBasicAuth;
85#[cfg(feature = "argon2-hasher")]
86pub use default_user::DefaultUser;
87#[cfg(feature = "argon2-hasher")]
88pub use default_user_manager::DefaultUserManager;
89pub use group_management::{
90	CreateGroupData, Group, GroupManagementError, GroupManagementResult, GroupManager,
91};
92#[cfg(feature = "sessions")]
93pub use handlers::{LoginCredentials, LoginHandler, LogoutHandler, SESSION_COOKIE_NAME};
94pub use ip_permission::{CidrRange, IpBlacklistPermission, IpWhitelistPermission};
95#[cfg(feature = "jwt")]
96pub use jwt::{Claims, JwtAuth};
97pub use mfa::MFAAuthentication as MfaManager;
98pub use model_permissions::{
99	DjangoModelPermissions, DjangoModelPermissionsOrAnonReadOnly, ModelPermission,
100};
101#[cfg(feature = "oauth")]
102pub use oauth2::{
103	AccessToken, AuthorizationCode, GrantType, InMemoryOAuth2Store, OAuth2Application,
104	OAuth2Authentication, OAuth2TokenStore, SimpleUserRepository, UserRepository,
105};
106pub use object_permissions::{ObjectPermission, ObjectPermissionChecker, ObjectPermissionManager};
107pub use permission_operators::{AndPermission, NotPermission, OrPermission};
108#[cfg(feature = "social")]
109pub use social::{
110	AppleProvider, GitHubProvider, GoogleProvider, IdToken, MicrosoftProvider, OAuthProvider,
111	OAuthToken, PkceFlow, ProviderConfig, SocialAuthBackend, SocialAuthError, StandardClaims,
112	StateStore, TokenResponse,
113};
114
115#[cfg(feature = "rate-limit")]
116pub use rate_limit_permission::{RateLimitPermission, RateLimitPermissionBuilder};
117pub use remote_user::RemoteUserAuthentication as RemoteUserAuth;
118pub use rest_authentication::{
119	BasicAuthConfig, CompositeAuthentication, RemoteUserAuthentication, RestAuthentication,
120	SessionAuthConfig, SessionAuthentication, TokenAuthConfig, TokenAuthentication,
121};
122#[cfg(feature = "sessions")]
123pub use session::{InMemorySessionStore, SESSION_KEY_USER_ID, Session, SessionId, SessionStore};
124pub use time_based_permission::{DateRange, TimeBasedPermission, TimeWindow};
125#[cfg(any(feature = "jwt", feature = "token"))]
126pub use token_blacklist::{
127	BlacklistReason, BlacklistStats, BlacklistedToken, InMemoryRefreshTokenStore,
128	InMemoryTokenBlacklist, RefreshToken, RefreshTokenStore, TokenBlacklist, TokenRotationManager,
129};
130#[cfg(any(feature = "jwt", feature = "token"))]
131pub use token_rotation::{AutoTokenRotationManager, TokenRotationConfig, TokenRotationRecord};
132#[cfg(feature = "database")]
133pub use token_storage::DatabaseTokenStorage;
134#[cfg(any(feature = "jwt", feature = "token"))]
135pub use token_storage::{
136	InMemoryTokenStorage, StoredToken, TokenStorage, TokenStorageError, TokenStorageResult,
137};
138pub use user_management::{
139	CreateUserData, UpdateUserData, UserManagementError, UserManagementResult, UserManager,
140};
141
142/// Authentication errors
143#[derive(Debug, Clone)]
144pub enum AuthenticationError {
145	InvalidCredentials,
146	UserNotFound,
147	SessionExpired,
148	InvalidToken,
149	NotAuthenticated,
150	DatabaseError(String),
151	Unknown(String),
152}
153
154impl std::fmt::Display for AuthenticationError {
155	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
156		match self {
157			AuthenticationError::InvalidCredentials => write!(f, "Invalid credentials"),
158			AuthenticationError::UserNotFound => write!(f, "User not found"),
159			AuthenticationError::SessionExpired => write!(f, "Session expired"),
160			AuthenticationError::InvalidToken => write!(f, "Invalid token"),
161			AuthenticationError::NotAuthenticated => write!(f, "User is not authenticated"),
162			AuthenticationError::DatabaseError(msg) => write!(f, "Database error: {}", msg),
163			AuthenticationError::Unknown(msg) => write!(f, "Authentication error: {}", msg),
164		}
165	}
166}
167
168impl std::error::Error for AuthenticationError {}
169
170/// Authentication backend trait
171///
172/// All authentication operations are asynchronous to support various backends
173/// including database lookups, external API calls, and distributed systems.
174#[async_trait::async_trait]
175pub trait AuthenticationBackend: Send + Sync {
176	/// Authenticate a request and return a user if successful
177	///
178	/// # Arguments
179	///
180	/// * `request` - The incoming HTTP request
181	///
182	/// # Returns
183	///
184	/// - `Ok(Some(user))` if authentication succeeded
185	/// - `Ok(None)` if authentication failed but should try next backend
186	/// - `Err(error)` if a fatal error occurred
187	async fn authenticate(
188		&self,
189		request: &reinhardt_http::Request,
190	) -> Result<Option<Box<dyn User>>, AuthenticationError>;
191
192	/// Get a user by their ID
193	///
194	/// # Arguments
195	///
196	/// * `user_id` - The user's unique identifier
197	///
198	/// # Returns
199	///
200	/// - `Ok(Some(user))` if user was found
201	/// - `Ok(None)` if user doesn't exist
202	/// - `Err(error)` if an error occurred
203	async fn get_user(&self, user_id: &str) -> Result<Option<Box<dyn User>>, AuthenticationError>;
204}
205
206#[cfg(test)]
207mod tests {
208	use super::*;
209	use uuid::Uuid;
210
211	#[test]
212	#[cfg(feature = "jwt")]
213	fn test_auth_jwt_generate_unit() {
214		let jwt_auth = JwtAuth::new(b"test_secret_key");
215		let user_id = "user123".to_string();
216		let username = "testuser".to_string();
217
218		let token = jwt_auth.generate_token(user_id, username).unwrap();
219
220		assert!(!token.is_empty());
221	}
222
223	#[tokio::test]
224	async fn test_permission_allow_any() {
225		use bytes::Bytes;
226		use hyper::Method;
227		use reinhardt_http::Request;
228
229		let permission = AllowAny;
230		let request = Request::builder()
231			.method(Method::GET)
232			.uri("/test")
233			.body(Bytes::new())
234			.build()
235			.unwrap();
236
237		let context = PermissionContext {
238			request: &request,
239			is_authenticated: false,
240			is_admin: false,
241			is_active: false,
242			user: None,
243		};
244
245		assert!(permission.has_permission(&context).await);
246	}
247
248	#[tokio::test]
249	async fn test_permission_is_authenticated_with_auth() {
250		use bytes::Bytes;
251		use hyper::Method;
252		use reinhardt_http::Request;
253
254		let permission = IsAuthenticated;
255		let request = Request::builder()
256			.method(Method::GET)
257			.uri("/test")
258			.body(Bytes::new())
259			.build()
260			.unwrap();
261
262		let context = PermissionContext {
263			request: &request,
264			is_authenticated: true,
265			is_admin: false,
266			is_active: true,
267			user: None,
268		};
269
270		assert!(permission.has_permission(&context).await);
271	}
272
273	#[tokio::test]
274	async fn test_permission_is_authenticated_without_auth() {
275		use bytes::Bytes;
276		use hyper::Method;
277		use reinhardt_http::Request;
278
279		let permission = IsAuthenticated;
280		let request = Request::builder()
281			.method(Method::GET)
282			.uri("/test")
283			.body(Bytes::new())
284			.build()
285			.unwrap();
286
287		let context = PermissionContext {
288			request: &request,
289			is_authenticated: false,
290			is_admin: false,
291			is_active: false,
292			user: None,
293		};
294
295		assert!(!permission.has_permission(&context).await);
296	}
297
298	#[tokio::test]
299	async fn test_permission_is_admin_user() {
300		use bytes::Bytes;
301		use hyper::Method;
302		use reinhardt_http::Request;
303
304		let permission = IsAdminUser;
305		let request = Request::builder()
306			.method(Method::GET)
307			.uri("/test")
308			.body(Bytes::new())
309			.build()
310			.unwrap();
311
312		// Admin user
313		let context = PermissionContext {
314			request: &request,
315			is_authenticated: true,
316			is_admin: true,
317			is_active: true,
318			user: None,
319		};
320		assert!(permission.has_permission(&context).await);
321
322		// Non-admin user
323		let context = PermissionContext {
324			request: &request,
325			is_authenticated: true,
326			is_admin: false,
327			is_active: true,
328			user: None,
329		};
330		assert!(!permission.has_permission(&context).await);
331	}
332
333	#[tokio::test]
334	async fn test_permission_is_active_user() {
335		use bytes::Bytes;
336		use hyper::Method;
337		use reinhardt_http::Request;
338
339		let permission = IsActiveUser;
340		let request = Request::builder()
341			.method(Method::GET)
342			.uri("/test")
343			.body(Bytes::new())
344			.build()
345			.unwrap();
346
347		// Active user
348		let context = PermissionContext {
349			request: &request,
350			is_authenticated: true,
351			is_admin: false,
352			is_active: true,
353			user: None,
354		};
355		assert!(permission.has_permission(&context).await);
356
357		// Inactive user
358		let context = PermissionContext {
359			request: &request,
360			is_authenticated: true,
361			is_admin: false,
362			is_active: false,
363			user: None,
364		};
365		assert!(!permission.has_permission(&context).await);
366	}
367
368	#[tokio::test]
369	async fn test_permission_is_authenticated_or_read_only_get() {
370		use bytes::Bytes;
371		use hyper::Method;
372		use reinhardt_http::Request;
373
374		let permission = IsAuthenticatedOrReadOnly;
375		let request = Request::builder()
376			.method(Method::GET)
377			.uri("/test")
378			.body(Bytes::new())
379			.build()
380			.unwrap();
381
382		// Unauthenticated GET should be allowed
383		let context = PermissionContext {
384			request: &request,
385			is_authenticated: false,
386			is_admin: false,
387			is_active: false,
388			user: None,
389		};
390		assert!(permission.has_permission(&context).await);
391	}
392
393	#[tokio::test]
394	async fn test_permission_is_authenticated_or_read_only_post() {
395		use bytes::Bytes;
396		use hyper::Method;
397		use reinhardt_http::Request;
398
399		let permission = IsAuthenticatedOrReadOnly;
400		let request = Request::builder()
401			.method(Method::POST)
402			.uri("/test")
403			.body(Bytes::new())
404			.build()
405			.unwrap();
406
407		// Unauthenticated POST should be denied
408		let context = PermissionContext {
409			request: &request,
410			is_authenticated: false,
411			is_admin: false,
412			is_active: false,
413			user: None,
414		};
415		assert!(!permission.has_permission(&context).await);
416
417		// Authenticated POST should be allowed
418		let context = PermissionContext {
419			request: &request,
420			is_authenticated: true,
421			is_admin: false,
422			is_active: true,
423			user: None,
424		};
425		assert!(permission.has_permission(&context).await);
426	}
427
428	#[test]
429	fn test_simple_user_implementation() {
430		let user = SimpleUser {
431			id: Uuid::new_v4(),
432			username: "testuser".to_string(),
433			email: "test@example.com".to_string(),
434			is_active: true,
435			is_admin: false,
436			is_staff: false,
437			is_superuser: false,
438		};
439
440		assert!(!user.id().is_empty());
441		assert_eq!(user.username(), "testuser");
442		assert!(user.is_authenticated());
443		assert!(user.is_active());
444		assert!(!user.is_admin());
445	}
446
447	#[test]
448	fn test_anonymous_user() {
449		let user = AnonymousUser;
450
451		assert_eq!(user.id(), "");
452		assert_eq!(user.username(), "");
453		assert!(!user.is_authenticated());
454		assert!(!user.is_active());
455		assert!(!user.is_admin());
456	}
457}