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