Full-stack authentication for Rust (Axum), React & Next.js
JWT · MFA · OAuth · Sessions · Multi-Tenant · Pure-Rust Crypto · WebAssembly Edge Verification
GitHub · Issues · Quick Start · docs.rs · API Reference · Examples
Important
🚧 Early development. This repository is under active construction. The crates are not yet published to crates.io or npm, and the API described below is the target surface the project is building toward — not all of it exists yet. Badges, version numbers, and install commands reflect the intended 1.0 release. See the development plan for current status.
bymax-auth is a complete authentication and authorization solution shipped as a lean Cargo workspace (crates.io) plus a matching npm package — covering everything from an Axum backend to React hooks, Next.js route handlers, and an edge JWT verifier compiled to WebAssembly.
Instead of stitching together a dozen crates and packages for JWT, MFA, OAuth, sessions, password reset, and brute-force protection, you add one library and get a production-ready auth system that spans your entire stack — with the frontend types generated from the Rust source, so the server and the client can never drift.
- 🎯 One workspace, full stack — A backend crate (
bymax-auth) and an npm package (@bymax-one/rust-auth) in lockstep. Shared types and constants are generated from Rust viats-rsand re-checked in CI — zero manual synchronization, zero drift. - 🔌 Your database, your rules — The library defines async traits (
UserRepository,EmailProvider). You implement them withsqlx,SeaORM,Diesel, or anything else. No ORM dependency, no driver baked in. - 🦀 Pure-Rust crypto only — Password hashing, MFA encryption, TOTP, and token generation run entirely on RustCrypto (
scrypt,argon2,aes-gcm,hmac,sha2,subtle). Noring, no OpenSSL, no C bindings, and#![forbid(unsafe_code)]on every first-party crate. - ⚡ Pay for what you use — A tiny always-compiled core; every heavy integration (Redis, Axum,
reqwest, MFA) is a Cargo feature or a trait you plug. A per-feature dependency budget is enforced in CI, so a minimal build pulls a minimal tree. - 🏢 Multi-tenant ready — Every operation is scoped by
tenant_id, taken from a resolver and never the request body. A separate platform-admin identity domain is isolated from tenant users from day one. - 🌐 Edge-native — The exact same HS256 verifier that runs on the server compiles to
wasm32-unknown-unknownand runs in the Next.js Edge runtime with no network call — one implementation, server and edge, proven by tests.
cargo add bymax-auth --features "argon2,sessions,mfa,oauth,oauth-reqwest,redis,axum"
pnpm add @bymax-one/rust-authNote
Production status. Bymax Live — a Rust-backend + React/Next.js application — is the project's first production consumer (the dogfood target). It will run the library as its authentication and authorization layer (sessions, MFA, OAuth, platform admin, Redis), so the wire contract, the security invariants, and server/edge JWT parity are validated against real traffic rather than synthetic tests alone.
- ✅ Registration & Login — Email/password hashed with scrypt and Argon2id (configurable, rehash-on-verify, self-describing PHC strings)
- ✅ HS256 Access + Opaque Refresh Tokens — Atomic rotation with a grace window for concurrent requests; refresh tokens are opaque and never JWTs
- ✅ Multi-Factor Authentication — TOTP with
otpauth://URI + QR, hashed recovery codes, and a temp-token challenge flow - ✅ OAuth 2.0 — Google built-in, PKCE + single-use
state, account create/link/reject decisioning, MFA branch - ✅ Password Reset — Token-link or OTP, configurable per deployment, uniformly anti-enumerating
- ✅ Email Verification — OTP-based with atomic resend cooldown
- ✅ Pure-Rust Crypto — RustCrypto only (scrypt/Argon2id, AES-256-GCM, HMAC-SHA1 TOTP, HS256); no
ring/OpenSSL/C on any path - ✅ Brute-Force Protection — Fixed-window Redis counters keyed on
HMAC(tenant:email)— no PII in keys - ✅ Session Management — Active-session tracking with FIFO eviction, device/IP metadata, and new-session alerts
- ✅ HttpOnly Cookies —
Secure-by-default, refresh cookie path-scoped withSameSite=Strict, plus a non-HttpOnlyhas_sessionsignal - ✅ Constant-Time Comparisons — Every secret/token/OTP/recovery-code compare goes through
subtle— never==on secret bytes - ✅ JWT Revocation — Instant access-token revocation via a Redis
jtiblacklist - ✅ Anti-Enumeration — Identical status, body, and timing for known vs. unknown accounts, with an always-run sentinel hash
- ✅ Tenant Isolation — All operations scoped by
tenant_idvia a configurableTenantIdResolver(anti-spoofing: the body is ignored) - ✅ Platform Admin Auth — A separate identity domain with its own claims, sessions, and role hierarchy — fully isolated from tenant users
- ✅ User Invitations — Single-use tokenized invites with role + tenant assignment and re-validation on accept
- ✅ Role-Based Access Control — Hierarchical roles enforced by the
RequireRole<R>extractor
- ✅ Full-Stack Typed — Rust domain types → TypeScript via
ts-rs, drift-gated in CI - ✅ Two Published Artifacts —
bymax-authon crates.io,@bymax-one/rust-authon npm, versioned in lockstep - ✅ Builder + Validated Config — Assemble the engine with
AuthEngineBuilder;build()fails fast with a typedConfigError - ✅ Trait-Pluggable — Bring your own database, email transport, HTTP client, and stores
- ✅ Axum-Native Extractors —
FromRequestPartsguards (AuthUser,RequireRole<R>, …) — no middleware soup, no Passport equivalent
The backend ships on crates.io; the frontend ships on npm. The WebAssembly edge verifier is bundled inside the npm package (never published as a standalone crate).
A thin facade over a set of focused internal crates. You enable only the capabilities you need; the always-compiled core stays tiny.
| Feature | Enables |
|---|---|
scrypt (default) |
Default password KDF (parity baseline) — drop-in scrypt |
argon2 |
Argon2id KDF + the hardened AuthConfig::secure_defaults() profile |
sessions |
Redis-backed session tracking + FIFO eviction |
mfa |
TOTP setup/verify/challenge, recovery codes (AES-256-GCM secret at rest) |
oauth |
OAuth 2.0 + PKCE orchestration (transport injected — zero HTTP deps) |
oauth-reqwest |
Bundled reqwest-backed HttpClient (opt-in; omit to supply your own) |
platform |
Platform-admin identity domain (login/MFA/sessions) |
invitations |
Tokenized user invitations |
redis |
Canonical Redis stores (redis + deadpool-redis) with atomic Lua |
axum |
HTTP router, extractors, DTO validation, cookie delivery, per-route rate limiting, WebSocket tickets |
client |
Native Rust typed auth client |
full |
Every backend feature above |
Note
There is no core feature — the engine is always compiled and individual flows are runtime toggles. When a feature is disabled, its crates and dependencies are never linked (a no-MFA build pulls in none of aes-gcm/sha1).
One package, four entry points — import only what your app needs:
| Subpath | Import | Purpose | Peer deps |
|---|---|---|---|
| Shared | @bymax-one/rust-auth/shared |
Types, constants, error codes — generated from Rust | None |
| Client | @bymax-one/rust-auth/client |
Native-fetch client with single-flight refresh |
None |
| React | @bymax-one/rust-auth/react |
AuthProvider + hooks |
React 19 |
| Next.js | @bymax-one/rust-auth/nextjs |
Proxy, route handlers, WASM-backed edge JWT verify | Next.js 16 |
shared (generated, zero deps)
↗ ↖
client (Rust crate: bymax-auth-client)
↑
react
↑
nextjs ──→ bymax-auth-wasm (edge HS256 verifier)
Tip
Prefer to learn from working code? The examples/ directory ships runnable apps — axum-minimal, axum-mfa, axum-oauth-google, react-vite, and nextjs — each built and linted in CI so they never rot.
# Cargo.toml
[dependencies]
bymax-auth = { version = "1", features = ["argon2", "sessions", "mfa", "oauth", "oauth-reqwest", "redis", "axum"] }The library defines what it needs — your app provides how. Map the abstract AuthUser contract onto your own schema; the only invariant is that password_hash is persisted exactly as the library produced it (a self-describing PHC string).
use async_trait::async_trait;
use bymax_auth::{
AuthUser, CreateUserData, CreateWithOAuthData, RepositoryError, UpdateMfaData, UserRepository,
};
pub struct PgUserRepository {
pool: sqlx::PgPool,
}
#[async_trait]
impl UserRepository for PgUserRepository {
async fn find_by_email(
&self,
email: &str,
tenant_id: &str,
) -> Result<Option<AuthUser>, RepositoryError> {
sqlx::query_as::<_, AuthUser>(
"SELECT * FROM users WHERE email = $1 AND tenant_id = $2",
)
.bind(email.to_lowercase())
.bind(tenant_id)
.fetch_optional(&self.pool)
.await
.map_err(RepositoryError::backend)
}
async fn create(&self, data: CreateUserData) -> Result<AuthUser, RepositoryError> {
// INSERT … RETURNING *, storing data.password_hash verbatim.
todo!()
}
async fn update_password(&self, id: &str, password_hash: &str) -> Result<(), RepositoryError> {
todo!()
}
async fn update_mfa(&self, id: &str, data: UpdateMfaData) -> Result<(), RepositoryError> {
todo!()
}
async fn find_by_oauth_id(
&self,
provider: &str,
provider_id: &str,
tenant_id: &str,
) -> Result<Option<AuthUser>, RepositoryError> {
todo!()
}
async fn create_with_oauth(
&self,
data: CreateWithOAuthData,
) -> Result<AuthUser, RepositoryError> {
todo!()
}
// … link_oauth, update_status, update_email_verified, etc.
}Email delivery is fully delegated — the library never imports a mailer SDK. It passes structured data (token, OTP, session info, invite data), never rendered HTML.
use async_trait::async_trait;
use bymax_auth::{EmailProvider, InviteData, SessionInfo};
pub struct ResendEmailProvider { /* client, from, app_url */ }
#[async_trait]
impl EmailProvider for ResendEmailProvider {
async fn send_password_reset_token(&self, email: &str, token: &str, locale: Option<&str>) {
// Render and send with your transport of choice (Resend/SES/SMTP).
}
async fn send_email_verification_otp(&self, email: &str, otp: &str, locale: Option<&str>) {
// …
}
async fn send_new_session_alert(&self, email: &str, info: &SessionInfo, locale: Option<&str>) {
// …
}
async fn send_invitation(&self, email: &str, data: &InviteData, locale: Option<&str>) {
// …
}
// … password-reset OTP, MFA enabled/disabled notifications, etc.
}Warning
Any consumer-supplied value (display name, tenant name, inviter name, device string) interpolated into an HTML email body MUST be escaped to prevent stored XSS. Tokens and OTPs are library-generated and safe; the placeholders you fill in are not.
build() runs full startup validation and returns Result<AuthEngine, ConfigError> — a misconfiguration fails fast at boot, never at the first request.
use std::sync::Arc;
use bymax_auth::{
auth_router, AuthConfig, AuthEngine, AxumAuthConfig, Environment, RedisStores,
};
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let engine = AuthEngine::builder()
.config(AuthConfig::secure_defaults()) // or AuthConfig::nest_compat_defaults()
.environment(Environment::Production) // resolves Secure cookies + prod redirect checks
.user_repository(Arc::new(PgUserRepository { pool }))
.email_provider(Arc::new(ResendEmailProvider { /* … */ }))
.redis_stores(Arc::new(RedisStores::connect("redis://127.0.0.1").await?))
.oauth_provider(Arc::new(bymax_auth::GoogleProvider::new(/* client id/secret */)))
.http_client(Arc::new(bymax_auth::ReqwestHttpClient::default()))
.build()?;
let app = auth_router(AxumAuthConfig::new(Arc::new(engine)));
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
axum::serve(listener, app).await?;
Ok(())
}Guards are Axum FromRequestParts extractors — compose them straight into a handler signature. They read the token from a cookie or Authorization: Bearer header — never from the query string.
use axum::Json;
use bymax_auth::{AuthUser, RequireRole, SafeAuthUser, SelfOrAdmin};
// Any authenticated dashboard user.
async fn profile(user: AuthUser) -> Json<SafeAuthUser> {
Json(user.into_safe())
}
// Admins (and anything above them in the hierarchy) only.
async fn list_users(_: RequireRole<{ "admin" }>) -> &'static str {
"ok"
}
// The resource owner, or an admin.
async fn update_user(_: SelfOrAdmin) -> &'static str {
"ok"
}Build an AuthClient once, hand it to AuthProvider; the hooks read the context it populates — exactly as in @bymax-one/nest-auth, so existing consumers change only the import specifier.
// app/providers.tsx
'use client'
import { AuthProvider } from '@bymax-one/rust-auth/react'
import { createAuthClient } from '@bymax-one/rust-auth/client'
const authClient = createAuthClient({
// Same-origin calls flow through the Next.js proxy under `/api/auth/*`.
// Set `baseUrl` only for a cross-origin API.
})
export function Providers({ children }: { children: React.ReactNode }) {
return (
<AuthProvider client={authClient} onSessionExpired={() => (location.href = '/login')}>
{children}
</AuthProvider>
)
}// app/(dashboard)/profile.tsx
'use client'
import { useAuth, useSession } from '@bymax-one/rust-auth/react'
export function Profile() {
const { user, status } = useSession()
const { logout } = useAuth()
if (status === 'loading') return <div>Loading…</div>
if (status === 'unauthenticated') return <div>Please log in</div>
return (
<div>
<p>Welcome, {user.name}!</p>
<button onClick={() => logout()}>Sign out</button>
</div>
)
}Mount the auth proxy at the project root and expose the /api/auth/* route handlers. The proxy verifies the access token at the edge using the WebAssembly build of the same HS256 verifier the backend uses — no network round-trip.
// proxy.ts — Next.js 16 Edge middleware
import { createAuthProxy } from '@bymax-one/rust-auth/nextjs'
export const { proxy } = createAuthProxy({
publicRoutes: ['/', '/auth/login', '/auth/register'],
protectedRoutes: [
{ pattern: '/dashboard/:path*', allowedRoles: ['admin', 'member'] },
{ pattern: '/admin/:path*', allowedRoles: ['admin'] },
],
loginPath: '/auth/login',
apiBase: process.env.API_BASE_URL!,
jwtSecret: process.env.JWT_SECRET, // verified at the edge via WASM
cookieNames: { access: 'access_token', refresh: 'refresh_token', hasSession: 'has_session' },
blockedUserStatuses: ['BANNED', 'INACTIVE', 'SUSPENDED'],
})
export const config = { matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'] }Important
The WASM module and verifyJwtToken are server/edge only — never import them into a Client Component. The HS256 secret must never reach the browser; an accidental client import is a security defect (a server-only guard catches it at build time).
// app/api/auth/logout/route.ts
import { createLogoutHandler } from '@bymax-one/rust-auth/nextjs'
export const POST = createLogoutHandler({
apiBase: process.env.API_BASE_URL!,
mode: 'redirect',
loginPath: '/auth/login',
cookieNames: { access: 'access_token', refresh: 'refresh_token', hasSession: 'has_session' },
})Everything is configured through AuthConfig. Two ready-made profiles bundle sensible choices; Default equals nest_compat_defaults().
| Profile | Posture |
|---|---|
AuthConfig::nest_compat_defaults() |
Behavioral parity with @bymax-one/nest-auth out of the box (scrypt, email verification required, brute-force max_attempts = 5) — the Default |
AuthConfig::secure_defaults() |
Hardened opt-in profile (Argon2id, stricter cookies) — available under the argon2 feature |
| Group | Key options | nest-compat default |
|---|---|---|
| jwt | secret (required, ≥ 32 chars), access_ttl, refresh_expires_in_days |
15m, 7d, HS256 (pinned) |
| password | active_algorithm, scrypt cost_factor / Argon2id memory_kib |
scrypt N=2¹⁵, r=8, p=1 |
| token_delivery | Cookie | Bearer | Both |
Cookie |
| cookies | names, refresh_cookie_path, same_site, resolve_domains |
HttpOnly, Secure, Strict |
| mfa | encryption_key (32 bytes), issuer, totp_window, recovery_code_count |
— |
| sessions | enabled, default_max_sessions, max_sessions_resolver |
false, 5 |
| brute_force | max_attempts, window_seconds |
5, 900 |
| password_reset | method (Token | Otp), otp_length, token_ttl |
Token, 600 s |
| platform | enabled (requires roles.platform_hierarchy) |
false |
| invitations | enabled, token_ttl |
false, 48 h |
| roles | hierarchy (required), platform_hierarchy |
— |
| oauth | google, redirect_allowlist, *_redirect_url |
— |
| controllers | per-group route toggles | feature-driven |
Note
build() validates every cross-field invariant (secret length/entropy, role referential integrity, parameter floors, SameSite=None ⇒ Secure, OAuth redirect allow-listing, required stores) and rejects an invalid config with a precise ConfigError.
The library is a set of crates you embed in your Axum service — a framework-agnostic core wrapped by thin adapters.
┌──────────────────────────────────────────────────────────┐
│ Your Axum Application │
│ │
│ bymax-auth-axum → router · extractors · rate-limit │
│ │ │
│ bymax-auth-core → AuthEngine · services · 14 hooks │
│ ╱ │ ╲ │
│ -crypto -jwt -types (pure-Rust, wasm-safe) │
│ │ │
│ ┌────▼─────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ User │ │ Email │ │ SessionStore │ │
│ │ Repository│ │ Provider │ │ (bymax-auth- │ │
│ │ (yours) │ │ (yours) │ │ redis) │ │
│ └──────────┘ └──────────────┘ └──────────────┘ │
└──────────────────────────────────────────────────────────┘
bindings/bymax-auth-wasm → edge HS256 verifier (npm)
| Principle | Description |
|---|---|
| 🔌 Trait-Driven | Define contracts, inject implementations — works with sqlx, SeaORM, Diesel, or any store |
| 🔒 Secure by Default | scrypt/Argon2id, HttpOnly cookies, jti blacklist, brute-force, anti-enumeration — all on out of the box |
| 🪶 Tiny Mandatory Tree | A small always-compiled core; Redis, Axum, reqwest, and MFA are feature-gated, with a CI dependency budget |
| 🦀 One Crypto, Everywhere | A single pure-Rust HS256 primitive runs on the server and at the edge (WASM) — never two implementations |
| 🌳 wasm-clean boundary | bymax-auth-wasm depends only on -crypto/-jwt/-types; a CI job builds wasm32 to prove it |
| ⚡ Fast & measured | No GC and no FFI boundary; hot paths run in ns–µs while memory-hard KDFs stay tunable — tracked with Criterion benches (details) |
The security architecture is codified as a set of inviolable invariants that the quality gates exist to protect — relaxing one is treated as a vulnerability, not a feature.
Every token carries a type claim that extractors validate before acceptance:
| Token type | Issued when | Accepted by |
|---|---|---|
dashboard |
Successful login or MFA challenge | AuthUser |
platform |
Platform-admin login or MFA challenge | PlatformUser |
mfa_challenge |
Login with MFA enabled (pre-verification) | MFA challenge endpoint |
This prevents token-type-confusion attacks. A dashboard token presented to a platform route is rejected with PlatformAuthRequired (not the generic TokenInvalid), so clients can distinguish wrong-context from expired.
Verification pins header.alg == "HS256" before any signature math — none, RS256, ES256, and every asymmetric algorithm are rejected, and only a symmetric key exists. Refresh tokens are opaque CSPRNG strings, inert without their server-side Redis record, persisted only as sha256(token) — never signed or parsed as JWTs.
Platform admins and tenant users are fully isolated — separate repositories, claims, extractors, and routes. tenant_id is always taken from the configured TenantIdResolver, never the request body, preventing tenant spoofing at the architecture level.
Every read→decide→write on contended state — refresh rotation + grace, ownership-checked revoke, brute-force increment, OTP verify+consume, single-use WebSocket ticket — runs as a single atomic Lua script, closing the concurrency races those flows would otherwise expose.
When integrating bymax-auth in production, verify each of the following:
cookies.resolve_domainsvalidates against an allow-list — theHostheader is never used to derive a parent domainoauth.redirect_allowlistis set so no redirect/callback URL is request-derived (no open redirect)- The MFA
encryption_keyis 32 bytes from a secret manager; the JWTsecretis 32 random bytes, high-entropy - HS256 pinning, constant-time comparisons (
subtle), and CSPRNG (OsRng) are never bypassed - HttpOnly +
Secure(outside development); refresh cookie path-scoped withSameSite=Strict
| Layer | Implementation |
|---|---|
| Password Hashing | RustCrypto scrypt (N=2¹⁵, r=8, p=1) or argon2 Argon2id (PHC) |
| MFA Encryption | aes-gcm AES-256-GCM with a fresh 12-byte CSPRNG IV per call |
| TOTP | hmac + sha1 per RFC 4226/6238, ±1 step window, anti-replay marked |
| Recovery Codes | Keyed HMAC-SHA-256 digests (never plaintext, never reversible) |
| Token Generation | getrandom/OsRng CSPRNG — 256 bits of entropy |
| Secret Comparison | subtle constant-time — never == on secret bytes |
| JWT | Hand-rolled HS256 (hmac + sha2), jti blacklist via Redis |
| Cookies | HttpOnly, Secure-by-default, SameSite=Strict, path-scoped refresh |
| Brute-Force | Redis atomic fixed-window counters per HMAC(tenant:email) |
| CSRF (OAuth) | 64-hex single-use state (GETDEL) + PKCE code_verifier (S256) |
| Edge Verify | Same HS256 primitive compiled to WebAssembly — no network call |
Important
All cryptography uses RustCrypto — no ring, no OpenSSL, no C/C++ bindings on any target — and every first-party crate is #![forbid(unsafe_code)] (the sole exception being the wasm-bindgen glue in the edge binding).
Rust's advantage over a managed runtime (Node, the JVM) is per-operation cost with no garbage collector and no FFI marshalling. bymax-auth is built to spend that advantage deliberately — and to prove it with benchmarks rather than assert it.
| Lever | What it buys |
|---|---|
| No runtime, no GC | Auth work is synchronous CPU over owned bytes — no allocator pauses, no event-loop hops, predictable tail latency under load |
| Pure-Rust RustCrypto | Every primitive is inlined Rust — no C bindings and no per-call FFI boundary to cross (the cost a native Node addon pays on every call) |
| Static where it's hot | Internal services are monomorphized; dynamic dispatch (Arc<dyn _>) is paid only at the host-pluggable trait boundary, never in the inner loop |
| Allocation-aware | Digests return a stack [u8; 32]; fixed-size randomness uses a stack [u8; N] instead of a Vec; transient key material lives in Zeroizing buffers wiped on drop |
| Pay for what you use | A bare build is a tiny core — Redis, Axum, OAuth, and MFA are feature-gated behind a CI dependency budget, so unused capabilities cost zero binary and zero attack surface |
| Tiny edge | The Next.js middleware verifies JWTs through a wasm-bindgen module — the same HS256 code as the server, with no network round-trip |
Password hashing is deliberately expensive: scrypt and Argon2id are memory-hard so an attacker who exfiltrates the hash store cannot brute-force it cheaply. Their cost is a security knob (cost_factor / memory_kib), not a bottleneck — and it is the one operation the engine hands to tokio::task::spawn_blocking, so a burst of logins never stalls the async runtime. Everything around the KDF stays in the nanosecond–microsecond range.
Tracked with Criterion so a regression surfaces as a number — the same discipline applied to the coverage gate.
| Primitive (per call) | Median | Role |
|---|---|---|
SHA-256 (mac::sha256) |
~110 ns | high-entropy token → Redis key suffix |
| Keyed HMAC-SHA-256 | ~430 ns | low-entropy identifier / recovery-code hashing |
| Secure token (32 B → hex) | ~870 ns | dominated by the OS CSPRNG syscall, not allocation |
| AES-256-GCM encrypt / decrypt | ~2.1 µs / ~1.3 µs | TOTP secret encrypted at rest |
| TOTP generate / verify (±1 window) | ~200 ns / ~710 ns | RFC 6238, constant-time |
| scrypt hash / verify (N=2¹⁵) | ~37 ms | memory-hard — tunable security cost |
| Argon2id hash / verify (19 MiB) | ~10 ms | memory-hard — tunable security cost |
Indicative medians on an Apple M4 Max, release profile, Rust 1.96. Reproduce with cargo bench -p bymax-auth-crypto --bench crypto --all-features. Absolute figures are hardware-dependent — the point is the order of magnitude and that the numbers are tracked, not hand-waved.
Note
The cheap operations cost nanoseconds to microseconds; the only deliberately slow step is the memory-hard KDF, which is a security control you dial in — not a hot loop. Optimisation is a standing project premise, but it never outranks a constant-time, zeroize, or no-oracle guarantee.
Authentication is critical infrastructure, so the suite is held to a bar beyond "it compiles" — every behavior is pinned so a regression fails a test.
- ✅ 100% line + region coverage — enforced as a release gate via
cargo-llvm-covacross the fullcargo-hackfeature matrix - ✅ Near-100% mutation score — verified with
cargo-mutants: faults are seeded into the source and the suite must catch them - ✅ Property tests + fuzzing —
proptestround-trips andcargo-fuzzsmoke runs over the trust-boundary parsers (JWT, PHC, base32) - ✅ Real-Redis E2E — atomic Lua, rotation/grace, and revocation proven against
redis:8viatestcontainers - ✅ Edge parity —
wasm-bindgen-testconfirms the WASM verifier accepts a token signed by the backend - ✅ Supply chain —
cargo-deny(advisories/licenses/bans/sources),cargo-vet, a dependency budget, andcargo-public-api+cargo-semver-checkson every PR
cargo test --workspace --all-features # unit + integration
cargo llvm-cov --workspace --all-features # 100% line/region gate
cargo mutants # mutation testing
cargo deny check # supply-chain policy
wasm-pack test --node bindings/bymax-auth-wasmNote
Line coverage proves a line executed; mutation testing proves a test would fail if that line were wrong. Both run in CI, alongside a wasm32-unknown-unknown build that guarantees the edge crate stays free of server dependencies.
Route groups mount only when their feature and runtime toggle are enabled, so an unconfigured group contributes zero routes. Paths are shown under the default auth prefix.
| Method | Path | Guard / Auth | Description |
|---|---|---|---|
| POST | /auth/register |
Public | Register a dashboard user and issue tokens |
| POST | /auth/login |
Public | Authenticate (may return an MFA challenge) |
| POST | /auth/logout |
AuthUser |
Revoke the access jti and the refresh session |
| POST | /auth/refresh |
Public (refresh cookie) | Rotate the refresh token, issue a new access token |
| GET | /auth/me |
AuthUser |
Current dashboard user |
| POST | /auth/verify-email |
Public | Verify email with an OTP |
| POST | /auth/resend-verification |
Public | Resend the email-verification OTP |
| POST | /auth/password/forgot-password |
Public | Request a password reset (token or OTP) |
| POST | /auth/password/reset-password |
Public | Submit a new password |
| POST | /auth/password/verify-otp |
Public | Verify a password-reset OTP |
| POST | /auth/password/resend-otp |
Public | Resend the password-reset OTP |
| POST | /auth/mfa/setup |
AuthUser |
Generate the TOTP secret + recovery codes |
| POST | /auth/mfa/verify-enable |
AuthUser |
Confirm setup and enable MFA |
| POST | /auth/mfa/challenge |
Public (MFA temp token) | Submit a TOTP / recovery code after login |
| POST | /auth/mfa/disable |
AuthUser |
Disable MFA |
| POST | /auth/mfa/recovery-codes |
AuthUser |
Regenerate recovery codes (TOTP-gated) |
| GET | /auth/sessions |
AuthUser, UserStatus |
List active sessions |
| DELETE | /auth/sessions/all |
AuthUser, UserStatus |
Revoke all sessions |
| DELETE | /auth/sessions/:id |
AuthUser, UserStatus |
Revoke a specific session (ownership-checked) |
| POST | /auth/invitations |
AuthUser |
Create a tenant invitation |
| POST | /auth/invitations/accept |
Public | Accept an invitation and create the user |
| POST | /auth/platform/login |
Public | Platform-admin login (separate context) |
| POST | /auth/platform/mfa/challenge |
Public | Platform-admin MFA challenge |
| GET | /auth/platform/me |
PlatformUser |
Current platform admin |
| POST | /auth/platform/logout |
PlatformUser |
Revoke platform tokens |
| POST | /auth/platform/refresh |
Public (platform refresh cookie) | Rotate the platform refresh token |
| DELETE | /auth/platform/sessions |
PlatformUser |
Revoke all platform sessions |
| GET | /auth/oauth/:provider |
Public | Initiate the OAuth authorize redirect |
| GET | /auth/oauth/:provider/callback |
Public | Handle the callback, exchange the code, issue tokens |
| POST | /auth/ws-ticket |
AuthUser, UserStatus, MfaSatisfied |
Mint a single-use WebSocket upgrade ticket |
| Extractor | Purpose |
|---|---|
AuthUser |
Validates the dashboard JWT (cookie or Authorization header) |
OptionalAuthUser |
Routes that differ for anonymous vs. authenticated users |
RequireRole<R> |
Hierarchical role check |
PlatformUser |
Platform-admin JWT validation |
RequirePlatformRole<R> |
Platform role hierarchy enforcement |
CurrentUser |
Extracts the validated claims |
SelfOrAdmin |
Ownership-or-admin access |
UserStatus |
Blocks inactive/banned users (Redis-cached status) |
MfaSatisfied |
Enforces a completed MFA verification on the request |
| Hook | Returns |
|---|---|
useSession() |
{ user, status, isLoading, refresh(), lastValidation } — session state + revalidate |
useAuth() |
{ login(), logout(), register(), forgotPassword(), resetPassword() } — auth actions |
useAuthStatus() |
{ isAuthenticated, isLoading } — derived state |
| Factory | Type | Purpose |
|---|---|---|
createAuthProxy() |
Proxy config | Auth-aware edge proxy for proxy.ts |
createSilentRefreshHandler() |
GET handler | iframe-based token refresh |
createClientRefreshHandler() |
POST handler | Client-triggered token refresh |
createLogoutHandler() |
POST handler | Clear tokens and session |
verifyJwtToken() |
Edge helper | WASM-backed HS256 verification (server/edge only) |
Contributions are welcome! Please read the contributing guidelines before opening a pull request.
# Clone the repository
git clone https://github.com/bymaxone/rust-auth.git
cd rust-auth
# Build the workspace
cargo build --workspace --all-features
# Run the test suite
cargo test --workspace --all-features
# Lint + format
cargo clippy --workspace --all-targets --all-features -- -D warnings
cargo fmt --all -- --check
# Frontend package
pnpm install && pnpm --filter @bymax-one/rust-auth buildIf you discover a security vulnerability, please do not open a public issue. Email support@bymax.one with the details. We take security seriously and will respond promptly. See SECURITY.md.
Built with ❤️ and 🦀 by Bymax One