A stateless authentication service built in Rust, featuring JWT-based access and refresh token flows backed by PostgreSQL.
- Tech Stack
- Project Structure
- Getting Started
- Configuration
- RSA Key Generation
- Database Migrations
- Running the Application
- Environment Variables
| Layer | Technology |
|---|---|
| Language | Rust |
| Web Framework | Axum |
| Database | PostgreSQL |
| ORM / Migrations | SQLx |
| Authentication | JWT (RS256) |
| Config Templating | Tera |
| Logging | tracing / tracing-subscriber |
.
├── config/ # Environment-specific YAML configs
│ ├── development.yaml
│ ├── production.yaml
│ └── testing.yaml
├── migrations/ # SQLx migration files (up/down)
├── secrets/keys/ # RSA key pairs (not committed to VCS)
├── src/
│ ├── bin/main.rs # Entrypoint
│ ├── app.rs # CLI parsing and server startup
│ ├── context.rs # Shared application state (AppContext)
│ ├── config/ # Config loading and validation
│ ├── controllers/ # Route handlers
│ ├── middlewares/ # Request tracing, JSON extraction
│ ├── repository/ # Database access layer
│ ├── validator/ # Input validation
│ └── views/ # Response serialization
├── compose.yaml # Docker Compose (PostgreSQL)
└── Cargo.toml
- Rust (stable toolchain)
- PostgreSQL running on port
5432 - sqlx-cli for managing migrations
cargo install sqlx-cli --no-default-features --features postgresAlternatively, spin up PostgreSQL via Docker Compose:
docker compose up -d# Clone the repository
git clone https://github.com/<your-username>/auth.git
cd auth
# Build the project
cargo buildThe application reads configuration from config/<environment>.yaml at startup. The environment is selected via the --env CLI flag (default: development).
Config files are rendered as Tera templates, allowing environment variables to be interpolated at runtime using get_env().
Example — config/development.yaml:
server:
protocol: http
host: {{ get_env(name = "SERVER_HOST", default = "127.0.0.1") }}
port: {{ get_env(name = "SERVER_PORT", default = "7150") }}
database:
uri: {{ get_env(name = "DATABASE_URL", default = "postgresql://username:password@localhost:5432/database") }}
max_connections: {{ get_env(name = "DATABASE_MAX_CONNECTIONS", default = "10") }}
min_connections: {{ get_env(name = "DATABASE_MIN_CONNECTIONS", default = "0") }}
connection_timeout: {{ get_env(name = "DATABASE_CONNECTION_TIMEOUT", default = "5") }}
idle_timeout: {{ get_env(name = "DATABASE_IDLE_TIMEOUT", default = "5") }}
auto_migrate: {{ get_env(name = "DATABASE_AUTO_MIGRATE", default = "true") }}
dangerously_truncate: {{ get_env(name = "DATABASE_DANGEROUSLY_TRUNCATE", default = "false") }}
dangerously_recreate: {{ get_env(name = "DATABASE_DANGEROUSLY_RECREATE", default = "false") }}
logger:
level: debug
format: pretty
crates:
- auth
- axum
- sqlx
- tower
- tower_http
auth:
access:
private_key: secrets/keys/dev/access_key.pem
public_key: secrets/keys/dev/access_key_pub.pem
maxage: 900 # 15 minutes
refresh:
private_key: secrets/keys/dev/refresh_key.pem
public_key: secrets/keys/dev/refresh_key_pub.pem
maxage: 604800 # 7 daysThe following flags in the database section control migration behaviour. Use with caution outside of development:
| Flag | Description |
|---|---|
auto_migrate |
Automatically run pending migrations on startup |
dangerously_truncate |
Truncate all tables (planned — not yet implemented) |
dangerously_recreate |
Roll back all migrations and re-run them from scratch |
⚠️ Warning:dangerously_recreate: truewill drop and recreate all tables. Never enable this in production.
AUTH uses RS256 (RSA + SHA-256) for signing both access and refresh JWTs. You must generate two separate key pairs — one for each token type — and place them in the secrets/keys/dev/ directory.
mkdir -p secrets/keys/dev
# Access token key pair
openssl genrsa -out secrets/keys/dev/access_key.pem 2048
openssl rsa -in secrets/keys/dev/access_key.pem \
-pubout -out secrets/keys/dev/access_key_pub.pem
# Refresh token key pair
openssl genrsa -out secrets/keys/dev/refresh_key.pem 2048
openssl rsa -in secrets/keys/dev/refresh_key.pem \
-pubout -out secrets/keys/dev/refresh_key_pub.pemThe expected directory layout after generation:
secrets/keys/dev/
├── access_key.pem
├── access_key_pub.pem
├── refresh_key.pem
└── refresh_key_pub.pem
Important: Never commit private keys to version control. Add
secrets/to your.gitignore.
Migrations live in the migrations/ directory and are managed by SQLx. Each migration has an up and a down file.
Run migrations manually:
sqlx migrate runRoll back the latest migration:
sqlx migrate revertAlternatively, set auto_migrate: true in your config to have migrations run automatically on startup.
The application reads a .env file from the project root if one is present (via dotenvy). Create one to override config defaults:
DATABASE_URL=postgresql://username:password@localhost:5432/auth
SERVER_HOST=127.0.0.1
SERVER_PORT=7150Then start the server:
# Development (default)
cargo run
# Specify an environment explicitly
cargo run -- --env productionAvailable --env values: development (or dev), production (or prod), testing (or test).
On a successful start you should see:
INFO auth: Server running at http://127.0.0.1:7150
All variables below have defaults defined in the config templates and are optional unless marked otherwise.
| Variable | Default | Description |
|---|---|---|
SERVER_HOST |
127.0.0.1 |
Host the HTTP server binds to |
SERVER_PORT |
7150 |
Port the HTTP server listens on |
DATABASE_URL |
postgresql://username:password@localhost:5432/database |
PostgreSQL connection string |
DATABASE_MAX_CONNECTIONS |
10 |
Maximum connections in the pool |
DATABASE_MIN_CONNECTIONS |
0 |
Minimum idle connections in the pool |
DATABASE_CONNECTION_TIMEOUT |
5 |
Seconds to wait for a connection |
DATABASE_IDLE_TIMEOUT |
5 |
Seconds before an idle connection is closed |
DATABASE_AUTO_MIGRATE |
true |
Run migrations on startup |
DATABASE_DANGEROUSLY_TRUNCATE |
false |
Truncate all tables on startup |
DATABASE_DANGEROUSLY_RECREATE |
false |
Drop and recreate schema on startup |