axum_gate/
lib.rs

1#![deny(missing_docs)]
2#![deny(unsafe_code)]
3#![deny(clippy::unwrap_used)]
4#![deny(clippy::expect_used)]
5
6//! # axum-gate
7//!
8//! Flexible, type-safe authentication and authorization for axum using JWTs and optional OAuth2.
9//! Supports cookie and bearer authentication, plus an OAuth2 Authorization Code + PKCE login flow
10//! that mints a first-party JWT cookie for browser sessions. Designed for single nodes and
11//! distributed systems with multiple storage backends.
12//!
13//! ## Key Features
14//!
15//! - **Cookie and bearer JWT authentication** - Choose HTTP-only cookies or Authorization: Bearer
16//! - **OAuth2 login flow builder** - Authorization Code + PKCE; mints first-party JWT cookies
17//! - **Role-based access control** - Hierarchical roles with supervisor inheritance
18//! - **Group-based access control** - Organize users by teams, departments, or projects
19//! - **Permission system** - Fine-grained permissions with deterministic hashing
20//! - **Multiple storage backends** - In-memory, SurrealDB, SeaORM support
21//! - **Distributed system ready** - Zero-synchronization permission system
22//! - **Pre-built handlers** - Login/logout endpoints with timing attack protection
23//! - **Optional anonymous context** - Install `Option<Account>` and `Option<RegisteredClaims>`
24//! - **Static token mode** - Simple shared-secret bearer auth for internal services
25//! - **Audit and metrics (feature-gated)** - Structured audit logs and Prometheus metrics
26//!
27//! ### Re-exports
28//! This crate re-exports selected external crates (e.g., `jsonwebtoken`, `cookie`, `uuid`, `axum_extra`, and, behind a feature flag, `prometheus`) because types from these crates are part of this crate’s public API. Keeping these re-exports is intentional so users can import the exposed types from a single namespace.
29//!
30//! ### Prelude
31//! A convenience prelude is available via `axum_gate::prelude::*` that re-exports the most commonly used types.
32//!
33//! ### Feature Flags
34//! - `storage-surrealdb` — SurrealDB repositories (see [BUSL-1.1 license note](https://github.com/emirror-de/axum-gate?tab=readme-ov-file#msrv-and-license))!
35//! - `storage-seaorm` — SeaORM repositories (original `sea-orm` crate; mutually exclusive with `storage-seaorm-v2`)
36//! - `storage-seaorm-v2` — SeaORM v2 repositories
37//! - `audit-logging` — emit structured audit events
38//! - `prometheus` — export metrics for audit logging (implies `audit-logging`)
39//! - `insecure-fast-hash` — faster Argon2 preset for development only (opt-in for release, not recommended)
40//! - `aws_lc_rs`: Uses AWS Libcrypto for JWT cryptographic operations
41//!
42//! Note: `storage-seaorm` and `storage-seaorm-v2` are mutually exclusive. Enable only one of these features per build to avoid compilation/feature conflicts.
43//!
44//! For common integration issues and debugging tips, [see the Troubleshooting guide](https://github.com/emirror-de/axum-gate/blob/nightly/TROUBLESHOOTING.md).
45//!
46//! ## Quick Start
47//!
48//! ```rust
49//! use axum::{routing::get, Router};
50//! use axum_gate::prelude::*;
51//! use axum_gate::repositories::memory::{MemoryAccountRepository, MemorySecretRepository};
52//! use std::sync::Arc;
53//!
54//! #[tokio::main]
55//! async fn main() {
56//!     // Set up storage (dev-friendly in-memory backends)
57//!     let account_repo = Arc::new(MemoryAccountRepository::<Role, Group>::default());
58//!     let secret_repo = Arc::new(MemorySecretRepository::new_with_argon2_hasher().unwrap());
59//!
60//!     // Create a JWT codec. Use a persistent key in production (e.g., env/secret manager).
61//!     let secret = std::env::var("JWT_SECRET").unwrap_or_else(|_| "dev-secret-key".to_string());
62//!     let options = JsonWebTokenOptions {
63//!         enc_key: jsonwebtoken::EncodingKey::from_secret(secret.as_bytes()),
64//!         dec_key: jsonwebtoken::DecodingKey::from_secret(secret.as_bytes()),
65//!         header: None,
66//!         validation: None,
67//!     };
68//!     let jwt = Arc::new(JsonWebToken::<JwtClaims<Account<Role, Group>>>::new_with_options(options));
69//!
70//!     // Protect routes with role-based access (cookie auth)
71//!     let app = Router::<()>::new()
72//!         .route("/admin", get(admin_handler))
73//!         .layer(
74//!             Gate::cookie::<_, Role, Group>("my-app", jwt)
75//!                 .with_policy(AccessPolicy::require_role(Role::Admin))
76//!                 .configure_cookie_template(|tpl| tpl.name("auth-token"))
77//!                 .unwrap(),
78//!         );
79//! }
80//!
81//! async fn admin_handler() -> &'static str { "Admin access granted!" }
82//! ```
83//!
84//! ## Access Control
85//!
86//! ### Role-Based Access
87//! ```rust
88//! use axum_gate::prelude::{Role, Group, AccessPolicy};
89//!
90//! // Single role requirement
91//! let policy = AccessPolicy::<Role, Group>::require_role(Role::Admin);
92//!
93//! // Multiple role options
94//! let policy = AccessPolicy::<Role, Group>::require_role(Role::Admin)
95//!     .or_require_role(Role::Moderator);
96//!
97//! // Hierarchical access (role + all supervisor roles)
98//! let policy = AccessPolicy::<Role, Group>::require_role_or_supervisor(Role::User);
99//! ```
100//!
101//! ### Group-Based Access
102//! ```rust
103//! use axum_gate::prelude::{Role, Group, AccessPolicy};
104//!
105//! let policy = AccessPolicy::<Role, Group>::require_group(Group::new("engineering"))
106//!     .or_require_group(Group::new("management"));
107//! ```
108//!
109//! ### Permission-Based Access
110//! ```rust
111//! use axum_gate::prelude::{Role, Group, AccessPolicy, PermissionId};
112//!
113//! // Validate permissions at compile-time (checks for hash collisions)
114//! axum_gate::validate_permissions!["read:api", "write:api", "admin:system"];
115//!
116//! // Use in access policies
117//! let policy = AccessPolicy::<Role, Group>::require_permission(PermissionId::from("read:api"));
118//! ```
119//!
120//! ### Convenient Login Check
121//! ```rust
122//! use axum_gate::prelude::*;
123//! use std::sync::Arc;
124//!
125//! # let jwt_codec = Arc::new(JsonWebToken::<JwtClaims<Account<Role, Group>>>::default());
126//! // Allow any authenticated user (all roles: User, Reporter, Moderator, Admin)
127//! let gate = Gate::cookie::<_, Role, Group>("my-app", jwt_codec)
128//!     .require_login()  // Convenience method for any logged-in user
129//!     .configure_cookie_template(|tpl| tpl.name("auth-token"))
130//!     .unwrap();
131//! ```
132//!
133//! ## Authentication Modes
134//!
135//! ### Cookie (Optional User Context)
136//! For routes that should never be blocked but may use authenticated context when present:
137//!
138//! ```rust
139//! use axum::{routing::get, Router, extract::Extension};
140//! use axum_gate::prelude::*;
141//! use axum_gate::codecs::jwt::RegisteredClaims;
142//! use std::sync::Arc;
143//!
144//! async fn homepage(
145//!     Extension(user_opt): Extension<Option<Account<Role, Group>>>,
146//!     Extension(claims_opt): Extension<Option<RegisteredClaims>>,
147//! ) -> String {
148//!     if let (Some(user), Some(claims)) = (user_opt, claims_opt) {
149//!         format!("Welcome back {} (token expires at {})", user.user_id, claims.expiration_time)
150//!     } else {
151//!         "Welcome anonymous visitor".into()
152//!     }
153//! }
154//!
155//! # let jwt = Arc::new(JsonWebToken::<JwtClaims<Account<Role, Group>>>::default());
156//! let app = Router::<()>::new()
157//!     .route("/", get(homepage))
158//!     .layer(
159//!         Gate::cookie::<_, Role, Group>("my-app", jwt)
160//!             .allow_anonymous_with_optional_user()
161//!     );
162//! ```
163//!
164//! ### Bearer (Strict, Optional, Static Token)
165//! Strict bearer (JWT) example:
166//! ```rust
167//! # use axum::{routing::get, Router};
168//! # use axum_gate::prelude::*;
169//! # use std::sync::Arc;
170//! # async fn handler() {}
171//! let jwt = Arc::new(JsonWebToken::<JwtClaims<Account<Role, Group>>>::default());
172//! let app = Router::<()>::new()
173//!     .route("/admin", get(handler))
174//!     .layer(
175//!         Gate::bearer("my-app", Arc::clone(&jwt))
176//!             .with_policy(AccessPolicy::<Role, Group>::require_role(Role::Admin))
177//!     );
178//! ```
179//!
180//! Optional mode (never blocks; installs `Option<Account>` and `Option<RegisteredClaims>`):
181//! ```rust
182//! # use axum_gate::prelude::*;
183//! # use std::sync::Arc;
184//! let jwt = Arc::new(JsonWebToken::<JwtClaims<Account<Role, Group>>>::default());
185//! let gate = Gate::bearer::<_, Role, Group>("my-app", jwt).allow_anonymous_with_optional_user();
186//! ```
187//!
188//! Static token mode (shared secret; useful for internal services):
189//! ```rust
190//! # use axum_gate::prelude::*;
191//! # use std::sync::Arc;
192//! # async fn handler() {}
193//! let jwt = Arc::new(JsonWebToken::<JwtClaims<Account<Role, Group>>>::default());
194//! let app = axum::Router::<()>::new()
195//!     .route("/internal", axum::routing::get(handler))
196//!     .layer(Gate::bearer::<_, Role, Group>("my-app", jwt).with_static_token("very-secret-token"));
197//! ```
198//!
199//! ### OAuth2 (Authorization Code + PKCE → first-party JWT)
200//! Minimal setup for mounting "/auth/login" and "/auth/callback":
201//! ```rust
202//! use axum::{Router, routing::get};
203//! use axum_gate::prelude::*;
204//! use std::sync::Arc;
205//!
206//! // Provide a JWT codec to mint the session cookie after successful callback.
207//! let jwt = Arc::new(JsonWebToken::<JwtClaims<Account<Role, Group>>>::default());
208//!
209//! let oauth_routes = Gate::oauth2_with_jwt("my-app", jwt, 3600)
210//!     .auth_url("https://provider.example.com/oauth2/authorize")
211//!     .token_url("https://provider.example.com/oauth2/token")
212//!     .client_id("CLIENT_ID")
213//!     .client_secret("CLIENT_SECRET")
214//!     .redirect_url("https://your.app/auth/callback")
215//!     .add_scope("openid")
216//!     .add_scope("email")
217//!     // Map provider token response to your Account<Role, Group>:
218
219//!     .with_account_mapper(|_token_resp| {
220//!         Box::pin(async {
221//!             // fetch userinfo as needed, then construct Account<Role, Group>
222//!             Ok(Account::<Role, Group>::new("user@example.com", &[], &[]))
223//!         })
224//!     })
225//!     .routes("/auth")
226//!     .expect("valid oauth2 config");
227//!
228//! let app = Router::<()>::new().nest("/auth", oauth_routes);
229//! ```
230//!
231//! ## Account Management
232//!
233//! ```rust
234//! use axum_gate::accounts::AccountInsertService;
235//! use axum_gate::permissions::Permissions;
236//! use axum_gate::prelude::{Role, Group, Account};
237//! use axum_gate::repositories::memory::{MemoryAccountRepository, MemorySecretRepository};
238//! use std::sync::Arc;
239//!
240//! # tokio_test::block_on(async {
241//! # let account_repo = Arc::new(MemoryAccountRepository::<Role, Group>::default());
242//! # let secret_repo = Arc::new(MemorySecretRepository::new_with_argon2_hasher().unwrap());
243//! // Create account with roles, groups, and permissions
244//! let account = AccountInsertService::insert("user@example.com", "password")
245//!     .with_roles(vec![Role::User])
246//!     .with_groups(vec![Group::new("staff")])
247//!     .with_permissions(Permissions::from_iter(["read:profile"]))
248//!     .into_repositories(account_repo, secret_repo)
249//!     .await;
250//! # });
251//! ```
252//!
253//! ## Storage Backends
254//!
255//! ### In-Memory (Development)
256//! ```rust
257//! use axum_gate::repositories::memory::{MemoryAccountRepository, MemorySecretRepository};
258//! use axum_gate::prelude::{Role, Group};
259//! use std::sync::Arc;
260//!
261//! let account_repo = Arc::new(MemoryAccountRepository::<Role, Group>::default());
262//! let secret_repo = Arc::new(MemorySecretRepository::new_with_argon2_hasher().unwrap());
263//! ```
264//!
265//! ### SurrealDB (Feature: `storage-surrealdb`)
266//! ```rust
267//! # #[cfg(feature="storage-surrealdb")]
268//! # {
269//! use axum_gate::repositories::surrealdb::{DatabaseScope, SurrealDbRepository};
270//! use std::sync::Arc;
271//!
272//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
273//! # let db: surrealdb::Surreal<surrealdb::engine::any::Any> = todo!();
274//! # let scope = DatabaseScope::default();
275//! let repo = Arc::new(SurrealDbRepository::new(db, scope));
276//! # Ok(())
277//! # }
278//! # }
279//! ```
280//!
281//! ### SeaORM (Feature: `storage-seaorm`)
282//! ```rust
283//! # #[cfg(feature="storage-seaorm")]
284//! # {
285//! use axum_gate::repositories::sea_orm::{SeaOrmRepository, models};
286//! use std::sync::Arc;
287//!
288//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
289//! # let db: sea_orm::DatabaseConnection = todo!();
290//! let repo = Arc::new(SeaOrmRepository::new(&db));
291//! # Ok(())
292//! # }
293//! # }
294//! ```
295//!
296//! ## Authentication Handlers
297//!
298//! Pre-built [`route_handlers::login`] and [`route_handlers::logout`] handlers integrate with
299//! your storage backends and JWT configuration. See examples in the repository for complete
300//! implementation patterns with dependency injection and routing setup.
301//!
302//! Note: The `login` handler is for username/password flows and is not used with
303//! `OAuth2Gate` (which mounts its own `/login` and `/callback` routes). For sign-out,
304//! the same `logout` handler remains applicable—as long as its `CookieTemplate` matches
305//! the auth cookie name/template used by your OAuth2-issued first‑party JWT.
306//!
307//! ## User Data in Handlers
308//!
309//! ```rust
310//! use axum::extract::Extension;
311//! use axum_gate::codecs::jwt::RegisteredClaims;
312//! use axum_gate::prelude::{Account, Role, Group};
313//!
314//! async fn profile_handler(
315//!     Extension(user): Extension<Account<Role, Group>>,
316//!     Extension(claims): Extension<RegisteredClaims>,
317//! ) -> String {
318//!     format!(
319//!         "Hello {}, roles: {:?}, issued at: {}, expires: {}",
320//!         user.user_id, user.roles, claims.issued_at_time, claims.expiration_time
321//!     )
322//! }
323//! ```
324//!
325//! ## Security Features
326//!
327//! ### Cookie Security
328//! - **Secure defaults**: [`CookieTemplate::recommended`](cookie_template::CookieTemplate::recommended) provides secure defaults
329//! - **HTTPS enforcement**: `secure(true)` cookies in production
330//! - **XSS protection**: `http_only(true)` prevents script access
331//! - **CSRF mitigation**: `SameSite::Strict` for sensitive operations
332//!
333//! ### JWT Security
334//! - **Persistent keys**: Use stable signing keys in production (see [`JsonWebToken`](codecs::jwt::JsonWebToken) docs)
335//! - **Proper expiration**: Set reasonable JWT expiration times
336//! - **Key rotation**: Manual key replacement only; rotation invalidates existing tokens
337//!
338//! ### Timing Attack Protection
339//! Built-in protection against timing attacks:
340//! - Constant-time credential verification using the [`subtle`] crate
341//! - Always performs password verification, even for non-existent users
342//! - Unified error responses prevent user enumeration
343//! - Applied to all storage backends
344//!
345//! ### Audit and Metrics (feature-gated)
346//! - Enable `audit-logging` to emit structured audit events for authentication flows
347//! - Enable `prometheus` (implies `audit-logging`) to export metrics; in bearer mode you can
348//!   also call `with_prometheus_metrics()` or `with_prometheus_registry(..)` on the gate builder
349//! - Never log sensitive values (secrets, tokens, cookies); only high-level event metadata
350//!
351//! ## Permission System
352//! - **Compile-time validation**: Use [`validate_permissions!`] macro for collision detection
353//! - **Runtime validation**: [`permissions::PermissionCollisionChecker`] for dynamic permissions
354//! - **Deterministic hashing**: No coordination needed between distributed nodes
355//! - **Efficient storage**: Bitmap-based permission storage with fast lookups
356//!
357//! Note for client and WASM usage:
358//! If you're building client-side or WebAssembly (wasm) applications and only need the crate's data models (types) without server-only dependencies, you can depend on this crate with default features disabled. For example:
359//!
360//! axum-gate = { version = "1", default-features = false }
361//!
362//! This allows using the models and core types in constrained runtimes (like wasm) while avoiding optional server features that require a full server environment.
363
364#[cfg(feature = "server")]
365pub use axum_extra;
366#[cfg(feature = "server")]
367pub use cookie;
368#[cfg(feature = "server")]
369pub use jsonwebtoken;
370#[cfg(all(feature = "server", feature = "prometheus"))]
371pub use prometheus;
372pub use uuid;
373pub mod accounts;
374#[cfg(feature = "server")]
375pub mod audit;
376#[cfg(feature = "server")]
377pub mod authn;
378pub mod authz;
379#[cfg(feature = "server")]
380pub mod codecs;
381#[cfg(all(
382    feature = "server",
383    any(feature = "storage-seaorm", feature = "storage-seaorm-v2")
384))]
385pub mod comma_separated_value;
386#[cfg(feature = "server")]
387pub mod cookie_template;
388pub mod credentials;
389#[cfg(feature = "server")]
390pub mod errors;
391#[cfg(feature = "server")]
392pub mod gate;
393pub mod groups;
394#[cfg(feature = "server")]
395pub mod hashing;
396pub mod permissions;
397pub mod prelude;
398#[cfg(feature = "server")]
399pub mod repositories;
400pub mod roles;
401#[cfg(feature = "server")]
402pub mod route_handlers;
403#[cfg(feature = "server")]
404pub mod secrets;
405#[cfg(feature = "server")]
406pub mod verification_result;