{"id":50341195,"url":"https://github.com/mano8/fa-auth-m8","last_synced_at":"2026-05-29T17:01:25.674Z","repository":{"id":356178363,"uuid":"1227599352","full_name":"mano8/fa-auth-m8","owner":"mano8","description":"Self-contained FastAPI authentication microservice — JWT (HS256/RS256/ES256), Google OAuth2 + PKCE, API keys with rate limiting, and Redis-backed token revocation. Drop into any Docker Compose project.","archived":false,"fork":false,"pushed_at":"2026-05-22T15:56:05.000Z","size":1510,"stargazers_count":1,"open_issues_count":1,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-22T16:11:19.529Z","etag":null,"topics":["authentication-backend","docker","docker-compose","fastapi","microservices","python"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mano8.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"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-05-02T22:53:17.000Z","updated_at":"2026-05-20T14:27:45.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/mano8/fa-auth-m8","commit_stats":null,"previous_names":["mano8/fa-auth-m8"],"tags_count":10,"template":false,"template_full_name":null,"purl":"pkg:github/mano8/fa-auth-m8","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mano8%2Ffa-auth-m8","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mano8%2Ffa-auth-m8/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mano8%2Ffa-auth-m8/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mano8%2Ffa-auth-m8/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mano8","download_url":"https://codeload.github.com/mano8/fa-auth-m8/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mano8%2Ffa-auth-m8/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33662205,"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-05-29T02:00:06.066Z","response_time":107,"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-backend","docker","docker-compose","fastapi","microservices","python"],"created_at":"2026-05-29T17:01:25.001Z","updated_at":"2026-05-29T17:01:25.665Z","avatar_url":"https://github.com/mano8.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# fa-auth-m8 — FastAPI JWT Authentication Microservice\n\n![CI/CD](https://github.com/mano8/fa-auth-m8/actions/workflows/CI.yaml/badge.svg?branch=main)\n[![Codacy Badge](https://app.codacy.com/project/badge/Grade/edab51cc8805468fb3884e1d9e57ccdc)](https://app.codacy.com/gh/mano8/fa-auth-m8/dashboard?utm_source=gh\u0026utm_medium=referral\u0026utm_content=\u0026utm_campaign=Badge_grade)\n[![codecov](https://codecov.io/gh/mano8/fa-auth-m8/graph/badge.svg?token=LH7GTT2JZY)](https://codecov.io/gh/mano8/fa-auth-m8)\n[![Docker Pulls](https://img.shields.io/docker/pulls/tepochtli/fa-auth-m8)](https://hub.docker.com/r/tepochtli/fa-auth-m8)\n[![License: Apache-2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/mano8/fa-auth-m8/blob/main/LICENSE)\n\nA production-ready, self-hosted FastAPI (Python 3.14) authentication microservice for Docker Compose. Provides JWT authentication (HS256, RS256, ES256), Google OAuth2 with PKCE, Redis-backed stateful session management, role-based access control (RBAC), API key management with per-key rate limiting, and a private inter-service API — ready to drop into any Docker-based Python microservice stack.\n\nConsumer services validate tokens **locally** using the companion [auth-sdk-m8](https://github.com/mano8/auth-sdk-m8) package (`pip install auth-sdk-m8`) — no round-trip to the auth service on every request. In `stateful` mode, revocation is checked via a lightweight HTTP call to the auth service private API (`POST /private/v1/jti-status`) instead of connecting to auth Redis directly, keeping the Redis instance private to the auth service.\n\nThe included example stacks use `_m8` in their names as a personal naming convention — not a framework requirement. Any stack can be copied and adapted for your own project by renaming the Docker services, network, and env files.\n\n---\n\n## Table of Contents\n\n- [Features](#features)\n- [Architecture](#architecture)\n- [Docker Compose Stacks](#docker-compose-stacks)\n- [API Endpoints](#api-endpoints)\n- [Quick Start](#quick-start)\n- [Docker Hub image](#docker-hub-image)\n- [Choosing a Database](#choosing-a-database)\n- [Environment Variables](#environment-variables)\n- [Infrastructure Resilience](#infrastructure-resilience)\n- [Deployment Modes](#deployment-modes)\n- [API Key Authentication](#api-key-authentication)\n- [Private API](#private-api)\n- [Consumer Service Integration](#consumer-service-integration)\n- [Development](#development)\n- [Prometheus Metrics](#prometheus-metrics)\n- [Dependencies](#dependencies)\n\n---\n\n## Features\n\n- Email/password login with bcrypt password hashing (timing-attack safe)\n- Google OAuth2 login with PKCE\n- JWT access + refresh token pair (refresh token in HttpOnly cookie, atomically rotated on every use)\n- RS256 / ES256 asymmetric signing with JWKS endpoint for zero-downtime key rotation\n- Opt-in `iss`/`aud` JWT claim enforcement to prevent cross-service token reuse\n- Session tracking and JTI revocation via Redis\n- Login rate limiting per email (Redis-backed, namespace-hardened)\n- Refresh token rate limiting per user ID — 10 rotations / 5 min, prevents session integrity denial\n- **API key authentication** with per-key fixed-window rate limiting (MINUTE / HOUR / DAY / MONTH), `X-RateLimit-*` response headers, and write-behind `last_used_at` tracking\n- Role-based access control (`user`, `admin`, `superuser`)\n- User management CRUD (superuser only)\n- Profile self-service (read, update, password change, delete account, avatar URL)\n- Dashboard activity endpoints\n- Private inter-service API (protected by shared secret + Docker network isolation)\n- MySQL **or** PostgreSQL — switchable via a single env var\n- Prometheus metrics (`METRICS_ENABLED=true`) with API key–specific counters and alert rules\n- Alembic migrations auto-applied on first start\n- VS Code remote debugger support\n\n---\n\n## Architecture\n\n```text\n                        Internet\n                           │\n                    ┌──────▼──────┐\n                    │   Traefik   │  TLS termination, IP forwarding\n                    └──────┬──────┘\n                           │\n          ┌────────────────┼────────────────┐\n          │                │                │\n   ┌──────▼──────┐  ┌──────▼──────┐  ┌─────▼──────┐\n   │ auth-service│  │  consumer   │  │  Prometheus │\n   │  :8000      │  │  service    │  │  + Grafana  │\n   └──┬──────┬───┘  └─────────────┘  └────────────┘\n      │      │\n┌─────▼──┐ ┌─▼──────────┐\n│ MySQL/ │ │ Redis :6379 │\n│ Postgres│ └────────────┘\n└────────┘\n```\n\nConsumer services validate tokens locally (JWT signature check + optional Redis blacklist via `auth-sdk-m8`) — no per-request call to the auth service. Other services on the same Docker network can also call the private API at `http://auth-service:8000/user/private/` for operations such as creating users programmatically.\n\n---\n\n## Docker Compose Stacks\n\nFive ready-to-run stacks are provided under [`examples/docker_compose/`](https://github.com/mano8/fa-auth-m8/tree/main/examples/docker_compose). See the [stack selection guide](https://github.com/mano8/fa-auth-m8/tree/main/examples/docker_compose#which-stack-should-i-use) for help choosing.\n\n| Stack | Database | Algorithm | Token mode | Observability | Notes |\n| ----- | -------- | --------- | ---------- | ------------- | ----- |\n| [`quickstart_m8`](https://github.com/mano8/fa-auth-m8/tree/main/examples/docker_compose/quickstart_m8) | MariaDB | HS256 | `stateful` | — | **Start here** — simplest onboarding |\n| [`postgres_m8`](https://github.com/mano8/fa-auth-m8/tree/main/examples/docker_compose/postgres_m8) | PostgreSQL 16 | HS256 | `stateful` | — | PostgreSQL variant |\n| [`rs256_m8`](https://github.com/mano8/fa-auth-m8/tree/main/examples/docker_compose/rs256_m8) | MariaDB | RS256 | `hybrid` | — | Asymmetric signing + JWKS |\n| [`metrics_m8`](https://github.com/mano8/fa-auth-m8/tree/main/examples/docker_compose/metrics_m8) | PostgreSQL 16 | HS256 | `stateful` | Prometheus + Grafana | Metrics dashboards |\n| [`hardened_m8`](https://github.com/mano8/fa-auth-m8/tree/main/examples/docker_compose/hardened_m8) | PostgreSQL 16 | RS256 | `stateful` | Prometheus + Grafana | Container hardening + Docker Hub image |\n| [`vault_m8`](https://github.com/mano8/fa-auth-m8/tree/main/examples/docker_compose/vault_m8) | PostgreSQL 16 | RS256 | `stateful` | Prometheus + Grafana | HashiCorp Vault + Docker Hub image |\n\n**Start here →** [`quickstart_m8`](https://github.com/mano8/fa-auth-m8/tree/main/examples/docker_compose/quickstart_m8) for the fastest path to a running stack.\n\n### Token modes at a glance\n\nThe `TOKEN_MODE` column in the table above controls how tokens are validated across your services:\n\n| Mode | Redis for JWT | Instant revocation | Google OAuth | Best for |\n| ---- | ------------- | ------------------ | ------------ | -------- |\n| `stateless` | No | ✗ | ✗ | Maximum scalability, no revocation needed |\n| `hybrid` | Refresh only | Refresh tokens only | ✓ | Balance: scalable access + revocable refresh |\n| `stateful` | Yes (every request) | ✓ | ✓ | Instant logout guarantee, highest security |\n\n`stateless` disables Google OAuth (PKCE requires Redis). `hybrid` leaves a stolen access token valid until expiry after logout; use `stateful` if instant revocation is required.\n\n---\n\n## API Endpoints\n\nAll routes are prefixed with `API_PREFIX` (default `/user`).\n\n| Tag | Method | Path | Auth | Description |\n| --- | ------ | ---- | ---- | ----------- |\n| health | GET | `/health/` | — | Redis, database, effective token mode |\n| jwks | GET | `/.well-known/jwks.json` | — | JWKS endpoint (RS256/ES256 public key; `{\"keys\":[]}` for HS256) |\n| login | POST | `/login/access-token` | — | Email/password login — returns access token, sets refresh cookie |\n| login | POST | `/login/refresh-token/` | — | Refresh access token from HttpOnly cookie |\n| login | POST | `/login/logout/` | JWT | Revoke session, blacklist JTI, clear cookie |\n| login | POST | `/login/test-token/` | JWT | Validate access token, return current user |\n| google-api | GET | `/google-api/login-url/` | — | Return Google OAuth2 authorization URL (native-app PKCE flow) |\n| google-api | POST | `/google-api/exchange/` | — | One-time auth code exchange for tokens (PKCE verified, GETDEL atomic) |\n| google-auth | GET | `/google-auth/oauth-callback/` | — | Google OAuth2 PKCE callback — exchange code, create/update user |\n| profile | GET | `/profile/get/me/` | JWT | Read own profile |\n| profile | PATCH | `/profile/update/me/` | JWT | Update own profile |\n| profile | PATCH | `/profile/me/password/` | JWT | Change own password |\n| profile | DELETE | `/profile/delete/me/` | JWT | Delete own account |\n| api-keys | GET | `/profile/api-keys/verify` | X-API-Key | Validate key header, enforce rate limits, return key metadata |\n| api-keys | POST | `/profile/api-keys/` | JWT | Create API key — plaintext returned once, never stored |\n| api-keys | GET | `/profile/api-keys/` | JWT | List own API keys (metadata only) |\n| api-keys | GET | `/profile/api-keys/{key_id}` | JWT | Get single key metadata |\n| api-keys | DELETE | `/profile/api-keys/{key_id}` | JWT | Revoke API key |\n| sessions | GET | `/sessions/` | superuser | List all sessions (paginated) |\n| sessions | GET | `/sessions/get/{session_id}/` | superuser | Get session by ID |\n| sessions | GET | `/sessions/get-by-user/{user_id}/` | superuser | Get session by user ID |\n| sessions | GET | `/sessions/get-current/` | JWT | Get own current session |\n| sessions | POST | `/sessions/refresh-google-tokens/` | JWT | Refresh external Google tokens |\n| sessions | DELETE | `/sessions/delete-by-user/{user_id}/` | superuser | Delete all sessions for a user |\n| sessions | DELETE | `/sessions/delete/{session_id}/` | superuser | Delete specific session |\n| users | GET | `/users/` | superuser | List all users (paginated) |\n| users | POST | `/users/new_user/` | superuser | Create user with password |\n| users | POST | `/users/signup/` | superuser | Register user (no password set) |\n| users | GET | `/users/get/{user_id}/` | superuser | Get user by ID |\n| users | PATCH | `/users/update/{user_id}/` | superuser | Update user |\n| users | DELETE | `/users/delete/{user_id}/` | superuser | Delete user |\n| dashboard | GET | `/dashboard/users/activity/` | JWT | All-user activity stats (monthly) |\n| dashboard | GET | `/dashboard/users/activity/current/` | JWT | Own activity stats (monthly) |\n| metrics | GET | `/metrics` | — | Prometheus metrics (`METRICS_ENABLED=true` only) |\n| private | POST | `/private/users/` | X-Internal-Token | Create user (inter-service, Docker network only) |\n\nInteractive docs at `{BACKEND_HOST}{API_PREFIX}/docs` when `SET_DOCS=true`.\n\n---\n\n## Quick Start\n\n### 1. Choose a stack\n\n```bash\ncd examples/docker_compose/quickstart_m8      # fastest start — HS256 + stateful mode\n# or\ncd examples/docker_compose/rs256_m8           # asymmetric RS256 + JWKS\n```\n\nSee the [Docker Compose stack guide](https://github.com/mano8/fa-auth-m8/tree/main/examples/docker_compose) to pick the right stack.\n\n### 2. Copy env files and generate secrets\n\n```bash\ncp .env.example .env\ncp auth.env.example auth.env\ncp api.env.example api.env\n# Fill in all `changethis` values in .env, auth.env and api.env\n```\n\nGenerate secrets with:\n\n```bash\npython -c \"import secrets; print(secrets.token_urlsafe(64))\"\n```\n\n### 3. Install mkcert (optional — for browser-trusted TLS)\n\n\u003e **TLS works without this step.** Each stack includes a `cert-init` Docker service that generates a self-signed certificate automatically on the first `docker compose up`. Browsers will show a certificate warning in that case. Install mkcert only if you want a fully trusted cert with no browser warnings.\n\n`mkcert` creates a local CA trusted by your OS and browsers, eliminating the `ERR_CERT_AUTHORITY_INVALID` warning and silent `fetch()` failures in Chrome extensions.\n\n```bash\n# Windows\nwinget install FiloSottile.mkcert   # or: choco install mkcert\n\n# macOS\nbrew install mkcert \u0026\u0026 brew install nss   # nss = Firefox support\n\n# Linux — see https://github.com/FiloSottile/mkcert#linux\n```\n\nAfter installing, run **once** to register the local CA system-wide:\n\n```bash\nmkcert -install\n```\n\n`init.sh` detects `mkcert` automatically and falls back to a self-signed OpenSSL certificate if it is not installed (browsers will still warn in that case).\n\n#### Browser TLS compatibility\n\n| Browser | With mkcert | Without mkcert |\n| ------- | ----------- | -------------- |\n| Chrome, Edge, Brave, Opera, Vivaldi | ✅ Trusted automatically | ⚠️ Cert warning |\n| Safari (macOS) | ✅ Trusted automatically | ⚠️ Cert warning |\n| Firefox | ⚠️ Manual CA import needed | ⚠️ Cert warning |\n\nFirefox uses its own NSS certificate store and does not inherit the OS trust store.\nSee `traefik/certs/README_DEV.md` inside any example stack for the step-by-step\nFirefox CA import walkthrough.\n\n### 4. Generate keys and TLS certificate\n\n```bash\nbash init.sh\n# RS256/ES256 stacks: also generates the key pair and writes ACCESS_KEY_ID\n```\n\n\u003e **Windows:** use **Git Bash** (included with Git for Windows) or **WSL**.\n\n### 5. Start the stack\n\n```bash\ndocker compose up --build\n```\n\nAlembic migrations run automatically. The first start seeds the superuser from `FIRST_SUPERUSER` / `FIRST_SUPERUSER_PASSWORD`.\n\n### 6. Verify\n\n```http\nGET http://localhost:9000/user/health/\n```\n\n\u003e Health and metrics routes (`/user/health`, `/user/metrics`) are only reachable on the internal `api` entryPoint (port 9000, localhost-bound). They are blocked on the public `websecure` entryPoint (port 4430/443).\n\n### 7. Adapt for your own project\n\nThe example stacks are ready-to-copy templates. To use one as the base for a new project:\n\n- Copy the stack directory and rename it.\n- In `docker-compose.yml`, rename the Docker services and internal network to match your project.\n- Update all `changethis` values in the env files.\n- Add your own microservices to `docker-compose.yml` on the same internal network.\n\n---\n\n## Docker Hub image\n\nThe published image is available at:\n\n```bash\ndocker pull tepochtli/fa-auth-m8:latest\n```\n\n[![Docker Pulls](https://img.shields.io/docker/pulls/tepochtli/fa-auth-m8)](https://hub.docker.com/r/tepochtli/fa-auth-m8)\n\n### Tags\n\n| Tag | Description |\n| --- | ----------- |\n| `latest` | Latest release from the `main` branch |\n| `x.y.z` (e.g. `0.8.2`) | Pinned release — recommended for production |\n\n### Using the published image in a Compose stack\n\nThe example stacks under `examples/docker_compose/` use `build:` to build the\nservice image locally from source. To use the published image instead, replace\nthe `build:` block in the `auth_user_service` service with an `image:` line:\n\n```yaml\n# Replace this:\nauth_user_service:\n  build:\n    context: ../../../\n    dockerfile: ./auth_user_service/Dockerfile\n\n# With this:\nauth_user_service:\n  image: tepochtli/fa-auth-m8:0.8.2   # pin to a specific release for production\n```\n\nAll env files, volumes, labels, and `depends_on` entries remain unchanged —\nonly the `build:` block is replaced.\n\n### When to build locally vs. use the published image\n\n| Scenario | Recommendation |\n| -------- | -------------- |\n| Production deployment | `image: tepochtli/fa-auth-m8:x.y.z` — pinned, reproducible |\n| Evaluating or quick start | `image: tepochtli/fa-auth-m8:latest` — always current |\n| Active development / custom changes | `build:` (default in example stacks) — local source |\n\n---\n\n## Choosing a Database\n\nSet `SELECTED_DB` in `.env` (or `auth.env`):\n\n| Value | Driver | Default port |\n| ----- | ------ | ------------ |\n| `Mysql` (default) | `pymysql` / `aiomysql` | 3306 |\n| `Postgres` | `psycopg2` / `asyncpg` | 5432 |\n\n---\n\n## Environment Variables\n\n### Core\n\n| Variable | Required | Default | Description |\n| -------- | -------- | ------- | ----------- |\n| `DOMAIN` | yes | — | Public domain (e.g. `localhost`) |\n| `ENVIRONMENT` | yes | — | `local` \\| `development` \\| `staging` \\| `production` |\n| `API_PREFIX` | yes | `/user` | URL prefix for all routes |\n| `PROJECT_NAME` | yes | — | Project name shown in docs |\n| `STACK_NAME` | yes | — | Docker Compose stack slug |\n| `BACKEND_HOST` | yes | — | Full backend URL (e.g. `http://127.0.0.1:9000`) |\n| `FRONTEND_HOST` | yes | — | Full frontend URL (e.g. `http://localhost:5173`) |\n| `BACKEND_CORS_ORIGINS` | yes | — | Comma-separated allowed origins |\n| `TABLES_PREFIX` | no | `auth` | DB table name prefix (e.g. `auth_user`, `auth_api_key`) |\n| `SET_DOCS` | no | `true` | Enable Swagger UI at `{API_PREFIX}/docs` |\n| `SET_REDOC` | no | `true` | Enable ReDoc at `{API_PREFIX}/redoc` |\n\n### Tokens\n\n| Variable | Required | Default | Description |\n| -------- | -------- | ------- | ----------- |\n| `TOKEN_MODE` | no | `stateful` | `stateless` \\| `hybrid` \\| `stateful` — controls Redis usage and JTI revocation |\n| `ACCESS_TOKEN_ALGORITHM` | no | `HS256` | Signing algorithm for access tokens (`HS256`, `RS256`, `ES256`) |\n| `REFRESH_TOKEN_ALGORITHM` | no | `HS256` | Signing algorithm for refresh tokens |\n| `ACCESS_SECRET_KEY` | HS256 only | — | Symmetric signing key for access tokens |\n| `REFRESH_SECRET_KEY` | yes | — | Signing key for refresh tokens (always HS256) |\n| `REFRESH_SECRET_KEY_OLD` | no | — | Previous refresh signing key. Set during key rotation to allow old-key tokens to remain valid for the duration of their TTL. Remove once all pre-rotation refresh tokens have expired. |\n| `ACCESS_PRIVATE_KEY_FILE` | RS256/ES256 only | — | Path to PEM private key file (mounted into container) |\n| `ACCESS_PUBLIC_KEY_FILE` | RS256/ES256 only | — | Path to PEM public key file (distributed to consumers) |\n| `ACCESS_TOKEN_EXPIRE_MINUTES` | no | `30` | Access token lifetime |\n| `REFRESH_TOKEN_EXPIRE_MINUTES` | no | `120` | Refresh token lifetime |\n| `REFRESH_TOKEN_COOKIE_EXPIRE_SECONDS` | no | `3600` | Refresh cookie max-age |\n| `TOKENS_ENCRYPTION_KEY` | yes | — | Key for `SessionMiddleware` cookie signing |\n| `TOKEN_ISSUER` | no | — | When set, embeds `iss` in tokens and requires a match on validation |\n| `TOKEN_AUDIENCE` | no | — | When set, embeds `aud` in tokens and requires a match on validation |\n| `ACCESS_KEY_ID` | no | — | Explicit `kid` in JWT headers and JWKS; auto-derived from key fingerprint when unset |\n| `AUTH_SERVICE_ROLE` | no | `issuer` | `issuer` (auth service) or `consumer` (downstream services) |\n| `JWKS_URI` | no | — | Consumer services: JWKS endpoint URL; enables automatic `JwksKeyResolver` wiring |\n| `JWKS_CACHE_TTL_SECONDS` | no | `300` | JWKS key cache TTL in seconds |\n\n**HS256 (default)** — set `ACCESS_SECRET_KEY` and `REFRESH_SECRET_KEY`; leave asymmetric key vars blank.\n\n**RS256 / ES256** — set `ACCESS_TOKEN_ALGORITHM`, `ACCESS_PRIVATE_KEY_FILE`, `ACCESS_PUBLIC_KEY_FILE`. Mount the key files into the container (see `examples/docker_compose/rs256_m8/keys/`). Generate a key pair:\n\n```bash\n# RS256\nopenssl genrsa -out private.pem 2048\nopenssl rsa -in private.pem -pubout -out public.pem\n\n# ES256\nopenssl ecparam -genkey -name prime256v1 -noout -out private.pem\nopenssl ec -in private.pem -pubout -out public.pem\n```\n\nOr use `bash init.sh` in any asymmetric stack — it generates the correct key type automatically.\n\n### Database\n\n| Variable | Required | Default | Description |\n| -------- | -------- | ------- | ----------- |\n| `SELECTED_DB` | no | `Mysql` | `Mysql` or `Postgres` |\n| `DB_HOST` | yes | — | Database host |\n| `DB_PORT` | yes | — | Database port |\n| `DB_DATABASE` | yes | — | Database name |\n| `DB_USER` | yes | — | Database user |\n| `DB_PASSWORD` | yes | — | Database password |\n\n### Redis\n\n| Variable | Required | Description |\n| -------- | -------- | ----------- |\n| `REDIS_HOST` | yes | Redis host |\n| `REDIS_PORT` | yes | Redis port |\n| `REDIS_USER` | yes | Redis user |\n| `REDIS_PASSWORD` | yes | Redis password |\n| `REDIS_SSL` | no | Enable TLS for the Redis connection pool (default: `false`). Set `true` when Redis is reached over a network boundary in staging/production. |\n| `REDIS_SSL_CA` | no | Path to CA certificate file. **Required when `REDIS_SSL=true`** — without it the connection pool cannot verify the server cert and will raise `CERTIFICATE_VERIFY_FAILED`. |\n| `REDIS_SSL_CERT` | no | Path to client certificate for mTLS. Must be set together with `REDIS_SSL_KEY`; cannot be set without it. |\n| `REDIS_SSL_KEY` | no | Path to client private key for mTLS. Must be set together with `REDIS_SSL_CERT`; cannot be set without it. |\n\n### Auth \u0026 OAuth\n\n| Variable | Required | Description |\n| -------- | -------- | ----------- |\n| `FIRST_SUPERUSER` | yes | Email of the bootstrap superuser — used only on first run |\n| `FIRST_SUPERUSER_PASSWORD` | yes | Password of the bootstrap superuser — used only on first run |\n| `GOOGLE_CLIENT_ID` | no | Google OAuth2 client ID |\n| `GOOGLE_CLIENT_SECRET` | no | Google OAuth2 client secret |\n| `PRIVATE_API_SECRET` | yes | Shared secret for `X-Internal-Token` header |\n\n### Auth Degradation Policy\n\nControls what happens to each security control when Redis is unavailable. All settings are optional; the defaults represent the recommended production posture.\n\n| Variable | Default | Description |\n| -------- | ------- | ----------- |\n| `AUTH_STRICT_MODE` | `false` | When `true`, overrides all per-control modes to `fail_closed` |\n| `REFRESH_VALIDATION_FAILURE_MODE` | `fail_closed` | Refresh allowlist check unavailable → `fail_closed`: 503 \\| `fail_open`: skip check |\n| `SESSION_WRITE_FAILURE_MODE` | `fail_closed` | Token revocation on logout fails → `fail_closed`: 503 \\| `fail_open`: silent skip |\n| `RATE_LIMIT_FAILURE_MODE` | `fail_open` | Rate limiter unavailable → `fail_closed`: 503 \\| `fail_open`: skip check |\n| `ACCESS_REVOCATION_FAILURE_MODE` | `fail_open` | Access token blacklist check unavailable → `fail_closed`: 503 \\| `fail_open`: skip |\n\nDefault posture: refresh validation and session writes **fail closed** (logout is authoritative; unverifiable refresh tokens are rejected). Rate limiting and access revocation **fail open** (short token TTL bounds the exposure window; availability is preserved).\n\nEvery degraded-mode decision emits an `auth_degraded_decision_total` counter (labels: `control`, `mode`, `reason`) — see the [Prometheus metrics](#prometheus-metrics) table for full label values.\n\n### Login \u0026 Refresh Rate Limiting\n\nControls the fixed-window rate limits applied to the login and refresh-token endpoints (Redis-backed). All settings are optional; the defaults represent the recommended security posture.\n\n| Variable | Default | Description |\n| -------- | ------- | ----------- |\n| `LOGIN_RATE_LIMIT_REQUESTS` | `5` | Max login attempts per window per email before 429 |\n| `LOGIN_RATE_LIMIT_WINDOW_MINUTES` | `15` | Brute-force window in minutes |\n| `REFRESH_RATE_LIMIT_REQUESTS` | `10` | Max refresh token rotations per window per user |\n| `REFRESH_RATE_LIMIT_WINDOW_MINUTES` | `5` | Churn-prevention window in minutes |\n\nA startup warning is logged if the effective rate (requests ÷ window) exceeds 5 req/min for login or 20 req/min for refresh. When Redis is unavailable, behaviour falls back to `RATE_LIMIT_FAILURE_MODE`.\n\n### API Key Rate Limiting\n\n| Variable | Required | Default | Description |\n| -------- | -------- | ------- | ----------- |\n| `API_KEY_STRICT_RATE_LIMIT` | no | `false` | When `true`, return 503 instead of allowing requests when Redis is unavailable |\n| `API_KEY_DEFAULT_LIMIT_MINUTE` | no | `60` | Default requests per minute (`0` = disabled) |\n| `API_KEY_DEFAULT_LIMIT_HOUR` | no | `1000` | Default requests per hour |\n| `API_KEY_DEFAULT_LIMIT_DAY` | no | `10000` | Default requests per day |\n| `API_KEY_DEFAULT_LIMIT_MONTH` | no | `200000` | Default requests per month |\n| `API_KEY_MAX_PER_USER` | no | `10` | Maximum API keys a user may create |\n\n### Observability\n\n| Variable | Required | Default | Description |\n| -------- | -------- | ------- | ----------- |\n| `METRICS_ENABLED` | no | `false` | Expose `GET /metrics` Prometheus endpoint |\n| `METRICS_GROUPS` | no | `all` | Comma-separated groups: `all` \\| `traffic` \\| `performance` \\| `reliability` \\| `health` \\| `auth` |\n| `SENTRY_DSN` | no | — | Sentry DSN for error tracking |\n\n### Deployment\n\n| Variable | Default | Description |\n| -------- | ------- | ----------- |\n| `API_BIND_IP` | `127.0.0.1` | Host IP Traefik binds port 9000 to. Set to `0.0.0.0` for LAN/public exposure |\n| `TRUSTED_PROXY_IPS` | `172.16.0.0/12` | CIDR(s) Uvicorn trusts as reverse-proxy source for `X-Forwarded-For` |\n| `STRICT_PRODUCTION_MODE` | `false` | When `true`, enforce production-grade checks (e.g. secure cookies) even in non-production environments |\n\n---\n\n## Infrastructure Resilience\n\nThe service degrades gracefully when Redis or the database is temporarily unavailable.\n\n### Redis unavailable\n\nBehaviour when Redis is down is controlled by the [Auth Degradation Policy](#auth-degradation-policy) settings. The table below shows the default posture (`fail_closed` for refresh + logout, `fail_open` for rate limiting + access revocation):\n\n| `TOKEN_MODE` | Login | Refresh | Logout | Google OAuth |\n| ------------ | ----- | ------- | ------ | ------------ |\n| `stateless` | ✅ unaffected | ✅ unaffected | ✅ unaffected | ❌ 503 (PKCE requires Redis) |\n| `hybrid` | ✅ works, rate limiting skipped | ❌ 503 (`REFRESH_VALIDATION_FAILURE_MODE=fail_closed`) | ❌ 503 (`SESSION_WRITE_FAILURE_MODE=fail_closed`) | ❌ 503 |\n| `stateful` | ✅ works, rate limiting skipped | ❌ 503 (`REFRESH_VALIDATION_FAILURE_MODE=fail_closed`) | ❌ 503 (`SESSION_WRITE_FAILURE_MODE=fail_closed`) | ❌ 503 |\n\nSet `REFRESH_VALIDATION_FAILURE_MODE=fail_open` and `SESSION_WRITE_FAILURE_MODE=fail_open` to restore the previous fail-open behaviour (tokens accepted without allowlist check; logout silently skips revocation).\n\nIn `stateful`/`hybrid` mode with Redis down, the `/health/` endpoint reflects `effective_mode: stateless_degraded` and a `CRITICAL` log is emitted at startup.\n\n#### Degradation contract\n\nThe service operates under two stable states with a brief transient inconsistency regime between them:\n\n| State | Condition | Authorization correctness |\n| ----- | --------- | ------------------------- |\n| **Healthy** | Redis reachable | Full: JWT + allowlist + blacklist all consistent |\n| **Fully degraded** | Redis unreachable | Deterministic: each control follows its declared `fail_open` / `fail_closed` mode |\n| **Transient** | Partial Redis failure (some commands succeed, others fail within the same request) | Non-deterministic: rate-limit increment may fail while allowlist read succeeds; outcomes become request-order dependent |\n\nThe transient regime is observable — it does not enable a specific exploit, but authorization consistency is weakened until Redis returns to a stable state. Observable via:\n\n- `auth_redis_circuit_breaker_open` gauge → `1` means the circuit is open (full degradation)\n- `auth_degraded_decision_total` counter → increments on every per-control degraded decision\n- `/health/` `circuit_breaker` field → `\"open\"` | `\"closed\"`\n\nThe asymmetric posture (refresh + session writes fail-closed; rate limit + access revocation fail-open) is intentional: the highest-value targets for an attacker (token replay, unrevoked sessions) are hard-rejected; availability controls are preserved.\n\nAPI key rate limiting: when Redis is unavailable and `API_KEY_STRICT_RATE_LIMIT=false` (default), requests are allowed through. With `API_KEY_STRICT_RATE_LIMIT=true`, the endpoint returns 503.\n\n### Database unavailable\n\nAll routes that touch the database return `503 Service Unavailable` with a clear message.\n\n### Health endpoint\n\n```http\nGET {API_PREFIX}/health/\n```\n\n```json\n{\n  \"status\": \"ok\",\n  \"token_mode\": \"stateful\",\n  \"effective_mode\": \"stateful\",\n  \"redis\": \"ok\",\n  \"circuit_breaker\": \"closed\",\n  \"database\": \"ok\",\n  \"revocation_available\": true,\n  \"rate_limiting_available\": true,\n  \"degraded_since\": null,\n  \"degradation_modes\": {\n    \"rate_limit\": \"fail_open\",\n    \"refresh_validation\": \"fail_closed\",\n    \"session_write\": \"fail_closed\",\n    \"access_revocation\": \"fail_open\"\n  }\n}\n```\n\n`circuit_breaker` is `\"open\"` when Redis is required but currently unavailable (requests are short-circuited), and `\"closed\"` when healthy or not required.\n\n`degradation_modes` shows the effective per-control policy (respecting `AUTH_STRICT_MODE`). `degraded_since` is the UTC timestamp when Redis first became unreachable in the current process lifetime, or `null` when healthy.\n\n---\n\n## Deployment Modes\n\n| Mode | `API_BIND_IP` | TLS | HSTS | Use when |\n| ---- | ------------- | --- | ---- | -------- |\n| **Development** | `0.0.0.0` | mkcert (trusted) or self-signed | off | local machine, Docker dev loop |\n| **Private LAN / homelab** | `0.0.0.0` or `127.0.0.1` | local CA recommended | off | Raspberry Pi, NAS, private LAN |\n| **Public / production** | `127.0.0.1` | valid cert required | on (opt-in) | VPS, cloud, internet-facing |\n\n### Running behind a reverse proxy (real client IP)\n\nRequires a coordinated three-layer setup:\n\n1. **Traefik** — add `forwardedHeaders.trustedIPs` to each entrypoint in `traefik.yml` (strips client-supplied `X-Forwarded-For`, prevents IP spoofing).\n2. **Uvicorn** — the startup script reads `TRUSTED_PROXY_IPS` (default `172.16.0.0/12`) and passes it via `--proxy-headers --forwarded-allow-ips`. Never use `*`.\n3. **Application** — `_client_ip()` reads the leftmost `X-Forwarded-For` value, which is trustworthy only because layers 1 and 2 have been configured.\n\n### HSTS (opt-in, public deployments only)\n\n`Strict-Transport-Security` is commented out in all `traefik/dynamic_conf.yml` files. Uncomment after confirming TLS is stable and the hostname will remain HTTPS-only for the full `stsSeconds` period.\n\n---\n\n## API Key Authentication\n\nAPI keys are created by authenticated users and validated by consumer services via the `GET /profile/api-keys/verify` endpoint (or the `get_current_api_key` FastAPI dependency in the SDK).\n\n### Key lifecycle\n\n- Created with `POST /profile/api-keys/` — plaintext key returned **once only**, never stored.\n- Stored as a SHA-256 hash in the database alongside metadata (name, expiry, revocation flag).\n- `last_used_at` is updated via a Redis write-behind queue flushed every 60 seconds.\n- Revoked with `DELETE /profile/api-keys/{key_id}`.\n\n### Rate limiting\n\nEach key is checked against up to four fixed windows (MINUTE, HOUR, DAY, MONTH). Priority chain: per-key `RateLimit` rows → per-user defaults → `API_KEY_DEFAULT_LIMIT_*` settings.\n\nResponse headers on every API key request:\n\n| Header | Description |\n| ------ | ----------- |\n| `X-RateLimit-Limit` | Limit for the tightest (MINUTE) window |\n| `X-RateLimit-Remaining` | Remaining requests in the MINUTE window |\n| `X-RateLimit-Reset` | Unix timestamp when the MINUTE window resets |\n| `Retry-After` | Seconds to wait (429 responses only) |\n\n---\n\n## Private API\n\nEndpoints under `/user/private/` are for inter-service calls only:\n\n- Must not be exposed to the public internet — enforce at the reverse proxy / Docker network level.\n- Every request must include `X-Internal-Token: \u003cPRIVATE_API_SECRET\u003e`.\n\n| Method | Path | Description |\n| ------ | ---- | ----------- |\n| POST | `/private/users/` | Create a user account (called by other microservices) |\n| POST | `/private/v1/jti-status` | Check whether a JTI is revoked (`stateful` mode only; fails-open when Redis unavailable) |\n\n---\n\n## Consumer Service Integration\n\n`examples/fastapi_service` is a reference implementation showing how a downstream microservice integrates with `auth_user_service` using `auth-sdk-m8`.\n\n`auth-sdk-m8` is a standard pip package — install it in any FastAPI consumer service:\n\n```bash\npip install auth-sdk-m8\n```\n\n### Token validation\n\n```python\nfrom auth_sdk_m8.security import build_access_validator, ValidationHooks\n\n_validator = build_access_validator(settings, hooks=_hooks)\n```\n\n`build_access_validator` reads `ACCESS_TOKEN_ALGORITHM`, `ACCESS_SECRET_KEY` / `ACCESS_PUBLIC_KEY_FILE`, `TOKEN_ISSUER`, `TOKEN_AUDIENCE`, and `JWKS_URI` directly from a `CommonSettings` instance.\n\n### JWKS-based key validation (RS256/ES256)\n\nWhen `JWKS_URI` is set, `build_access_validator` wires up `JwksKeyResolver` automatically. The resolver fetches `/.well-known/jwks.json`, caches keys by `kid`, and refreshes on cache miss — supporting zero-downtime key rotation.\n\n```ini\nACCESS_TOKEN_ALGORITHM=RS256\nJWKS_URI=http://auth-service/user/.well-known/jwks.json\nJWKS_CACHE_TTL_SECONDS=300\n```\n\n### Revocation check (stateful mode)\n\nConsumer services check revocation via an HTTP call to the auth service private API —\nauth Redis is never shared with consumers. Set `INTROSPECTION_URL` and `PRIVATE_API_SECRET`\n(both must match the auth service) when `TOKEN_MODE=stateful`:\n\n```ini\nINTROSPECTION_URL=http://auth_user_service:8000/user/private/v1/jti-status\nPRIVATE_API_SECRET=\u003csame as auth service PRIVATE_API_SECRET\u003e\n```\n\nThe `RemoteRevocationClient` in `examples/fastapi_service/core/revocation.py` handles\nthe check asynchronously with configurable timeouts. It **fails-open** by default\n(network error → token treated as active). Set `fail_closed=True` to reject tokens\nwhen the endpoint is unreachable instead.\n\n### Issuer / audience enforcement (opt-in)\n\nSet `TOKEN_ISSUER` and `TOKEN_AUDIENCE` to the **same values** in both the auth service and every consumer. When set, the auth service embeds `iss`/`aud` claims in issued tokens and all validators require an exact match.\n\n---\n\n## Development\n\n### Run locally (without Docker)\n\n```bash\ncd auth_user_service\npip install -r requirements_base.txt -r requirements_dev.txt\nuvicorn auth_user_service.main:app --host 0.0.0.0 --port 8000 --reload\n```\n\n### VS Code remote debugging\n\nSet `VSCODE_DEBUG=true` in the container environment. The startup script launches `debugpy` on port `5678` and waits for the debugger to attach before starting Uvicorn.\n\n### Database migrations\n\nMigrations are applied automatically on container start. To run manually:\n\n```bash\nalembic -c auth_user_service/alembic.ini revision --autogenerate -m \"description\"\nalembic -c auth_user_service/alembic.ini upgrade head\n```\n\n### Linting \u0026 formatting\n\n```bash\nruff format .\nruff check .\nruff check . --fix\n```\n\n### Tests\n\n```bash\n# Unit + integration tests (default — no live stack required)\npytest\n\n# All live tests against a running stack\npytest -m live --no-cov\n\n# Target a specific algorithm or token mode\npytest tests/live/test_security_universal.py --no-cov   # any stack\npytest -m live_asymmetric --no-cov                      # RS256 / ES256 stacks\npytest -m live_hs256 --no-cov                           # HS256 stacks\npytest -m live_stateful --no-cov                        # TOKEN_MODE=stateful\npytest -m live_hybrid --no-cov                          # TOKEN_MODE=hybrid\npytest -m live_stateless --no-cov                       # TOKEN_MODE=stateless\n```\n\nThe live suite is modular — each file carries a `require_algorithm` / `require_token_mode` mark so tests are automatically skipped when the running stack does not match. `conftest.py` auto-detects the stack's algorithm, token mode, and Redis availability at session start. Tests decorated with `require_redis`, as well as all `live_stateful` and `live_hybrid` tests, are automatically skipped when the `/health/` endpoint reports `redis=unavailable`.\n\n| Module | Mark | Covers |\n| ------ | ---- | ------- |\n| `test_security_universal.py` | `live_security` | 13 attack categories (A–M): brute-force, JWT forgery, IDOR, rate-limit bypass, CORS, private API exposure, file upload, info disclosure, HTTP headers, cookie security, API key abuse |\n| `test_asymmetric.py` | `live_asymmetric` | alg=none confusion, JWKS exposure, attacker-generated key — RS256 / ES256 only |\n| `test_hs256.py` | `live_hs256` | HS256-specific attacks |\n| `test_stateful.py` | `live_stateful` | Token revocation, session-chain invalidation |\n| `test_hybrid.py` | `live_hybrid` | Partial-Redis degraded mode behaviour |\n| `test_stateless.py` | `live_stateless` | No-Redis guarantees |\n\nThe `tests/security/` unit suite (no live stack required) covers JWT security, Redis resilience, refresh lifecycle, refresh key-rotation fallback (`REFRESH_SECRET_KEY_OLD`), input sanitisation, JWKS endpoint, OAuth adversarial, iss/aud validation, session-chain invalidation, exception handling, and client IP attribution.\n\n---\n\n## Prometheus Metrics\n\nEnabled with `METRICS_ENABLED=true`. The metric prefix is derived from `API_PREFIX` (e.g. `/user` → `user_`).\n\n| Group | Metric | Type | Labels |\n| ----- | ------ | ---- | ------ |\n| traffic | `{prefix}http_requests_total` | Counter | method, endpoint, status_code |\n| performance | `{prefix}http_request_duration_seconds` | Histogram | method, endpoint |\n| reliability | `{prefix}http_errors_total` | Counter | method, endpoint, status_class |\n| health | `{prefix}http_status_total` | Counter | status_code |\n| auth | `{prefix}auth_login_attempts_total` | Counter | result: success \\| wrong_credentials \\| inactive_user \\| rate_limited |\n| auth | `{prefix}auth_token_refresh_total` | Counter | result: success \\| invalid \\| revoked \\| rate_limited |\n| auth | `{prefix}auth_logout_total` | Counter | — |\n| auth | `{prefix}auth_token_validation_failures_total` | Counter | reason: invalid \\| revoked \\| inactive |\n| auth | `{prefix}auth_oauth_attempts_total` | Counter | provider, result: success \\| failed |\n| auth | `{prefix}auth_revocation_failure_total` | Counter | operation: access_blacklist \\| refresh_allowlist \\| db_session |\n| auth | `{prefix}auth_degraded_decision_total` | Counter | control: rate_limit \\| refresh_validation \\| session_write \\| access_revocation; mode: fail_open \\| fail_closed; reason: redis_unavailable \\| revocation_failed |\n| auth | `{prefix}auth_redis_circuit_breaker_open` | Gauge | 1 = Redis unavailable (circuit open), 0 = Redis healthy (circuit closed) |\n| auth | `{prefix}auth_degradation_mode_active` | Gauge | control × mode label pair; value always 1 for active mode; set at startup |\n| auth | `{prefix}auth_session_integrity_denial_total` | Counter | trigger: reuse_detected |\n| auth | `{prefix}auth_api_key_validations_total` | Counter | result: success \\| invalid \\| revoked \\| expired |\n| auth | `{prefix}auth_api_key_rate_limit_checks_total` | Counter | result: checked \\| allowed \\| blocked |\n| auth | `{prefix}auth_api_key_rate_limit_hits_total` | Counter | period: minute \\| hour \\| day \\| month |\n| auth | `{prefix}auth_api_key_lifecycle_total` | Counter | action: created \\| revoked |\n| auth | `{prefix}auth_api_key_flush_duration_seconds` | Histogram | — |\n\nAlert rules for `metrics_m8` and `vault_m8` stacks (`prometheus/alerts.yml`):\n\n- `ApiKeyBlockRatioHigh` — hits/checks \u003e 10% over 5 min\n- `ApiKeyRateLimitInvariantViolation` — hits \u003e checks × 1.1 (instrumentation sanity guard)\n- `ApiKeyFlushLatencyHigh` — p99 flush latency \u003e 500 ms\n- `ApiKeyHighInvalidRate` — \u003e 1 invalid/revoked/expired key/s over 5 min\n\n---\n\n## Dependencies\n\n**Stack:** Python 3.14 · FastAPI · SQLModel · Redis · MariaDB / PostgreSQL 16 · Traefik · Docker Compose · Prometheus · Grafana\n\n- [FastAPI](https://fastapi.tiangolo.com/)\n- [SQLModel](https://sqlmodel.tiangolo.com/) + [Alembic](https://alembic.sqlalchemy.org/)\n- [auth-sdk-m8](https://github.com/mano8/auth-sdk-m8) — shared schemas, JWT validation, refresh token rotation, JWKS resolver, base controllers\n- [Redis](https://redis.io/) — session revocation, refresh token allowlist, rate limiting, PKCE store, write-behind queue\n- [PyJWT](https://pyjwt.readthedocs.io/) + [passlib](https://passlib.readthedocs.io/) + [cryptography](https://cryptography.io/)\n- [google-auth](https://google-auth.readthedocs.io/) — Google OAuth2\n\n---\n\n## License\n\nApache2 © Eli Serra\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmano8%2Ffa-auth-m8","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmano8%2Ffa-auth-m8","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmano8%2Ffa-auth-m8/lists"}