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: httphost: {{ 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: debugformat: prettycrates:
- auth
- axum
- sqlx
- tower
- tower_httpauth:
access:
private_key: secrets/keys/dev/access_key.pempublic_key: secrets/keys/dev/access_key_pub.pemmaxage: 900# 15 minutesrefresh:
private_key: secrets/keys/dev/refresh_key.pempublic_key: secrets/keys/dev/refresh_key_pub.pemmaxage: 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/authSERVER_HOST=127.0.0.1SERVER_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 |