{"id":50584726,"url":"https://github.com/geheb/gtid","last_synced_at":"2026-06-05T05:02:01.099Z","repository":{"id":347416736,"uuid":"1191960165","full_name":"geheb/gtid","owner":"geheb","description":"Simple OIDC Provider","archived":false,"fork":false,"pushed_at":"2026-05-15T21:48:21.000Z","size":1402,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-16T00:14:01.517Z","etag":null,"topics":["authentication","jwt","openid-connect","rust","security"],"latest_commit_sha":null,"homepage":"","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/geheb.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-03-25T18:52:12.000Z","updated_at":"2026-05-15T21:48:25.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/geheb/gtid","commit_stats":null,"previous_names":["geheb/gtid"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/geheb/gtid","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/geheb%2Fgtid","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/geheb%2Fgtid/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/geheb%2Fgtid/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/geheb%2Fgtid/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/geheb","download_url":"https://codeload.github.com/geheb/gtid/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/geheb%2Fgtid/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33930311,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-05T02:00:06.157Z","response_time":120,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["authentication","jwt","openid-connect","rust","security"],"created_at":"2026-06-05T05:02:00.275Z","updated_at":"2026-06-05T05:02:01.070Z","avatar_url":"https://github.com/geheb.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"# GT Id\n\nA minimalist OpenID Connect provider in Rust. Single binary with SQLite.\n\n## Why?\n\nYou 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.\n\nYour options:\n- **Keycloak** - Java process, needs PostgreSQL, XML configuration, realm concepts. For a project with three users.\n- **Authentik** - Python, Redis, PostgreSQL, Docker Compose with five services. Nice UI, but you just wanted login.\n- **Zitadel** - Go, CockroachDB. Enterprise features you'll never need.\n- **Auth0/Clerk** - Cloud, vendor lock-in, costs from user X.\n- and many others\n\nGT 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.\n\n## Features\n\n- **OIDC-compliant** - Discovery, JWKS, Authorization Code Flow, Token Endpoint, UserInfo\n- **PKCE mandatory** (S256) - no insecure fallback, code_challenge 43–128 characters validated (RFC 7636)\n- **Ed25519 signatures** - ephemeral keys with key rotation support\n- **Multi-client** - manage any number of clients via admin panel, secrets hashed with Argon2id\n- **Client auth** - `client_secret_basic` and `client_secret_post`\n- **Token Revocation** (RFC 7009) with cascade revocation of the entire token family\n- **Token Introspection** (RFC 7662) - resource servers can validate tokens\n- **Refresh Token Rotation** - old token is automatically revoked on use\n- **Refresh Token Chain Tracking** - on token reuse the entire family is revoked\n- **Auth Code Replay Detection** - on code reuse all derived tokens are revoked\n- **at_hash in ID Token** - binds access token to ID token (OIDC Core 3.1.3.6)\n- **Client binding** - auth codes and refresh tokens are bound to the client_id\n- **Nonce mandatory** - prevents ID token replay attacks\n- **Scope downscoping** - clients can request a subset of scopes on refresh\n- **Grant type restriction** - configurable which grant types are allowed\n- **RP-Initiated Logout** - with id_token_hint and post_logout_redirect_uri validation\n- **Session fixation protection** - old sessions are invalidated on login\n- **Admin panel** - create, edit, delete users and clients\n- **Roles** - configurable, included in the ID token as `roles` claim\n- **Account lockout + rate limiting** - brute force protection\n- **CSRF protection** - double-submit cookie with SHA256 and SameSite=Strict\n- **Security headers** - CSP, HSTS (1 year), X-Frame-Options, Referrer-Policy, Cache-Control\n- **Constant-time comparisons** - `subtle` crate against timing attacks on credentials and PKCE\n- **email_verified claim** - included in ID token per OIDC Core\n- **Security event logging** - structured tracing for failed logins, lockouts, token replay, admin operations\n- **Redirect URI validation** - only http/https schemes allowed on client creation\n- **i18n (DE/EN)** - UI language auto-detected from `Accept-Language` header, powered by Mozilla Project Fluent\n- **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\n- **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\n- **Email queue** - background worker sends queued emails every 30 seconds via SMTP, with exponential backoff on failure (60s, 120s, ... max 1h)\n\n## What it doesn't do (by design)\n\n- Implicit/Hybrid Flow (authorization code only)\n- Social Login / Federation\n- Multi-Tenancy\n- SCIM / User Provisioning\n\n## Quickstart\n\n```bash\n# Create .env (see Configuration)\nnano .env\n# Adjust: PUBLIC_UI_URI, SECURE_COOKIES, etc.\n\n# Start\ncargo run\n\n# UI:  http://localhost:3001 (Login, Consent, Admin)\n# API: http://localhost:3000 (OIDC endpoints)\n\n# On first launch, open http://localhost:3001 to create the initial admin account.\n# Then create a client in the admin panel: http://localhost:3001/admin/clients/create\n```\n\n## OIDC Endpoints\n\n| Endpoint | Method | Description |\n|----------|--------|-------------|\n| `/.well-known/openid-configuration` | GET | Discovery |\n| `/jwks` | GET | JSON Web Key Set (current + previous key) |\n| `/authorize-url?client_id=...` | GET | Ready-made authorize URL incl. PKCE, state, nonce |\n| `/token` | POST | Token exchange (auth code + refresh) |\n| `/userinfo` | GET | User claims via Bearer token |\n| `/revoke` | POST | Token Revocation (RFC 7009) with cascade |\n| `/introspect` | POST | Token Introspection (RFC 7662) |\n| `/admin/users` | POST | Create user (client auth, email confirmation optional) |\n| `/logout` | GET | RP-Initiated Logout (OIDC) |\n\n## Configuration (.env)\n\n| Variable | Description | Default |\n|----------|-------------|---------|\n| `ISSUER_URI` | OIDC Issuer URL | `http://localhost:3000` |\n| `PUBLIC_UI_URI` | Public UI URL (for authorize redirects) | `http://localhost:3001` |\n| `API_LISTEN_PORT` | Port for API (OIDC) | `3000` |\n| `UI_LISTEN_PORT` | Port for UI (Login, Admin) | `3001` |\n| `DATABASE_URI_USERS` | SQLite path (users) | `sqlite:gtid_users.db` |\n| `DATABASE_URI_CLIENTS` | SQLite path (clients) | `sqlite:gtid_clients.db` |\n| `DATABASE_URI_EMAILS` | SQLite path (emails + queue) | `sqlite:gtid_emails.db` |\n| `DATABASE_URI_CONFIG` | SQLite path (config) | `sqlite:gtid_config.db` |\n| `ROLES` | Comma-separated roles | `member` |\n| `LOCKOUT_MAX_ATTEMPTS` | Failed attempts before lockout | `3` |\n| `LOCKOUT_DURATION_SECS` | Lockout duration in seconds | `3600` |\n| `SESSION_LIFETIME_SECS` | Session lifetime in seconds | `86400` (24h) |\n| `SECURE_COOKIES` | Secure flag for HTTPS cookies | `true` |\n| `ALLOWED_GRANT_TYPES` | Allowed grant types (comma-separated) | `authorization_code,refresh_token` |\n| `KEY_ROTATION_INTERVAL_SECS` | Ed25519 key rotation interval in seconds | `86400` (24h) |\n| `CORS_ALLOWED_ORIGINS` | Allowed CORS origins (comma-separated) | *none* (no cross-origin) |\n| `MAX_REQUEST_BODY_BYTES` | Max request body size in bytes | `65536` (64 KB) |\n| `TRUSTED_PROXIES` | Trust X-Forwarded-For header for client IP | `false` |\n| `ACCESS_TOKEN_EXPIRY_SECS` | Access token lifetime in seconds | `900` (15 min) |\n| `ID_TOKEN_EXPIRY_SECS` | ID token lifetime in seconds | `600` (10 min) |\n| `REFRESH_TOKEN_EXPIRY_DAYS` | Refresh token lifetime in days | `30` |\n| `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) |\n| `TRUST_DEVICE_LIFETIME_SECS` | How long a trusted-browser cookie skips 2FA (seconds) | `2592000` (30 days) |\n| `SMTP_HOST` | SMTP server hostname (unset = email disabled) | *none* |\n| `SMTP_PORT` | SMTP server port | `587` |\n| `SMTP_USERNAME` | SMTP authentication username | *none* |\n| `SMTP_PASSWORD` | SMTP authentication password | *none* |\n| `SMTP_FROM` | Sender address for outgoing emails | `noreply@localhost` |\n| `SMTP_STARTTLS` | Use STARTTLS for SMTP connection | `true` |\n\n## Security Architecture\n\n### Token Security\n\n```\nAuth Code ──┬──\u003e Access Token (JWT, 15 min default, at_hash in ID token)\n            ├──\u003e ID Token (JWT, 10 min default, with at_hash + nonce)\n            └──\u003e Refresh Token ──\u003e new Refresh Token ──\u003e ...\n                 (30 days default) (same token_family)\n```\n\n**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.\n\n**Auth Code Replay:** If an already redeemed auth code is presented again, all derived tokens are immediately revoked (OAuth Security BCP).\n\n### Key Rotation\n\nEd25519 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.\n\n### Security Measures\n\nFor detailed security patterns and guidelines for contributors, see [SECURITY.md](SECURITY.md).\n\n| Attack | Protection |\n|--------|------------|\n| Timing attacks | Argon2id for client secrets, `subtle::ConstantTimeEq` for URI comparisons |\n| Brute force | Rate limiting (IP + User-Agent) + account lockout |\n| Session fixation | All old sessions are invalidated on login |\n| CSRF | Double-submit cookie (SHA256, SameSite=Strict) |\n| Token substitution | at_hash binds access token to ID token |\n| Token theft | Refresh token chain tracking with family revocation |\n| Code replay | One-time codes with cascade revocation on reuse |\n| Admin compromise | Mandatory TOTP 2FA (optional for non-admins), secrets encrypted at rest (AES-256-GCM) |\n| Open redirect | Exact match of redirect_uri against registered client URIs |\n| ID token replay | Nonce mandatory |\n| Scope escalation | Downscoping allowed, upscoping prevented |\n| Clickjacking | X-Frame-Options: DENY + CSP frame-ancestors 'none' |\n| MITM | HSTS with 1 year + includeSubDomains |\n| Cross-origin abuse | CORS with explicit origin allowlist (default: none) |\n| Oversized payloads | Request body size limit (default: 64 KB) |\n| Page caching | Cache-Control: no-store on all API and UI responses |\n| Open redirect | Redirect URI scheme validation (http/https only) |\n| Missing audit trail | Structured security event logging (login, lockout, admin ops, token replay) |\n\n## Architecture\n\n```\ngtid (single binary, ~3 MB)\n  |\n  +-- UI (:3001) ---- Login, Consent, Admin, Profile, RP-Logout\n  |                    Templates embedded, no filesystem needed\n  |\n  +-- API (:3000) --- OIDC endpoints, JWKS, Token, UserInfo, Revoke, Introspect\n  |\n  +-- SQLite -------- Users, Clients, Sessions, Auth Codes, Refresh Tokens, Email Queue\n  |\n  +-- Ed25519 ------- KeyStore with rotation (current + previous key)\n```\n\n\u003e **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.\n\nBoth ports always bind to `127.0.0.1`. For external access place a reverse proxy (nginx, Caddy) with TLS in front.\n\n## Integration\n\nSee [HOWTO.md](HOWTO.md) for a step-by-step guide with curl examples.\n\nAny OIDC-compliant library can use GT Id via discovery:\n\n```\nhttp://localhost:3000/.well-known/openid-configuration\n```\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgeheb%2Fgtid","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fgeheb%2Fgtid","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgeheb%2Fgtid/lists"}