Secure authentication and authorization system built with Rust (Axum) and SvelteKit. Local authentication uses PASETO V4 tokens signed by HashiCorp Vault, with Redis Sentinel for session management and MariaDB for user storage. External authentication (OAuth2/OIDC via social providers) is supported through Rauthy as the identity provider.
graph LR
spa["SvelteKit SPA\n(frontend)"]
nginx["Nginx :8080\n(reverse proxy)"]
issuer["Issuer :3000\n(auth service)"]
api["API :3001\n(protected API)"]
rauthy["Rauthy :8090\n(IdP / OAuth2)"]
subgraph infra["Infrastructure"]
mariadb["MariaDB"]
redis["Redis Sentinel"]
vault["Vault\n(Transit / Ed25519)"]
end
spa -->|HTTP| nginx
nginx -->|auth routes| issuer
nginx -->|protected routes| api
issuer -->|user storage| mariadb
issuer -->|session / role cache| redis
issuer -->|sign tokens| vault
issuer -->|BFF token exchange| rauthy
api -->|role lookup| redis
api -->|verify tokens| vault
spa -->|"OAuth2/OIDC\nlogin redirect"| rauthy
| Crate | Role | Depends On |
|---|---|---|
rust-backend-issuer |
Login, token refresh, logout, OAuth2/OIDC | MariaDB, Redis, Vault |
rust-backend-api |
Protected endpoints (dashboard, admin) | Redis, Vault |
shared |
Domain traits, adapters, middleware | — |
Handlers (HTTP) → Services (business logic) → Domain (traits/models) ← Adapters (implementations)
shared/src/domain/ — pure traits and models, no external dependenciesissuer/src/services/, api/src/services/ — use-case implementationsissuer/src/handlers/, api/src/handlers/ — Axum HTTP handlersshared/src/adapters/ (Redis, Vault), issuer/src/adapters/mariadb/ (sqlx)shared/src/middleware/role_middleware.rs — PASETO verification + role-based access controlPOST /v1/auth/login verifies credentials (bcrypt), issues a PASETO V4.public access token signed by Vault Transit (Ed25519), and stores a random refresh token in Redis.refresh_token:by_token:{token} ↔ refresh_token:by_uid:{uid}).role_middleware verifies the PASETO token, fetches roles from Redis (role:{uid}), and enforces RequiredRoles. Returns 401 Unauthorized or 403 Forbidden on failure.bff confidential client handles server-side token exchange. Additional clients can be registered via OAUTH_CLIENTS_JSON.| Layer | Technology |
|---|---|
| Backend Framework | Axum 0.8 |
| Tokens | PASETO V4.public (pasetors) |
| Key Management | HashiCorp Vault Transit (Ed25519) |
| Database | MariaDB (sqlx) |
| Cache / Sessions | Redis + Sentinel (deadpool-redis) |
| OAuth2 / OIDC Provider | Rauthy 0.34.3 |
| Frontend | SvelteKit 2 + Svelte 5 (static SPA) |
| Styling | Tailwind CSS v4 |
| API Client | openapi-fetch |
| API Docs | utoipa + Swagger UI |
| Reverse Proxy | Nginx 1.29 |
| E2E Tests | Playwright |
PASETO (Platform-Agnostic Security Tokens) was chosen over JWT for the following reasons:
| Concern | JWT | PASETO V4.public |
|---|---|---|
| Algorithm agility | Vulnerable — alg: none and weak algorithm substitution attacks are well-known |
Not applicable — algorithm is fixed by the version (v4.public = Ed25519) |
| Key confusion | RS256 vs HS256 confusion attacks possible |
No confusion — public/secret key roles are unambiguous |
| Signing | Any algorithm selectable at runtime | Ed25519 only, signed by Vault Transit (HSM-backed) |
| Footer | Not standardized | Structured, authenticated footer for key ID and metadata |
| Spec clarity | Complex, many optional parts | Simple, opinionated, hard to misuse |
In this project, PASETO tokens are signed by HashiCorp Vault Transit (Ed25519), meaning the private key never leaves Vault — adding an additional layer of protection even if the application server is compromised.
Copy the example environment file and fill in your values before starting the stack:
cp docker/.env.example docker/.env
# Edit docker/.env with your values (Vault token, database credentials, BFF secret, etc.)
Then start all services:
cd docker
docker compose up -d
This starts: Nginx, Issuer, API, Vault, MariaDB, Redis (master + sentinel), and Rauthy (OAuth2/OIDC provider).
Note — Development environment only: All inter-service communication uses HTTP. When deploying to production, switch all endpoints to HTTPS.
Note — Rauthy setup required: After starting the Docker stack, Rauthy must be configured before OAuth2/OIDC login works. See the Rauthy OAuth2/OIDC Provider Setup section in
docker/README.mdfor instructions.
Test accounts: Pre-seeded development accounts (email / password / roles) are listed in
docker/README.md.
To build and serve the frontend, use the build profile:
# Build the SvelteKit SPA and copy static assets into the shared volume
docker compose --profile build run --rm frontend
# Build all workspace crates
cargo build --manifest-path backend/Cargo.toml
# Build a specific crate
cargo build --manifest-path backend/Cargo.toml -p rust-backend-issuer
cargo build --manifest-path backend/Cargo.toml -p rust-backend-api
# Run tests
cargo test --manifest-path backend/Cargo.toml
# Lint and format
cargo clippy --manifest-path backend/Cargo.toml
cargo fmt --manifest-path backend/Cargo.toml
cd frontend
npm install
npm run dev # Vite dev server at :5173
npm run build # Static SPA build
npm run check # Svelte type checking
npm run lint # ESLint + Prettier
npm test # Vitest
End-to-end tests are located in e2e/ and run against the full Docker stack at http://localhost:8080.
cd e2e
cp .env.example .env # fill in test user credentials
npm install
npx playwright install
npx playwright test --project=chromium
For full setup instructions, CI configuration, and the complete test case list, see e2e/README.md.
Environment variables are loaded via dotenvy + envy (type-safe deserialization into config structs). Copy docker/.env.example to docker/.env and edit the values before running Docker Compose. Never commit secrets to version control.
cp docker/.env.example docker/.env
# Edit docker/.env with your values
| Variable | Description |
|---|---|
RUST_LOG |
Log level (e.g. info, debug) |
SERVER_PORT |
HTTP listen port |
COOKIE_SECURE |
Set true in production (requires HTTPS) |
CORS_ORIGINS |
Comma-separated list of allowed CORS origins |
ACCESS_TOKEN_ISSUER |
PASETO token issuer claim (your domain) |
ACCESS_TOKEN_TTL |
Access token lifetime in seconds |
VAULT_URL |
Vault server address |
VAULT_TOKEN |
Vault authentication token — use a secret manager in production |
VAULT_ALLOW_HTTP |
Set true only in development; must be false in production |
MOUNT_PATH |
Vault Transit mount path |
TRANSIT_KEY |
Vault Transit key name |
MARIADB_URL |
MariaDB connection string — keep secret |
REDIS_SENTINELS |
Comma-separated sentinel addresses |
REDIS_MASTER_NAME |
Redis Sentinel master name |
REDIS_REFRESH_TOKEN_TTL |
Refresh token lifetime in seconds |
REDIS_ROLE_TTL |
Role cache lifetime in seconds |
REDIS_NAMESPACE_* |
Redis key namespace prefixes (see .env.example) |
BFF_CLIENT_SECRET |
Secret for the built-in BFF OAuth2 client — use a long random string in production |
BFF_REDIRECT_URI |
Callback URI registered for the BFF client |
MOBILE_REDIRECT_URIS |
Comma-separated redirect URIs for the built-in mobile public client (optional) |
OAUTH_CLIENTS_JSON |
JSON array of additional OAuth2 client definitions (optional) |
| Variable | Description |
|---|---|
RUST_LOG |
Log level |
SERVER_PORT |
HTTP listen port |
ACCESS_TOKEN_ISSUER |
Must match the issuer service value |
VAULT_URL |
Vault server address |
VAULT_TOKEN |
Vault authentication token |
VAULT_ALLOW_HTTP |
Development-only HTTP override |
MOUNT_PATH |
Vault Transit mount path |
TRANSIT_KEY |
Vault Transit key name |
REDIS_SENTINELS |
Comma-separated sentinel addresses |
REDIS_MASTER_NAME |
Redis Sentinel master name |
REDIS_NAMESPACE_UID_TO_ROLE |
Redis key namespace prefix for role lookups |
See
docker/.env.examplefor the full list of variables and their descriptions.
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /auth/v1/login |
— | Initiate local or OAuth2 login |
| POST | /auth/v1/login/local |
— | Authenticate with username and password (rate-limited) |
| GET | /auth/v1/callback |
— | OAuth2 authorization code callback (BFF flow) |
| POST | /auth/v1/refresh |
— | Refresh the access token via HttpOnly cookie |
| POST | /auth/v1/logout |
— | Invalidate session and clear cookie |
| GET | /auth/v1/oauth/init |
— | Initiate external OAuth2/OIDC login (Rauthy) |
| GET | /auth/v1/oauth/callback |
— | External OAuth2 callback |
| POST | /auth/v1/oauth/link |
— | Link external OAuth2 account |
| GET | /users/v1 |
Admin | List users |
| POST | /users/v1 |
Admin | Create user |
| PUT | /users/v1/{user_id} |
Admin | Update user |
| DELETE | /users/v1/{user_id} |
Admin | Delete user |
| GET | /auth/v1/providers |
Admin | List auth providers |
| POST | /auth/v1/providers |
Admin | Create auth provider |
| PUT | /auth/v1/providers/{id} |
Admin | Update auth provider |
| DELETE | /auth/v1/providers/{id} |
Admin | Deactivate auth provider |
| GET | /.well-known/openid-configuration |
— | OIDC Discovery endpoint |
| GET | /.well-known/jwks.json |
— | OIDC JWKS endpoint |
| GET | /userinfo |
User, Admin | OIDC userinfo endpoint |
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/dashboard |
User, Admin | Dashboard data |
| GET | /swagger-ui |
— | Interactive API documentation (utoipa) |
.
├── backend/
│ ├── Cargo.toml # Workspace root (resolver=2, edition 2024)
│ ├── issuer/ # Authentication service (rust-backend-issuer)
│ │ └── src/
│ │ ├── handlers/ # login, logout, refresh, OAuth2/OIDC, users
│ │ ├── services/ # AuthenticationService, IdTokenVerifier
│ │ ├── adapters/mariadb/
│ │ └── config.rs
│ ├── api/ # Protected API service (rust-backend-api)
│ │ └── src/
│ │ ├── handlers/ # dashboard, admin
│ │ ├── services/ # SystemMonitor
│ │ └── config.rs
│ └── shared/ # Common library crate
│ └── src/
│ ├── domain/ # Traits and models (no external deps)
│ ├── adapters/ # Redis, Vault, Authorization implementations
│ └── middleware/ # role_middleware (PASETO + RBAC)
├── frontend/ # SvelteKit SPA (Svelte 5, Tailwind v4)
│ └── src/
│ ├── lib/
│ │ ├── api/ # openapi-fetch typed client
│ │ ├── components/ui/ # Shared UI components
│ │ └── stores/ # Svelte 5 $state auth store
│ └── routes/
│ ├── login/
│ └── (protected)/ # dashboard, users, auth-providers
├── e2e/ # Playwright end-to-end tests
│ ├── playwright.config.ts
│ ├── helpers/
│ │ └── login.ts # loginAs() / navigateSPA() helpers
│ └── tests/
│ └── auth.spec.ts # GROUP A — Authentication tests
├── docker/
│ ├── docker-compose.yml
│ ├── .env.example # Template — copy to .env before running
│ ├── frontend/ # Frontend builder Dockerfile
│ ├── mariadb/ # Database initialization scripts
│ ├── nginx/ # Nginx reverse proxy configuration
│ ├── rauthy/ # Rauthy OAuth2/OIDC provider config
│ ├── redis/ # Redis Sentinel configuration
│ ├── rust_app/ # Dockerfile for Rust services
│ └── vault/ # Vault dev setup scripts
└── documents/ # Design documents and test scope
| Network | Services |
|---|---|
backend-net |
nginx, rust_issuer, rust_api |
redis-net |
redis-master, sentinel-1, rust_issuer, rust_api |
mariadb-net |
mariadb, rust_issuer |
vault-net |
vault, rust_issuer, rust_api |
rauthy-net |
rauthy, rust_issuer |
| Port | Service | Bound To | Notes |
|---|---|---|---|
8080 |
nginx | 127.0.0.1 |
Frontend + API gateway (primary entry point) |
8090 |
rauthy | 127.0.0.1 |
OAuth2 / OIDC provider UI |
3000 |
rust_issuer | 0.0.0.0 |
Direct backend access (development only) |
3001 |
rust_api | 0.0.0.0 (mapped to internal 3000) |
Direct backend access (development only) |
3306 |
mariadb | 127.0.0.1 |
Database (development only) |
6379 |
redis-master | 127.0.0.1 |
Redis (development only) |
26379 |
sentinel-1 | 127.0.0.1 |
Redis Sentinel (development only) |
In production, ensure that ports
3000,3001,3306,6379, and26379are not exposed to the public internet.
Pull requests and issues are welcome. Please follow the project conventions:
cargo clippy and cargo fmt pass before submitting a PR.403 / 401 flows on new features.This project is released under the MIT License. See the LICENSE file for details.