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