feat: update configuration and README for improved authentication and database support

This commit is contained in:
2026-03-05 01:49:37 +01:00
parent 9ca4eeddb4
commit 690425e144
4 changed files with 131 additions and 225 deletions

View File

@@ -2,82 +2,64 @@
# K-Template Configuration # K-Template Configuration
# ============================================================================ # ============================================================================
# Copy this file to .env and adjust values for your environment. # Copy this file to .env and adjust values for your environment.
# All values shown are defaults or examples.
# ============================================================================ # ============================================================================
# Server Configuration # Server
# ============================================================================ # ============================================================================
HOST=127.0.0.1 HOST=127.0.0.1
PORT=3000 PORT=3000
# ============================================================================ # ============================================================================
# Database Configuration # Database
# ============================================================================ # ============================================================================
# SQLite (default) # SQLite (default)
DATABASE_URL=sqlite:data.db?mode=rwc DATABASE_URL=sqlite:data.db?mode=rwc
# PostgreSQL (alternative - requires postgres feature) # PostgreSQL (requires postgres feature flag)
# DATABASE_URL=postgres://user:password@localhost:5432/mydb # DATABASE_URL=postgres://user:password@localhost:5432/mydb
# Connection pool settings
DB_MAX_CONNECTIONS=5 DB_MAX_CONNECTIONS=5
DB_MIN_CONNECTIONS=1 DB_MIN_CONNECTIONS=1
# ============================================================================ # ============================================================================
# Authentication Mode # Cookie Secret
# ============================================================================ # ============================================================================
# Options: session, jwt, both # Used to encrypt the OIDC state cookie (CSRF token, PKCE verifier, nonce).
# - session: Cookie-based sessions (requires auth-axum-login feature) # Must be at least 64 characters in production.
# - jwt: Bearer token authentication (requires auth-jwt feature) COOKIE_SECRET=your-cookie-secret-key-must-be-at-least-64-characters-long-for-security!!
# - both: Support both methods (try JWT first, fall back to session)
AUTH_MODE=jwt
# ============================================================================ # Set to true when serving over HTTPS
# Session Configuration (for session/both modes)
# ============================================================================
# Must be at least 64 characters in production
SESSION_SECRET=your-super-secret-key-must-be-at-least-64-characters-long-for-security
# Set to true in production for HTTPS-only cookies
SECURE_COOKIE=false SECURE_COOKIE=false
# ============================================================================ # ============================================================================
# JWT Configuration (for jwt/both modes) # JWT
# ============================================================================ # ============================================================================
# Must be at least 32 characters in production # Must be at least 32 characters in production.
JWT_SECRET=your-jwt-secret-key-at-least-32-chars JWT_SECRET=your-jwt-secret-key-at-least-32-chars
# Optional: JWT issuer and audience for token validation # Optional: embed issuer/audience claims in tokens
JWT_ISSUER=your-app-name # JWT_ISSUER=your-app-name
JWT_AUDIENCE=your-app-audience # JWT_AUDIENCE=your-app-audience
# Token expiry in hours (default: 24) # Token lifetime in hours (default: 24)
JWT_EXPIRY_HOURS=24 JWT_EXPIRY_HOURS=24
# ============================================================================ # ============================================================================
# OIDC Configuration (optional - requires auth-oidc feature) # OIDC (optional requires auth-oidc feature flag)
# ============================================================================ # ============================================================================
# Your OIDC provider's issuer URL (e.g., Keycloak, Auth0, Zitadel) # OIDC_ISSUER=https://your-oidc-provider.com
OIDC_ISSUER=https://your-oidc-provider.com # OIDC_CLIENT_ID=your-client-id
# OIDC_CLIENT_SECRET=your-client-secret
# Client credentials from your OIDC provider # OIDC_REDIRECT_URL=http://localhost:3000/api/v1/auth/callback
OIDC_CLIENT_ID=your-client-id # OIDC_RESOURCE_ID=your-resource-id # optional audience claim to verify
OIDC_CLIENT_SECRET=your-client-secret
# Callback URL (must match what's configured in your OIDC provider)
OIDC_REDIRECT_URL=http://localhost:3000/api/v1/auth/callback
# Optional: Resource ID for audience verification
# OIDC_RESOURCE_ID=your-resource-id
# ============================================================================ # ============================================================================
# CORS Configuration # CORS
# ============================================================================ # ============================================================================
# Comma-separated list of allowed origins
CORS_ALLOWED_ORIGINS=http://localhost:5173,http://localhost:3000 CORS_ALLOWED_ORIGINS=http://localhost:5173,http://localhost:3000
# ============================================================================ # ============================================================================
# Production Mode # Production Mode
# ============================================================================ # ============================================================================
# Set to true/production/1 to enable production checks (secret length, etc.) # Set to true/production/1 to enforce minimum secret lengths and other checks.
PRODUCTION=false PRODUCTION=false

128
Cargo.lock generated
View File

@@ -803,20 +803,6 @@ dependencies = [
"percent-encoding", "percent-encoding",
] ]
[[package]]
name = "futures"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
dependencies = [
"futures-channel",
"futures-core",
"futures-io",
"futures-sink",
"futures-task",
"futures-util",
]
[[package]] [[package]]
name = "futures-channel" name = "futures-channel"
version = "0.3.31" version = "0.3.31"
@@ -1395,8 +1381,8 @@ dependencies = [
[[package]] [[package]]
name = "k-core" name = "k-core"
version = "0.1.10" version = "0.1.11"
source = "git+https://git.gabrielkaszewski.dev/GKaszewski/k-core#7a72f5f54ad45ba82f451e90c44c0581d13194d9" source = "git+https://git.gabrielkaszewski.dev/GKaszewski/k-core#0ea9aa7870d73b5f665241a4183ffd899e628b9c"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-nats", "async-nats",
@@ -1408,12 +1394,9 @@ dependencies = [
"serde", "serde",
"sqlx", "sqlx",
"thiserror 2.0.17", "thiserror 2.0.17",
"time",
"tokio", "tokio",
"tower", "tower",
"tower-http", "tower-http",
"tower-sessions",
"tower-sessions-sqlx-store",
"tracing", "tracing",
"tracing-subscriber", "tracing-subscriber",
"uuid", "uuid",
@@ -1475,7 +1458,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
dependencies = [ dependencies = [
"scopeguard", "scopeguard",
"serde",
] ]
[[package]] [[package]]
@@ -1639,7 +1621,7 @@ version = "5.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51e219e79014df21a225b1860a479e2dcd7cbd9130f4defd4bd0e191ea31d67d" checksum = "51e219e79014df21a225b1860a479e2dcd7cbd9130f4defd4bd0e191ea31d67d"
dependencies = [ dependencies = [
"base64 0.22.1", "base64 0.21.7",
"chrono", "chrono",
"getrandom 0.2.16", "getrandom 0.2.16",
"http", "http",
@@ -2189,25 +2171,6 @@ dependencies = [
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
[[package]]
name = "rmp"
version = "0.8.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ba8be72d372b2c9b35542551678538b562e7cf86c3315773cae48dfbfe7790c"
dependencies = [
"num-traits",
]
[[package]]
name = "rmp-serde"
version = "1.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72f81bee8c8ef9b577d1681a70ebbc962c232461e397b22c208c43c04b67a155"
dependencies = [
"rmp",
"serde",
]
[[package]] [[package]]
name = "rsa" name = "rsa"
version = "0.9.9" version = "0.9.9"
@@ -2707,7 +2670,6 @@ dependencies = [
"sha2", "sha2",
"smallvec", "smallvec",
"thiserror 2.0.17", "thiserror 2.0.17",
"time",
"tokio", "tokio",
"tokio-stream", "tokio-stream",
"tracing", "tracing",
@@ -2792,7 +2754,6 @@ dependencies = [
"sqlx-core", "sqlx-core",
"stringprep", "stringprep",
"thiserror 2.0.17", "thiserror 2.0.17",
"time",
"tracing", "tracing",
"uuid", "uuid",
"whoami", "whoami",
@@ -2832,7 +2793,6 @@ dependencies = [
"sqlx-core", "sqlx-core",
"stringprep", "stringprep",
"thiserror 2.0.17", "thiserror 2.0.17",
"time",
"tracing", "tracing",
"uuid", "uuid",
"whoami", "whoami",
@@ -2859,7 +2819,6 @@ dependencies = [
"serde_urlencoded", "serde_urlencoded",
"sqlx-core", "sqlx-core",
"thiserror 2.0.17", "thiserror 2.0.17",
"time",
"tracing", "tracing",
"url", "url",
"uuid", "uuid",
@@ -3129,22 +3088,6 @@ dependencies = [
"tracing", "tracing",
] ]
[[package]]
name = "tower-cookies"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "151b5a3e3c45df17466454bb74e9ecedecc955269bdedbf4d150dfa393b55a36"
dependencies = [
"axum-core",
"cookie",
"futures-util",
"http",
"parking_lot",
"pin-project-lite",
"tower-layer",
"tower-service",
]
[[package]] [[package]]
name = "tower-http" name = "tower-http"
version = "0.6.8" version = "0.6.8"
@@ -3176,71 +3119,6 @@ version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
[[package]]
name = "tower-sessions"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43a05911f23e8fae446005fe9b7b97e66d95b6db589dc1c4d59f6a2d4d4927d3"
dependencies = [
"async-trait",
"http",
"time",
"tokio",
"tower-cookies",
"tower-layer",
"tower-service",
"tower-sessions-core",
"tower-sessions-memory-store",
"tracing",
]
[[package]]
name = "tower-sessions-core"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce8cce604865576b7751b7a6bc3058f754569a60d689328bb74c52b1d87e355b"
dependencies = [
"async-trait",
"axum-core",
"base64 0.22.1",
"futures",
"http",
"parking_lot",
"rand 0.8.5",
"serde",
"serde_json",
"thiserror 2.0.17",
"time",
"tokio",
"tracing",
]
[[package]]
name = "tower-sessions-memory-store"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb05909f2e1420135a831dd5df9f5596d69196d0a64c3499ca474c4bd3d33242"
dependencies = [
"async-trait",
"time",
"tokio",
"tower-sessions-core",
]
[[package]]
name = "tower-sessions-sqlx-store"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e054622079f57fc1a7d6a6089c9334f963d62028fe21dc9eddd58af9a78480b3"
dependencies = [
"async-trait",
"rmp-serde",
"sqlx",
"thiserror 1.0.69",
"time",
"tower-sessions-core",
]
[[package]] [[package]]
name = "tracing" name = "tracing"
version = "0.1.44" version = "0.1.44"

148
README.md
View File

@@ -5,10 +5,10 @@ A production-ready, modular Rust API template for K-Suite applications, followin
## Features ## Features
- **Hexagonal Architecture**: Clear separation between Domain, Infrastructure, and API layers - **Hexagonal Architecture**: Clear separation between Domain, Infrastructure, and API layers
- **Multiple Auth Modes**: Session-based, JWT, or both - fully configurable - **JWT-Only Authentication**: Stateless Bearer token auth — no server-side sessions
- **OIDC Integration**: Connect to any OpenID Connect provider (Keycloak, Auth0, Zitadel, etc.) - **OIDC Integration**: Connect to any OpenID Connect provider (Keycloak, Auth0, Zitadel, etc.) with stateless cookie-based flow state
- **Database Flexibility**: SQLite (default) or PostgreSQL via feature flags - **Database Flexibility**: SQLite (default) or PostgreSQL via feature flags
- **Type-Safe Configuration**: Newtypes with built-in validation for all security-sensitive values - **Type-Safe Domain**: Newtypes with built-in validation for emails, passwords, secrets, and OIDC values
- **Cargo Generate Ready**: Pre-configured for scaffolding new services - **Cargo Generate Ready**: Pre-configured for scaffolding new services
## Quick Start ## Quick Start
@@ -22,7 +22,6 @@ cargo generate --git https://github.com/GKaszewski/k-template.git
You'll be prompted to choose: You'll be prompted to choose:
- **Project name**: Your new service name - **Project name**: Your new service name
- **Database**: `sqlite` or `postgres` - **Database**: `sqlite` or `postgres`
- **Session auth**: Enable cookie-based sessions
- **JWT auth**: Enable Bearer token authentication - **JWT auth**: Enable Bearer token authentication
- **OIDC**: Enable OpenID Connect integration - **OIDC**: Enable OpenID Connect integration
@@ -33,15 +32,6 @@ git clone https://github.com/GKaszewski/k-template.git my-api
cd my-api cd my-api
cp .env.example .env cp .env.example .env
# Edit .env with your configuration # Edit .env with your configuration
```
### Run
```bash
# Development (with hot reload via cargo-watch)
cargo watch -x run
# Or simply
cargo run cargo run
``` ```
@@ -51,51 +41,51 @@ The API will be available at `http://localhost:3000/api/v1/`.
All configuration is done via environment variables. See [.env.example](.env.example) for all options. All configuration is done via environment variables. See [.env.example](.env.example) for all options.
### Authentication Modes ### Key Variables
Set `AUTH_MODE` to one of: | Variable | Default | Description |
|----------|---------|-------------|
| Mode | Description | Required Features | | `DATABASE_URL` | `sqlite:data.db?mode=rwc` | Database connection string |
|------|-------------|-------------------| | `COOKIE_SECRET` | *(insecure dev default)* | Secret for encrypting OIDC state cookie (≥64 bytes in production) |
| `session` | Cookie-based sessions | `auth-axum-login` | | `JWT_SECRET` | *(insecure dev default)* | Secret for signing JWT tokens (≥32 bytes in production) |
| `jwt` | Bearer token authentication | `auth-jwt` | | `JWT_EXPIRY_HOURS` | `24` | Token lifetime in hours |
| `both` | Try JWT first, fall back to session | `auth-axum-login`, `auth-jwt` | | `CORS_ALLOWED_ORIGINS` | `http://localhost:5173` | Comma-separated allowed origins |
| `SECURE_COOKIE` | `false` | Set `true` when serving over HTTPS |
| `PRODUCTION` | `false` | Enforces minimum secret lengths |
### OIDC Integration ### OIDC Integration
To enable OIDC login (e.g., "Login with Google"): To enable "Login with Google/Keycloak/etc.":
1. Enable the `auth-oidc` feature (enabled by default) 1. Enable the `auth-oidc` feature (on by default in cargo-generate)
2. Configure your OIDC provider in `.env`: 2. Set these environment variables:
```env ```env
OIDC_ISSUER=https://your-provider.com OIDC_ISSUER=https://your-provider.com
OIDC_CLIENT_ID=your-client-id OIDC_CLIENT_ID=your-client-id
OIDC_CLIENT_SECRET=your-secret OIDC_CLIENT_SECRET=your-secret
OIDC_REDIRECT_URL=http://localhost:3000/api/v1/auth/callback OIDC_REDIRECT_URL=http://localhost:3000/api/v1/auth/callback
``` ```
3. Users can login via `GET /api/v1/auth/login/oidc` 3. Users start the flow at `GET /api/v1/auth/login/oidc`
OIDC state (CSRF token, PKCE verifier, nonce) is stored in a short-lived encrypted cookie — no database session table required.
## Feature Flags ## Feature Flags
Features are configured in `api/Cargo.toml`:
```toml ```toml
[features] [features]
default = ["sqlite", "auth-axum-login", "auth-oidc", "auth-jwt"] default = ["sqlite", "auth-jwt"]
``` ```
| Feature | Description | | Feature | Description |
|---------|-------------| |---------|-------------|
| `sqlite` | SQLite database support (default) | | `sqlite` | SQLite database (default) |
| `postgres` | PostgreSQL database support | | `postgres` | PostgreSQL database |
| `auth-axum-login` | Session-based authentication | | `auth-jwt` | JWT Bearer token authentication |
| `auth-oidc` | OpenID Connect integration | | `auth-oidc` | OpenID Connect integration |
| `auth-jwt` | JWT token authentication |
| `auth-full` | All auth features combined |
### Common Configurations ### Common Configurations
**JWT-only API (stateless)**: **JWT-only (minimal, default)**:
```toml ```toml
default = ["sqlite", "auth-jwt"] default = ["sqlite", "auth-jwt"]
``` ```
@@ -105,48 +95,74 @@ default = ["sqlite", "auth-jwt"]
default = ["sqlite", "auth-oidc", "auth-jwt"] default = ["sqlite", "auth-oidc", "auth-jwt"]
``` ```
**Full-featured (all auth methods)**: **PostgreSQL + OIDC + JWT**:
```toml ```toml
default = ["sqlite", "auth-full"] default = ["postgres", "auth-oidc", "auth-jwt"]
``` ```
## API Endpoints ## API Endpoints
### Authentication ### Authentication
| Method | Endpoint | Description | | Method | Endpoint | Auth | Description |
|--------|----------|-------------| |--------|----------|------|-------------|
| `POST` | `/api/v1/auth/login` | Login with email/password | | `POST` | `/api/v1/auth/register` | — | Register with email + password → JWT |
| `POST` | `/api/v1/auth/register` | Register new user | | `POST` | `/api/v1/auth/login` | — | Login with email + password → JWT |
| `POST` | `/api/v1/auth/logout` | Logout (session mode) | | `POST` | `/api/v1/auth/logout` | — | Returns 200; client drops the token |
| `GET` | `/api/v1/auth/me` | Get current user | | `GET` | `/api/v1/auth/me` | Bearer | Current user info |
| `POST` | `/api/v1/auth/token` | Get JWT for session user | | `POST` | `/api/v1/auth/token` | Bearer | Issue a fresh JWT (`auth-jwt`) |
| `GET` | `/api/v1/auth/login/oidc` | Start OIDC login flow | | `GET` | `/api/v1/auth/login/oidc` | — | Start OIDC flow, sets encrypted state cookie (`auth-oidc`) |
| `GET` | `/api/v1/auth/callback` | OIDC callback | | `GET` | `/api/v1/auth/callback` | — | Complete OIDC flow → JWT, clears cookie (`auth-oidc`) |
### Example: Register and use a token
```bash
# Register
curl -X POST http://localhost:3000/api/v1/auth/register \
-H "Content-Type: application/json" \
-d '{"email": "user@example.com", "password": "mypassword"}'
# → {"access_token": "eyJ...", "token_type": "Bearer", "expires_in": 86400}
# Use the token
curl http://localhost:3000/api/v1/auth/me \
-H "Authorization: Bearer eyJ..."
```
## Project Structure ## Project Structure
``` ```
k-template/ k-template/
├── domain/ # Core business logic (no I/O dependencies) ├── domain/ # Pure business logic — zero I/O dependencies
│ └── src/ │ └── src/
│ ├── entities.rs # User entity │ ├── entities.rs # User entity
│ ├── value_objects.rs # Email, Password, OIDC newtypes │ ├── value_objects.rs # Email, Password, JwtSecret, OIDC newtypes
│ ├── repositories.rs # Repository interfaces (ports) │ ├── repositories.rs # Repository interfaces (ports)
── services.rs # Domain services ── services.rs # Domain services
│ └── errors.rs # DomainError (Unauthenticated 401, Forbidden 403, …)
├── infra/ # Infrastructure adapters ├── infra/ # Infrastructure adapters
│ └── src/ │ └── src/
│ ├── auth/ # Auth backends (OIDC, JWT) │ ├── auth/
└── user_repository.rs │ ├── jwt.rs # JwtValidator — create + verify tokens
│ │ └── oidc.rs # OidcService + OidcState (cookie-serializable)
│ ├── user_repository.rs # SQLite / PostgreSQL adapter
│ ├── db.rs # DatabasePool re-export
│ └── factory.rs # build_user_repository()
├── api/ # HTTP API layer ├── api/ # HTTP layer
│ └── src/ │ └── src/
│ ├── routes/ # API endpoints │ ├── routes/
├── config.rs # Configuration │ ├── auth.rs # Login, register, logout, me, OIDC flow
└── state.rs # Application state │ └── config.rs # /config endpoint
│ ├── config.rs # Config::from_env()
│ ├── state.rs # AppState (user_service, cookie_key, jwt_validator, …)
│ ├── extractors.rs # CurrentUser (JWT Bearer extractor)
│ ├── error.rs # ApiError → HTTP status mapping
│ └── dto.rs # LoginRequest, RegisterRequest, TokenResponse, …
├── .env.example # Configuration template ├── migrations_sqlite/
├── migrations_postgres/
├── .env.example
└── compose.yml # Docker Compose for local dev └── compose.yml # Docker Compose for local dev
``` ```
@@ -156,10 +172,13 @@ k-template/
```bash ```bash
# All tests # All tests
cargo test --all-features cargo test
# Domain tests only # Domain only
cargo test -p domain cargo test -p domain
# Infra only (SQLite integration tests)
cargo test -p infra
``` ```
### Database Migrations ### Database Migrations
@@ -168,10 +187,23 @@ cargo test -p domain
# SQLite # SQLite
sqlx migrate run --source migrations_sqlite sqlx migrate run --source migrations_sqlite
# PostgreSQL # PostgreSQL
sqlx migrate run --source migrations_postgres sqlx migrate run --source migrations_postgres
``` ```
### Building with specific features
```bash
# Minimal: SQLite + JWT only
cargo build -F sqlite,auth-jwt
# Full: SQLite + JWT + OIDC
cargo build -F sqlite,auth-jwt,auth-oidc
# PostgreSQL variant
cargo build --no-default-features -F postgres,auth-jwt,auth-oidc
```
## License ## License
MIT MIT

View File

@@ -34,7 +34,23 @@ async fn main() -> anyhow::Result<()> {
// Setup database // Setup database
tracing::info!("Connecting to database: {}", config.database_url); tracing::info!("Connecting to database: {}", config.database_url);
#[cfg(all(feature = "sqlite", not(feature = "postgres")))]
let db_type = k_core::db::DbType::Sqlite;
#[cfg(all(feature = "postgres", not(feature = "sqlite")))]
let db_type = k_core::db::DbType::Postgres;
// Both features enabled: fall back to URL inspection at runtime
#[cfg(all(feature = "sqlite", feature = "postgres"))]
let db_type = if config.database_url.starts_with("postgres") {
k_core::db::DbType::Postgres
} else {
k_core::db::DbType::Sqlite
};
let db_config = k_core::db::DatabaseConfig { let db_config = k_core::db::DatabaseConfig {
db_type,
url: config.database_url.clone(), url: config.database_url.clone(),
max_connections: config.db_max_connections, max_connections: config.db_max_connections,
min_connections: config.db_min_connections, min_connections: config.db_min_connections,
@@ -51,8 +67,6 @@ async fn main() -> anyhow::Result<()> {
let server_config = ServerConfig { let server_config = ServerConfig {
cors_origins: config.cors_allowed_origins.clone(), cors_origins: config.cors_allowed_origins.clone(),
// session_secret is unused (sessions removed); kept for k-core API compat
session_secret: None,
}; };
let app = Router::new() let app = Router::new()