A minimalist OpenID Connect provider in Rust. Single binary with SQLite.
You want to add login to a small project. A proper one. With OAuth2, PKCE, ID tokens, refresh tokens - the full OIDC stack that every library understands.
Your options:
- Keycloak - Java process, needs PostgreSQL, XML configuration, realm concepts. For a project with three users.
- Authentik - Python, Redis, PostgreSQL, Docker Compose with five services. Nice UI, but you just wanted login.
- Zitadel - Go, CockroachDB. Enterprise features you'll never need.
- Auth0/Clerk - Cloud, vendor lock-in, costs from user X.
- and many others
GT Id is the alternative when you don't need any of that: a single binary, one SQLite file, one .env. Done. Multiple clients are managed through the admin panel.
- OIDC-compliant - Discovery, JWKS, Authorization Code Flow, Token Endpoint, UserInfo
- PKCE mandatory (S256) - no insecure fallback, code_challenge 43–128 characters validated (RFC 7636)
- Ed25519 signatures - ephemeral keys with key rotation support
- Multi-client - manage any number of clients via admin panel, secrets hashed with Argon2id
- Client auth -
client_secret_basicandclient_secret_post - Token Revocation (RFC 7009) with cascade revocation of the entire token family
- Token Introspection (RFC 7662) - resource servers can validate tokens
- Refresh Token Rotation - old token is automatically revoked on use
- Refresh Token Chain Tracking - on token reuse the entire family is revoked
- Auth Code Replay Detection - on code reuse all derived tokens are revoked
- at_hash in ID Token - binds access token to ID token (OIDC Core 3.1.3.6)
- Client binding - auth codes and refresh tokens are bound to the client_id
- Nonce mandatory - prevents ID token replay attacks
- Scope downscoping - clients can request a subset of scopes on refresh
- Grant type restriction - configurable which grant types are allowed
- RP-Initiated Logout - with id_token_hint and post_logout_redirect_uri validation
- Session fixation protection - old sessions are invalidated on login
- Admin panel - create, edit, delete users and clients
- Roles - configurable, included in the ID token as
rolesclaim - Account lockout + rate limiting - brute force protection
- CSRF protection - double-submit cookie with SHA256 and SameSite=Strict
- Security headers - CSP, HSTS (1 year), X-Frame-Options, Referrer-Policy, Cache-Control
- Constant-time comparisons -
subtlecrate against timing attacks on credentials and PKCE - email_verified claim - included in ID token per OIDC Core
- Security event logging - structured tracing for failed logins, lockouts, token replay, admin operations
- Redirect URI validation - only http/https schemes allowed on client creation
- i18n (DE/EN) - UI language auto-detected from
Accept-Languageheader, powered by Mozilla Project Fluent - Per-language content - email templates and legal pages (imprint, privacy) editable per language in the admin panel, public pages served in the visitor's language with German fallback
- TOTP 2FA - mandatory for admins, optional for all other users. Secrets encrypted at rest (AES-256-GCM). "Trust this browser" option to skip 2FA for a configurable duration. Admins can reset any user's 2FA
- Email queue - background worker sends queued emails every 30 seconds via SMTP, with exponential backoff on failure (60s, 120s, ... max 1h)
- Implicit/Hybrid Flow (authorization code only)
- Social Login / Federation
- Multi-Tenancy
- SCIM / User Provisioning
# Create .env (see Configuration)
nano .env
# Adjust: PUBLIC_UI_URI, SECURE_COOKIES, etc.
# Start
cargo run
# UI: http://localhost:3001 (Login, Consent, Admin)
# API: http://localhost:3000 (OIDC endpoints)
# On first launch, open http://localhost:3001 to create the initial admin account.
# Then create a client in the admin panel: http://localhost:3001/admin/clients/create| Endpoint | Method | Description |
|---|---|---|
/.well-known/openid-configuration |
GET | Discovery |
/jwks |
GET | JSON Web Key Set (current + previous key) |
/authorize-url?client_id=... |
GET | Ready-made authorize URL incl. PKCE, state, nonce |
/token |
POST | Token exchange (auth code + refresh) |
/userinfo |
GET | User claims via Bearer token |
/revoke |
POST | Token Revocation (RFC 7009) with cascade |
/introspect |
POST | Token Introspection (RFC 7662) |
/admin/users |
POST | Create user (client auth, email confirmation optional) |
/logout |
GET | RP-Initiated Logout (OIDC) |
| Variable | Description | Default |
|---|---|---|
ISSUER_URI |
OIDC Issuer URL | http://localhost:3000 |
PUBLIC_UI_URI |
Public UI URL (for authorize redirects) | http://localhost:3001 |
API_LISTEN_PORT |
Port for API (OIDC) | 3000 |
UI_LISTEN_PORT |
Port for UI (Login, Admin) | 3001 |
DATABASE_URI_USERS |
SQLite path (users) | sqlite:gtid_users.db |
DATABASE_URI_CLIENTS |
SQLite path (clients) | sqlite:gtid_clients.db |
DATABASE_URI_EMAILS |
SQLite path (emails + queue) | sqlite:gtid_emails.db |
DATABASE_URI_CONFIG |
SQLite path (config) | sqlite:gtid_config.db |
ROLES |
Comma-separated roles | member |
LOCKOUT_MAX_ATTEMPTS |
Failed attempts before lockout | 3 |
LOCKOUT_DURATION_SECS |
Lockout duration in seconds | 3600 |
SESSION_LIFETIME_SECS |
Session lifetime in seconds | 86400 (24h) |
SECURE_COOKIES |
Secure flag for HTTPS cookies | true |
ALLOWED_GRANT_TYPES |
Allowed grant types (comma-separated) | authorization_code,refresh_token |
KEY_ROTATION_INTERVAL_SECS |
Ed25519 key rotation interval in seconds | 86400 (24h) |
CORS_ALLOWED_ORIGINS |
Allowed CORS origins (comma-separated) | none (no cross-origin) |
MAX_REQUEST_BODY_BYTES |
Max request body size in bytes | 65536 (64 KB) |
TRUSTED_PROXIES |
Trust X-Forwarded-For header for client IP | false |
ACCESS_TOKEN_EXPIRY_SECS |
Access token lifetime in seconds | 900 (15 min) |
ID_TOKEN_EXPIRY_SECS |
ID token lifetime in seconds | 600 (10 min) |
REFRESH_TOKEN_EXPIRY_DAYS |
Refresh token lifetime in days | 30 |
TOTP_ENCRYPTION_KEY |
Hex-encoded 32-byte key for TOTP secret encryption (64 hex chars). Must come from outside the DB layer (env var, secret manager) | all-zeros (dev only) |
TRUST_DEVICE_LIFETIME_SECS |
How long a trusted-browser cookie skips 2FA (seconds) | 2592000 (30 days) |
SMTP_HOST |
SMTP server hostname (unset = email disabled) | none |
SMTP_PORT |
SMTP server port | 587 |
SMTP_USERNAME |
SMTP authentication username | none |
SMTP_PASSWORD |
SMTP authentication password | none |
SMTP_FROM |
Sender address for outgoing emails | noreply@localhost |
SMTP_STARTTLS |
Use STARTTLS for SMTP connection | true |
Auth Code ──┬──> Access Token (JWT, 15 min default, at_hash in ID token)
├──> ID Token (JWT, 10 min default, with at_hash + nonce)
└──> Refresh Token ──> new Refresh Token ──> ...
(30 days default) (same token_family)
Token family: All refresh tokens derived from the same auth code form a family. On suspected token theft (reuse of an already revoked token) the entire family is revoked.
Auth Code Replay: If an already redeemed auth code is presented again, all derived tokens are immediately revoked (OAuth Security BCP).
Ed25519 keys are held in memory. On rotation the current key becomes the previous key and a new one is generated. The JWKS endpoint serves both keys so clients can still validate tokens signed with the old key.
For detailed security patterns and guidelines for contributors, see SECURITY.md.
| Attack | Protection |
|---|---|
| Timing attacks | Argon2id for client secrets, subtle::ConstantTimeEq for URI comparisons |
| Brute force | Rate limiting (IP + User-Agent) + account lockout |
| Session fixation | All old sessions are invalidated on login |
| CSRF | Double-submit cookie (SHA256, SameSite=Strict) |
| Token substitution | at_hash binds access token to ID token |
| Token theft | Refresh token chain tracking with family revocation |
| Code replay | One-time codes with cascade revocation on reuse |
| Admin compromise | Mandatory TOTP 2FA (optional for non-admins), secrets encrypted at rest (AES-256-GCM) |
| Open redirect | Exact match of redirect_uri against registered client URIs |
| ID token replay | Nonce mandatory |
| Scope escalation | Downscoping allowed, upscoping prevented |
| Clickjacking | X-Frame-Options: DENY + CSP frame-ancestors 'none' |
| MITM | HSTS with 1 year + includeSubDomains |
| Cross-origin abuse | CORS with explicit origin allowlist (default: none) |
| Oversized payloads | Request body size limit (default: 64 KB) |
| Page caching | Cache-Control: no-store on all API and UI responses |
| Open redirect | Redirect URI scheme validation (http/https only) |
| Missing audit trail | Structured security event logging (login, lockout, admin ops, token replay) |
gtid (single binary, ~3 MB)
|
+-- UI (:3001) ---- Login, Consent, Admin, Profile, RP-Logout
| Templates embedded, no filesystem needed
|
+-- API (:3000) --- OIDC endpoints, JWKS, Token, UserInfo, Revoke, Introspect
|
+-- SQLite -------- Users, Clients, Sessions, Auth Codes, Refresh Tokens, Email Queue
|
+-- Ed25519 ------- KeyStore with rotation (current + previous key)
Note: The Ed25519 key is generated in memory on each start and never written to disk. This means: after a restart all previously issued tokens become invalid and users must log in again.
Both ports always bind to 127.0.0.1. For external access place a reverse proxy (nginx, Caddy) with TLS in front.
See HOWTO.md for a step-by-step guide with curl examples.
Any OIDC-compliant library can use GT Id via discovery:
http://localhost:3000/.well-known/openid-configuration
MIT