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