Skip to main content

reinhardt_auth/
lib.rs

1#![warn(missing_docs)]
2#![cfg_attr(docsrs, feature(doc_cfg))]
3//! # Reinhardt Auth
4//!
5//! Authentication and authorization system for Reinhardt framework.
6//!
7//! ## Features
8//!
9//! - **DjangoModelPermissions**: Django-style model permissions with `app_label.action_model` format
10//! - **DjangoModelPermissionsOrAnonReadOnly**: Anonymous read access for unauthenticated users
11//! - **Object-Level Permissions**: Fine-grained access control on individual objects
12//! - **User Management**: CRUD operations for users with password hashing
13//! - **Group Management**: User groups and permission assignment
14//! - **REST API Authentication**: Multiple authentication backends (JWT, Token, Session, OAuth2)
15//! - **Standard Permissions**: Permission classes for common authorization scenarios
16//! - **createsuperuser Command**: CLI tool for creating admin users
17//!
18//! ## Quick Start
19//!
20//! ```rust
21//! use reinhardt_auth::core::{IsAuthenticated, PermissionContext};
22//!
23//! // Check if a permission is satisfied
24//! let permission = IsAuthenticated;
25//! // In actual usage, you would pass a real request context
26//! let _ = permission; // permission classes implement PermissionClass trait
27//! ```
28//!
29//! ## Architecture
30//!
31//! Key modules in this crate:
32//!
33//! - [`core`]: Authentication traits, user types, permission classes, and password hashing
34//! - [`sessions`]: Session backends (JWT, database, Redis, cookie, file)
35//! - `social` (feature-gated): OAuth2/OpenID Connect social authentication providers
36//! - `user_management`: CRUD operations for users and groups
37//!
38//! ## Feature Flags
39//!
40//! | Feature | Default | Description |
41//! |---------|---------|-------------|
42//! | `params` | enabled | Parameter extraction via DI |
43//! | `jwt` | disabled | JWT-based authentication backend |
44//! | `sessions` | disabled | Session-based authentication |
45//! | `oauth` | disabled | OAuth2 authorization code flow |
46//! | `token` | disabled | Token-based authentication |
47//! | `argon2-hasher` | disabled | Argon2 password hashing (alternative to bcrypt) |
48//! | `social` | disabled | Social authentication (OAuth2/OIDC providers) |
49//! | `database` | disabled | Database-backed user/group storage via ORM |
50//!
51//! ## Security Note: Client-Side vs Server-Side Checks
52//!
53//! Authentication state exposed via `reinhardt_http::AuthState` (e.g.,
54//! `is_authenticated()`, `is_admin()`) is populated by server-side
55//! middleware and stored in request extensions. When this state is
56//! forwarded to client-side code (e.g., via WASM or JSON responses),
57//! **it must only be used for UI display purposes** (showing/hiding
58//! elements). All authorization decisions must be enforced server-side
59//! through middleware and permission classes provided by this crate.
60
61pub mod sessions;
62
63// Core authentication types and traits (migrated from reinhardt-core-auth)
64pub mod core;
65
66// AuthInfo lightweight auth extractor
67pub mod auth_info;
68pub use auth_info::AuthInfo;
69
70// Guard types for permission-based DI resolution
71/// Permission guard types and combinators for DI-based authorization.
72pub mod guard;
73pub use guard::{All, Any, Guard, Not, Public};
74
75// Re-export guard!() macro from reinhardt-auth-macros
76pub use reinhardt_auth_macros::guard;
77
78// Authenticated user extractors
79pub mod auth_user;
80pub use auth_user::CurrentUser;
81
82// Startup validation for auth extractors
83pub mod auth_extractors;
84pub use auth_extractors::validate_auth_extractors;
85
86/// Project-specific UUID namespace for deterministic user ID generation.
87///
88/// Computed from `Uuid::new_v5(&Uuid::NAMESPACE_URL, b"https://reinhardt.rs/user-id")`.
89pub(crate) const USER_ID_NAMESPACE: uuid::Uuid =
90	uuid::uuid!("c7a85537-073f-5092-8d10-774e109477c9");
91
92pub(crate) mod internal_user;
93
94// Re-export core authentication types. The deprecated `User` trait,
95// `SimpleUser`, and `AnonymousUser` (which lived in `core::user`) were
96// removed in 0.2.0 per Issue #4520 — use `AuthIdentity` + `BaseUser` /
97// `FullUser` + `PermissionsMixin` instead.
98pub use core::{
99	AllowAny, AuthBackend, AuthIdentity, BaseUser, CompositeAuthBackend, FullUser, IsActiveUser,
100	IsAdminUser, IsAuthenticated, IsAuthenticatedOrReadOnly, PasswordHasher, Permission,
101	PermissionContext, PermissionsMixin, SuperuserCreator, SuperuserCreatorRegistration,
102	SuperuserInit, TypedSuperuserCreator, auto_register_superuser_creator, get_superuser_creator,
103	register_superuser_creator, superuser_creator_for,
104};
105
106#[cfg(feature = "argon2-hasher")]
107pub use core::Argon2Hasher;
108
109// Re-export permission operators from core
110pub use core::permission_operators;
111
112pub mod repository;
113pub use repository::{SimpleUserRepository, UserRepository};
114
115/// Advanced permission classes (role-based, object-level).
116pub mod advanced_permissions;
117/// Base user manager trait for CRUD operations.
118pub mod base_user_manager;
119/// HTTP Basic authentication backend.
120#[cfg_attr(docsrs, doc(cfg(feature = "basic")))]
121#[cfg(feature = "basic")]
122pub mod basic;
123/// Group management (create, delete, assign users).
124pub mod group_management;
125/// Login/logout HTTP handlers.
126#[cfg(feature = "sessions")]
127pub mod handlers;
128/// IP-based permission classes (whitelist/blacklist with CIDR).
129pub mod ip_permission;
130/// JWT (JSON Web Token) authentication.
131#[cfg(feature = "jwt")]
132pub mod jwt;
133/// Multi-factor authentication support.
134pub mod mfa;
135/// Django-compatible model-level permissions.
136pub mod model_permissions;
137/// OAuth2 authentication provider.
138#[cfg(feature = "oauth")]
139pub mod oauth2;
140/// Object-level permission checking.
141pub mod object_permissions;
142/// Database-backed permission model.
143#[cfg(feature = "database")]
144pub mod permission;
145/// Rate-limiting permission class.
146#[cfg(feature = "rate-limit")]
147pub mod rate_limit_permission;
148/// Remote user authentication (proxy-based).
149pub mod remote_user;
150/// REST API authentication backends.
151pub mod rest_authentication;
152/// Session-based authentication.
153#[cfg(feature = "sessions")]
154pub mod session;
155/// Social authentication providers (Google, GitHub, Apple, Microsoft).
156#[cfg(feature = "social")]
157pub mod social;
158/// Time-based permission class (time windows, date ranges).
159pub mod time_based_permission;
160/// Token blacklist for revocation.
161#[cfg(any(feature = "jwt", feature = "token"))]
162pub mod token_blacklist;
163/// Automatic token rotation.
164#[cfg(any(feature = "jwt", feature = "token"))]
165pub mod token_rotation;
166/// Token persistence storage backends.
167#[cfg(any(feature = "jwt", feature = "token"))]
168pub mod token_storage;
169/// User CRUD management.
170pub mod user_management;
171
172/// Settings fragments for authentication backends.
173pub mod settings;
174
175pub use advanced_permissions::{ObjectPermission as AdvancedObjectPermission, RoleBasedPermission};
176pub use base_user_manager::BaseUserManager;
177#[cfg_attr(docsrs, doc(cfg(feature = "basic")))]
178#[cfg(feature = "basic")]
179pub use basic::BasicAuthentication as HttpBasicAuth;
180pub use group_management::{
181	CreateGroupData, Group, GroupManagementError, GroupManagementResult, GroupManager,
182	get_group_manager, register_group_manager,
183};
184#[cfg(feature = "sessions")]
185pub use handlers::{LoginCredentials, LoginHandler, LogoutHandler, SESSION_COOKIE_NAME};
186pub use ip_permission::{CidrRange, IpBlacklistPermission, IpWhitelistPermission};
187#[cfg(feature = "jwt")]
188pub use jwt::{Claims, JwtAuth, JwtError};
189pub use mfa::MFAAuthentication as MfaManager;
190pub use model_permissions::{
191	DjangoModelPermissions, DjangoModelPermissionsOrAnonReadOnly, ModelPermission,
192};
193#[cfg(feature = "oauth")]
194pub use oauth2::{
195	AccessToken, AuthorizationCode, GrantType, InMemoryOAuth2Store, OAuth2Application,
196	OAuth2Authentication, OAuth2TokenStore,
197};
198pub use object_permissions::{ObjectPermission, ObjectPermissionChecker, ObjectPermissionManager};
199#[cfg(feature = "database")]
200pub use permission::AuthPermission;
201pub use permission_operators::{AndPermission, NotPermission, OrPermission};
202// Re-export the error type used by `BaseUserManager` so downstream code (and the
203// `#[user]` macro's auto-generated manager impl) can reference it without
204// taking a direct dependency on `reinhardt-core`.
205pub use reinhardt_core::exception::Error as BaseUserManagerError;
206
207/// Re-export of [`serde_json::Value`] for use in `BaseUserManager` method
208/// signatures emitted by the `#[user(...)]` auto-manager generator.
209///
210/// Consumers of the auto-generated manager get this re-export "for free" via
211/// `reinhardt_auth::JsonValue`, so they do not need to add a direct
212/// `serde_json` dependency just to satisfy the trait signature.
213pub use serde_json::Value as JsonValue;
214#[cfg(feature = "social")]
215pub use social::{
216	AppleProvider, GenericOidcConfig, GenericOidcProvider, GitHubProvider, GoogleProvider, IdToken,
217	MicrosoftProvider, OAuthProvider, OAuthToken, PkceFlow, ProviderConfig, SocialAuthBackend,
218	SocialAuthError, StandardClaims, StateStore, TokenResponse, UserInfoMapper,
219};
220
221#[cfg(feature = "rate-limit")]
222pub use rate_limit_permission::{RateLimitPermission, RateLimitPermissionBuilder};
223pub use remote_user::RemoteUserAuthentication as RemoteUserAuth;
224pub use rest_authentication::{
225	BasicAuthConfig, CompositeAuthentication, RemoteUserAuthentication, RestAuthentication,
226	SessionAuthConfig, SessionAuthentication, TokenAuthConfig, TokenAuthentication,
227};
228#[cfg(feature = "sessions")]
229pub use session::{InMemorySessionStore, SESSION_KEY_USER_ID, Session, SessionId, SessionStore};
230pub use time_based_permission::{DateRange, TimeBasedPermission, TimeWindow};
231#[cfg(any(feature = "jwt", feature = "token"))]
232pub use token_blacklist::{
233	BlacklistReason, BlacklistStats, BlacklistedToken, InMemoryRefreshTokenStore,
234	InMemoryTokenBlacklist, RefreshToken, RefreshTokenStore, TokenBlacklist, TokenRotationManager,
235};
236#[cfg(any(feature = "jwt", feature = "token"))]
237#[allow(deprecated)] // Re-export keeps the compatibility API discoverable during the 0.2 line.
238pub use token_rotation::{AutoTokenRotationManager, TokenRotationConfig, TokenRotationRecord};
239
240#[cfg(feature = "sessions")]
241pub use settings::SessionSettings;
242
243#[cfg(feature = "jwt")]
244pub use settings::{JwtSessionSettings, create_jwt_session_backend_from_settings};
245
246#[cfg(feature = "token")]
247pub use settings::{TokenRotationSettings, create_token_rotation_manager_from_settings};
248#[cfg(all(feature = "database", any(feature = "jwt", feature = "token")))]
249pub use token_storage::DatabaseTokenStorage;
250#[cfg(any(feature = "jwt", feature = "token"))]
251pub use token_storage::{
252	InMemoryTokenStorage, StoredToken, TokenStorage, TokenStorageError, TokenStorageResult,
253};
254pub use user_management::{
255	CreateUserData, ManagedUser, UpdateUserData, UserManagementError, UserManagementResult,
256	UserManager,
257};
258
259/// Authentication errors that can occur during user verification.
260#[non_exhaustive]
261#[derive(Debug, Clone, PartialEq, Eq)]
262pub enum AuthenticationError {
263	/// The provided credentials (username/password) are incorrect.
264	InvalidCredentials,
265	/// The requested user does not exist.
266	UserNotFound,
267	/// The user's session has expired.
268	SessionExpired,
269	/// The provided authentication token is invalid or malformed.
270	InvalidToken,
271	/// The JWT token has expired.
272	TokenExpired,
273	/// The request lacks valid authentication credentials.
274	NotAuthenticated,
275	/// A database error occurred during authentication.
276	DatabaseError(String),
277	/// An unspecified authentication error.
278	Unknown(String),
279}
280
281impl std::fmt::Display for AuthenticationError {
282	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
283		match self {
284			AuthenticationError::InvalidCredentials => write!(f, "Invalid credentials"),
285			AuthenticationError::UserNotFound => write!(f, "User not found"),
286			AuthenticationError::SessionExpired => write!(f, "Session expired"),
287			AuthenticationError::InvalidToken => write!(f, "Invalid token"),
288			AuthenticationError::TokenExpired => write!(f, "Token expired"),
289			AuthenticationError::NotAuthenticated => write!(f, "User is not authenticated"),
290			AuthenticationError::DatabaseError(msg) => write!(f, "Database error: {}", msg),
291			AuthenticationError::Unknown(msg) => write!(f, "Authentication error: {}", msg),
292		}
293	}
294}
295
296impl std::error::Error for AuthenticationError {}
297
298#[cfg(feature = "jwt")]
299impl From<JwtError> for AuthenticationError {
300	fn from(err: JwtError) -> Self {
301		match err {
302			JwtError::TokenExpired => AuthenticationError::TokenExpired,
303			JwtError::InvalidSignature(_) | JwtError::InvalidToken(_) => {
304				AuthenticationError::InvalidToken
305			}
306			JwtError::EncodingError(msg) => AuthenticationError::Unknown(msg),
307		}
308	}
309}
310
311// `AuthenticationBackend` trait was removed in 0.2.0 per Issue #4520.
312// Use `AuthBackend` (from `core::auth_backend`) instead. The old trait
313// depended on the removed `User` trait; `AuthBackend` works with
314// `AuthIdentity` and is the canonical authentication backend trait.
315
316#[cfg(test)]
317mod tests {
318	use super::*;
319
320	#[test]
321	#[cfg(feature = "jwt")]
322	fn test_auth_jwt_generate_unit() {
323		let jwt_auth = JwtAuth::new(b"test_secret_key");
324		let user_id = "user123".to_string();
325		let username = "testuser".to_string();
326
327		let token = jwt_auth
328			.generate_token(user_id, username, false, false)
329			.unwrap();
330
331		assert!(!token.is_empty());
332	}
333
334	#[tokio::test]
335	async fn test_permission_allow_any() {
336		use bytes::Bytes;
337		use hyper::Method;
338		use reinhardt_http::Request;
339
340		let permission = AllowAny;
341		let request = Request::builder()
342			.method(Method::GET)
343			.uri("/test")
344			.body(Bytes::new())
345			.build()
346			.unwrap();
347
348		let context = PermissionContext {
349			request: &request,
350			is_authenticated: false,
351			is_admin: false,
352			is_active: false,
353			user: None,
354		};
355
356		assert!(permission.has_permission(&context).await);
357	}
358
359	#[tokio::test]
360	async fn test_permission_is_authenticated_with_auth() {
361		use bytes::Bytes;
362		use hyper::Method;
363		use reinhardt_http::Request;
364
365		let permission = IsAuthenticated;
366		let request = Request::builder()
367			.method(Method::GET)
368			.uri("/test")
369			.body(Bytes::new())
370			.build()
371			.unwrap();
372
373		let context = PermissionContext {
374			request: &request,
375			is_authenticated: true,
376			is_admin: false,
377			is_active: true,
378			user: None,
379		};
380
381		assert!(permission.has_permission(&context).await);
382	}
383
384	#[tokio::test]
385	async fn test_permission_is_authenticated_without_auth() {
386		use bytes::Bytes;
387		use hyper::Method;
388		use reinhardt_http::Request;
389
390		let permission = IsAuthenticated;
391		let request = Request::builder()
392			.method(Method::GET)
393			.uri("/test")
394			.body(Bytes::new())
395			.build()
396			.unwrap();
397
398		let context = PermissionContext {
399			request: &request,
400			is_authenticated: false,
401			is_admin: false,
402			is_active: false,
403			user: None,
404		};
405
406		assert!(!permission.has_permission(&context).await);
407	}
408
409	#[tokio::test]
410	async fn test_permission_is_admin_user() {
411		use bytes::Bytes;
412		use hyper::Method;
413		use reinhardt_http::Request;
414
415		let permission = IsAdminUser;
416		let request = Request::builder()
417			.method(Method::GET)
418			.uri("/test")
419			.body(Bytes::new())
420			.build()
421			.unwrap();
422
423		// Admin user
424		let context = PermissionContext {
425			request: &request,
426			is_authenticated: true,
427			is_admin: true,
428			is_active: true,
429			user: None,
430		};
431		assert!(permission.has_permission(&context).await);
432
433		// Non-admin user
434		let context = PermissionContext {
435			request: &request,
436			is_authenticated: true,
437			is_admin: false,
438			is_active: true,
439			user: None,
440		};
441		assert!(!permission.has_permission(&context).await);
442	}
443
444	#[tokio::test]
445	async fn test_permission_is_active_user() {
446		use bytes::Bytes;
447		use hyper::Method;
448		use reinhardt_http::Request;
449
450		let permission = IsActiveUser;
451		let request = Request::builder()
452			.method(Method::GET)
453			.uri("/test")
454			.body(Bytes::new())
455			.build()
456			.unwrap();
457
458		// Active user
459		let context = PermissionContext {
460			request: &request,
461			is_authenticated: true,
462			is_admin: false,
463			is_active: true,
464			user: None,
465		};
466		assert!(permission.has_permission(&context).await);
467
468		// Inactive user
469		let context = PermissionContext {
470			request: &request,
471			is_authenticated: true,
472			is_admin: false,
473			is_active: false,
474			user: None,
475		};
476		assert!(!permission.has_permission(&context).await);
477	}
478
479	#[tokio::test]
480	async fn test_permission_is_authenticated_or_read_only_get() {
481		use bytes::Bytes;
482		use hyper::Method;
483		use reinhardt_http::Request;
484
485		let permission = IsAuthenticatedOrReadOnly;
486		let request = Request::builder()
487			.method(Method::GET)
488			.uri("/test")
489			.body(Bytes::new())
490			.build()
491			.unwrap();
492
493		// Unauthenticated GET should be allowed
494		let context = PermissionContext {
495			request: &request,
496			is_authenticated: false,
497			is_admin: false,
498			is_active: false,
499			user: None,
500		};
501		assert!(permission.has_permission(&context).await);
502	}
503
504	#[tokio::test]
505	async fn test_permission_is_authenticated_or_read_only_post() {
506		use bytes::Bytes;
507		use hyper::Method;
508		use reinhardt_http::Request;
509
510		let permission = IsAuthenticatedOrReadOnly;
511		let request = Request::builder()
512			.method(Method::POST)
513			.uri("/test")
514			.body(Bytes::new())
515			.build()
516			.unwrap();
517
518		// Unauthenticated POST should be denied
519		let context = PermissionContext {
520			request: &request,
521			is_authenticated: false,
522			is_admin: false,
523			is_active: false,
524			user: None,
525		};
526		assert!(!permission.has_permission(&context).await);
527
528		// Authenticated POST should be allowed
529		let context = PermissionContext {
530			request: &request,
531			is_authenticated: true,
532			is_admin: false,
533			is_active: true,
534			user: None,
535		};
536		assert!(permission.has_permission(&context).await);
537	}
538}