Version: 2.3 Last Updated: 2026-06-30 Author: Muhammed Taha Ayan Status: MCP-first re-baseline — see ADR-007. Phase 1 complete; near-term focus is MCP / AI-agent authentication.
🎉 MVP delivered (June 2026). The MVP milestone is complete and closed — core OAuth 2.1 / OIDC, the MCP authorization surface, client management, and the agent-native authorization track (ADR-007 §2) all shipped. Production hardening (T3) is now complete too — security headers, CSRF, secure cookies, XSS-safe output, OIDC ID token/nonce/claims, structured logging +
/metrics, failed-login lockout, and the developer-portal Docker image. The environment-aware posture (ADR-008 / T5) is now complete too —environmentas a fail-safe policy dimension plus environment-gated developer API keys. The near-term roadmap is finished; remaining work is the long-term platform (wallet federation + post-quantum signing, T4).
QAuth is an open-source federated identity platform. It accepts identity from multiple upstream sources — Verifiable Credential wallets (OID4VC / OID4VP), email/password, and external OIDC providers — normalises them through a common federation-core layer, and issues standard OAuth 2.1 access tokens and OIDC ID tokens to downstream applications.
The project is built toward a future where digital identity is portable and user-controlled across services and platforms. Standards-compliant VC wallet support (Phase 4) means any application using QAuth gains wallet-based login without modifying its auth layer. The eIDAS 2.0 regulated-industry deployment (EUDI Wallet, EU member states) is a primary real-world target for Phase 4, but the federation architecture is wallet-agnostic.
Core Philosophy: Ship working features incrementally. Each phase should be production-ready before moving to the next.
2026-06-23 re-baseline (MCP-first). While executing Phases 1–3, QAuth's OAuth 2.1 work (PR #156) delivered a working OAuth 2.1 authorization server for MCP servers and AI agents — the protocol foundation of Phase 9, years early and never previously in scope (validated end-to-end with Claude Code against a live MCP server). Per ADR-007, the near-term direction is now MCP-first: productize MCP auth (a resource-server SDK + dynamic-registration abuse controls) and build agent-native authorization (the Phase 9 substance), while wallet federation (Phase 4) and post-quantum signing (Phase 5) become the resequenced long-term platform. The identifier-abstraction migration (ADR-002) is deferred and re-scoped as the Phase 4 gate. The phase breakdown below predates this re-baseline and is retained for reference; ADR-007 governs near-term priority.
Goal: A working OAuth 2.1 / OIDC authentication server (Phase 1) that is the foundation for a full federated identity hub (Phases 2–5).
Phase 1 Success Criteria:
- A developer can register an OAuth client
- A developer can implement login/signup using QAuth
- Users can authenticate with email/password
- JWT tokens are issued and validated correctly (EdDSA)
- OAuth 2.1 authorization code flow with mandatory PKCE works end-to-end
- Basic security practices are implemented
Non-Goals for Phase 1:
- Verifiable Credential / wallet login (Phase 4)
- Post-quantum JWT signing (Phase 5)
- Social login, MFA, WebAuthn/Passkeys (Phase 6+)
- SAML support (Phase 6+)
- Custom domains, advanced RBAC (Phase 6+)
- Microservices architecture, GraphQL API (Phase 6+)
Note: Multi-tenancy is included via the Realms table for data isolation. Advanced multi-tenancy features (custom domains, tenant management UI) are Phase 6+.
Timeline: 1-2 weeks Status: Completed
Objective: Set up the development environment and project structure.
- Initialize Nx monorepo
- Configure pnpm workspace
- Set up ESLint + Prettier
- Configure Husky + commitlint
- Create project documentation
- Set up database schema (PostgreSQL + Drizzle ORM)
- Set up Redis connection
- Create base Fastify server structure
- Set up environment configuration
- Set up testing infrastructure (@qauth-labs/shared-testing)
- Create Fastify plugins (db, cache, password, email)
- ✅ Working Nx workspace
- ✅ Code quality tools configured
- ✅ Database schema defined
- ✅ Basic server running
- ✅ Testing infrastructure ready
- ✅ Fastify plugin architecture established
Timeline: 6-8 weeks Status: Completed
Objective: Implement basic email/password authentication with JWT tokens.
Tasks:
- Design database schema
- Realms table (multi-tenancy support)
- Users table (id, realm_id, email, password_hash, email_verified, created_at, updated_at)
- Sessions table (id, user_id, oauth_client_id, expires_at, created_at)
- OAuth clients table (id, realm_id, client_id, client_secret_hash, name, redirect_uris, grant_types, response_types, created_at)
- Authorization codes table (id, code, oauth_client_id, user_id, redirect_uri, code_challenge, code_challenge_method, expires_at, used)
- Refresh tokens table (id, token_hash, user_id, oauth_client_id, expires_at, revoked)
- Email verification tokens table (id, token, user_id, expires_at, used)
- Audit logs table (id, user_id, oauth_client_id, event, event_type, success, ip_address, created_at)
- Roles table (id, realm_id, name, oauth_client_id, enabled) - Phase 5+
- User roles table (user_id, role_id) - Phase 5+
- Set up Drizzle ORM schemas
- Create initial database migration (0000_glamorous_valkyrie.sql)
- Implement repository pattern with BaseRepository interface
- Create repositories for users, realms, audit logs, and email verification tokens
- Add centralized error handling library (@qauth-labs/shared-errors)
Acceptance Criteria:
- ✅ Database schema is normalized
- ✅ Migrations can be run and rolled back
- ✅ Basic queries work
Estimated Time: 1 week
Tasks:
- Implement user registration endpoint (
POST /auth/register) - Integrate @node-rs/argon2 for password hashing (@qauth-labs/server-password)
- Email validation schema (@qauth-labs/shared-validation)
- Password strength validation (zxcvbn) (@qauth-labs/shared-validation)
- Fastify password plugin (@qauth-labs/fastify-plugin-password)
- Check for duplicate emails (database unique constraint)
- Rate limiting on registration
API Endpoint:
POST /auth/register
{
"email": "user@example.com",
"password": "SecurePass123!"
}
Response:
{
"user": {
"id": "uuid",
"email": "user@example.com",
"email_verified": false,
"created_at": "2025-10-21T..."
}
}Password Hashing:
import { hash, verify } from '@node-rs/argon2';
// Hash password
const hashed = await hash(password, {
memoryCost: 65536, // 64MB
timeCost: 3,
parallelism: 4,
});
// Verify password
const valid = await verify(hashed, password);Acceptance Criteria:
- ✅ Users can register with email/password
- ✅ Passwords are hashed with Argon2id
- ✅ Duplicate emails are rejected
- ✅ Weak passwords are rejected
- ✅ Rate limiting prevents abuse
Estimated Time: 1 week
Tasks:
- Generate verification token (32 bytes, hex encoded)
- Store token hash in database (TTL: 24 hours)
- Email infrastructure ready (Resend, SMTP, Mock providers)
- React Email templates for verification emails
- Configure email provider from environment variables
- Send verification email on registration
- Implement verify endpoint (
GET /auth/verify?token=...) - Mark email as verified
- Handle expired tokens
API Endpoints:
POST /auth/resend-verification
{
"email": "user@example.com"
}
Response:
{
"message": "Verification email sent"
}
---
GET /auth/verify?token=abc123
Response:
{
"message": "Email verified successfully"
}Acceptance Criteria:
- ✅ Email provider can be configured via environment variables (mock, resend, smtp)
- ✅ Provider-specific configuration validated at startup
- ✅ Verification email is sent on registration
- ✅ Valid tokens verify the email
- ✅ Expired tokens are rejected
- ✅ Used tokens cannot be reused
- ✅ Users can request new verification email
Estimated Time: 3-4 days
Tasks:
- Implement login endpoint (
POST /auth/login) - Verify password using @node-rs/argon2
- Generate JWT access tokens (Ed25519)
- Generate refresh tokens
- Store session in Redis
- Implement JWT signing with jose library
API Endpoint:
POST /auth/login
{
"email": "user@example.com",
"password": "SecurePass123!"
}
Response:
{
"access_token": "eyJhbGc...",
"refresh_token": "refresh_token_here",
"expires_in": 900,
"token_type": "Bearer"
}JWT Implementation:
import { SignJWT, generateKeyPair } from 'jose';
// Generate EdDSA key pair
const { publicKey, privateKey } = await generateKeyPair('EdDSA');
// Sign JWT
const jwt = await new SignJWT({
sub: userId,
email: user.email,
})
.setProtectedHeader({ alg: 'EdDSA' })
.setIssuedAt()
.setExpirationTime('15m')
.sign(privateKey);Acceptance Criteria:
- ✅ Users can login with correct credentials
- ✅ Invalid credentials are rejected
- ✅ Unverified emails cannot login (optional for MVP)
- ✅ JWT tokens are properly signed with EdDSA
- ✅ Tokens have correct expiration (15 minutes)
- ✅ Refresh tokens are stored securely
Estimated Time: 1 week
Tasks:
- Implement token refresh endpoint (
POST /auth/refresh) - Validate refresh token
- Issue new access token
- Implement logout endpoint (
POST /auth/logout) - Revoke refresh token on logout
- Clear session from Redis
API Endpoints:
POST /auth/refresh
{
"refresh_token": "refresh_token_here"
}
Response:
{
"access_token": "eyJhbGc...",
"expires_in": 900
}
---
POST /auth/logout
Authorization: Bearer eyJhbGc...
Response:
{
"message": "Logged out successfully"
}Acceptance Criteria:
- ✅ Valid refresh tokens can be exchanged for new access tokens
- ✅ Expired refresh tokens are rejected
- ✅ Logout invalidates the session
- ✅ Logged out tokens cannot be used
Estimated Time: 3-4 days
Tasks:
- Implement client registration (manual for MVP — also scriptable via
nx run db:db:seed-oauth-clientsfor machine-client bootstrap at deploy time; seelibs/infra/db/README.md) - Create authorization endpoint (
GET /oauth/authorize) - Create token endpoint (
POST /oauth/token) - Implement PKCE (code_challenge, code_verifier)
- Generate authorization codes
- Exchange authorization code for tokens
- Validate redirect_uri
- State parameter validation
OAuth Flow:
1. Client redirects user to:
GET /oauth/authorize?
response_type=code&
client_id=CLIENT_ID&
redirect_uri=REDIRECT_URI&
code_challenge=CHALLENGE&
code_challenge_method=S256&
state=STATE
2. User logs in and consents
3. QAuth redirects to:
REDIRECT_URI?code=AUTH_CODE&state=STATE
4. Client exchanges code for tokens:
POST /oauth/token
{
"grant_type": "authorization_code",
"code": "AUTH_CODE",
"client_id": "CLIENT_ID",
"redirect_uri": "REDIRECT_URI",
"code_verifier": "VERIFIER"
}
5. Response:
{
"access_token": "...",
"refresh_token": "...",
"token_type": "Bearer",
"expires_in": 900
}
Acceptance Criteria:
- ✅ Authorization code flow works end-to-end
- ✅ PKCE is enforced
- ✅ Invalid redirect_uri is rejected
- ✅ Authorization codes expire after 5 minutes
- ✅ Authorization codes are single-use
- ✅ State parameter is validated
Post-Phase 1 additions (see ADR-006):
- ✅
client_credentialsgrant (RFC 6749 4.4) for service-to-service auth — no refresh token,sub = clientId - ✅
client_secret_basicclient authentication (RFC 6749 2.3.1) alongsideclient_secret_post - ✅
audandscopeJWT claims — per-client audience viaoauth_clients.audience, defaults toclient_id(RFC 8707 light-mode) - ✅
scopepropagated to/oauth/introspectand refresh responses (RFC 6749 5.1, RFC 7662 2.2)
Estimated Time: 1.5 weeks
Tasks:
- Create JWT validation middleware
- Implement token introspection endpoint (
POST /oauth/introspect) - Create userinfo endpoint (
GET /userinfo) - Handle expired tokens
- Handle invalid signatures
API Endpoints:
GET /userinfo (OIDC userinfo):
GET /userinfo
Authorization: Bearer eyJhbGc...
Response:
{
"sub": "user_id",
"email": "user@example.com",
"email_verified": true
}- Auth:
Authorization: Bearer <access_token>(required). JWT middleware verifies the token and attaches the payload. - Response:
sub(required),email(optional),email_verified(optional). Claims reflect the authenticated user.
POST /oauth/introspect (RFC 7662 token introspection):
POST /oauth/introspect
Content-Type: application/x-www-form-urlencoded
token=access_token_here&client_id=client_123&client_secret=client_secret_here
Response (active token):
{
"active": true,
"sub": "user_id",
"client_id": "client_123",
"exp": 1234567890,
"iat": 1234567800,
"iss": "https://auth.example.com",
"token_type": "Bearer"
}
Response (invalid, expired, or cross-client token):
{
"active": false
}- Body:
token(required),client_id(required),client_secret(required),token_type_hint(optional). Confidential client authentication (client_secret_post). - Response: RFC 7662 2.2 —
active(required); when active, optional claimssub,client_id,exp,iat,iss,token_typemay be returned.
Acceptance Criteria:
- ✅ Valid tokens can access protected resources
- ✅ Invalid tokens are rejected with 401
- ✅ Expired tokens are rejected
- ✅ Token introspection returns correct data
Estimated Time: 3-4 days
Status: ✅ Completed
Tasks:
- Implement health check endpoint (
GET /health) - Check database connection
- Check Redis connection
- Return service status
API Endpoint:
GET /health
Response:
{
"status": "ok",
"timestamp": "2025-10-21T...",
"services": {
"database": "connected",
"redis": "connected"
}
}Acceptance Criteria:
- ✅ Health endpoint returns 200 when all services are healthy
- ✅ Health endpoint returns 503 when services are down
- ✅ Docker health check configured
Estimated Time: 1 hour
Total Estimated Time: 6-8 weeks
Deliverables:
- ✅ Working auth server with email/password authentication
- ✅ OAuth 2.1 authorization code flow (with PKCE)
- ✅ JWT token generation and validation (EdDSA)
- ✅ Email verification
- ✅ Basic user management
- ✅ Secure password hashing (Argon2id)
- ✅ Health check endpoint
What You Can Do After Phase 1:
- Register users
- Verify emails
- Login users
- Issue OAuth 2.1 compliant tokens
- Validate tokens in your application
- Build applications with QAuth authentication
Timeline: 3-4 weeks Status: In Progress — the client-management REST API fully shipped (list/create/get/update/delete + regenerate-secret, issues #86–#90), the environment-gated developer API keys backend + UI shipped (ADR-008 §6, #97/#98), and the developer-portal Docker image shipped (T3). The TanStack Start pages (register/login/dashboard) remain in progress.
Objective: Allow developers to register and manage OAuth clients without manual intervention via a web UI. Machine clients (client_credentials) can already be provisioned from a JSON manifest using nx run db:db:seed-oauth-clients — the portal is the user-facing equivalent for developer-registered clients.
Tasks:
- Create developer registration page (TanStack Start)
- Implement email verification (reuse Phase 1 logic)
- Create developer dashboard UI
- Set up TanStack Start app structure
- Implement login/logout for developers
UI Pages:
/register- Developer registration/login- Developer login/verify- Email verification/dashboard- Developer dashboard
Acceptance Criteria:
- ✅ Developers can register with email/password
- ✅ Email verification works
- ✅ Developers can login to dashboard
- ✅ Basic dashboard layout exists
Estimated Time: 1 week
Tasks:
- Create REST API for client management (issues #86–#90) — list/create/get/update/delete + regenerate-secret all shipped
- Create "New Client" form (portal UI in progress)
- Generate client_id and client_secret (backend, 32-byte secrets, Argon2id-hashed)
- Store client credentials securely (hash client_secret)
- Display client details (portal UI in progress)
- Edit client (redirect URIs, name) (portal UI in progress)
- Delete/revoke client (portal UI in progress)
- List all clients for a developer —
GET /api/clients, scoped by developer, secret never returned
REST API Endpoints (all shipped in ADR-006):
GET /api/clients # shipped (#86)
POST /api/clients # shipped (#86)
GET /api/clients/:id # shipped (#87)
PATCH /api/clients/:id # shipped (#88)
DELETE /api/clients/:id # shipped (#89)
POST /api/clients/:id/regenerate-secret # shipped (#90)
POST /api/clients/:id/api-keys # shipped (#97, env-gated)
GET /api/clients/:id/api-keys # shipped (#97)
DELETE /api/clients/:id/api-keys/:keyId # shipped (#97)UI Features:
- Client creation form
- Client list view
- Client details view
- Copy client_id / client_secret
- Regenerate client_secret
- Delete confirmation modal
Acceptance Criteria:
- ✅ Developers can create OAuth clients
- ✅ Client credentials are generated securely
- ✅ Developers can view/edit/delete clients
- ✅ Client secrets are only shown once
- ✅ Client secrets can be regenerated
Estimated Time: 1.5 weeks
Tasks:
- Generate API keys for developers
- Display API keys in dashboard
- Create API reference documentation (manual)
- Add code examples (JavaScript/TypeScript)
- Create quick start guide
- Document OAuth flow with examples
Documentation Pages:
- Getting Started
- OAuth 2.1 Flow
- API Reference
- Code Examples (React, Node.js, etc.)
Acceptance Criteria:
- ✅ Developers can generate API keys
- ✅ API reference is complete
- ✅ Quick start guide exists
- ✅ Code examples are working
Estimated Time: 1 week
Total Estimated Time: 3-4 weeks
Deliverables:
- ⏳ Developer portal (TanStack Start) — scaffolded & typecheck-clean; pages still in progress
- ⏳ Self-service client registration (portal UI in progress)
- ✅ REST API for client management — list/create/get/update/delete + regenerate-secret all shipped (#86–#90)
- ✅ API key management — environment-gated developer API keys (ADR-008 §6, #97/#98)
- ⏳ Basic documentation
What You Can Do After Phase 2:
- Developers can register and create OAuth clients
- No manual intervention needed
- Developers have API keys
- Documentation available
Timeline: 4-6 weeks
Status: ✅ Complete (T3 milestone — June 2026). Security hardening (3.1), OIDC 1.0 compliance (3.2), monitoring & logging (3.3), and Docker & deployment (3.4) all shipped.
Objective: Make the system production-ready with security, monitoring, and OIDC compliance.
Tasks:
- Implement rate limiting (fastify-rate-limit) — Redis-backed global plugin in place; per-endpoint limits still to be tuned:
/auth/register: 3 requests/hour per IP/auth/login: 5 requests/15min per IP/auth/resend-verification: 3 requests/hour per email/oauth/token: 10 requests/min per client
- Add CSRF protection (session-bound double-submit token on the consent flow; #108)
- Secure cookie settings (HttpOnly, Secure, SameSite) —
__Host-session cookie, Secure default-on (#109) - Input validation and sanitization (zod via
ZodTypeProvideron every route) - SQL injection prevention (Drizzle parameterized queries throughout)
- XSS protection (
safeUrl()allowlist + HTML escaping on server-rendered pages; #112) - Security headers (helmet) — via
@fastify/helmet(#113)- Content-Security-Policy (nonce-based, no
unsafe-inlinescripts) - Strict-Transport-Security
- X-Frame-Options
- X-Content-Type-Options
- Content-Security-Policy (nonce-based, no
- Audit logging (all auth events via
auditLogs.create) - Failed login attempt tracking (Redis-backed per identifier/IP, with lockout; #115)
Security Headers:
import helmet from '@fastify/helmet';
fastify.register(helmet, {
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
scriptSrc: ["'self'"],
imgSrc: ["'self'", 'data:', 'https:'],
},
},
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true,
},
});Acceptance Criteria:
- ✅ Rate limiting is enforced
- ✅ CSRF attacks are prevented
- ✅ Security headers are set
- ✅ All inputs are validated
- ✅ Audit logs capture all events
Estimated Time: 1.5 weeks
Tasks:
- Implement OIDC discovery endpoint (
/.well-known/openid-configuration) - Implement JWKS endpoint (
/.well-known/jwks.json) - Add ID token support (EdDSA, issued on
openidscope; #118) - Add nonce parameter support (echoed from
/authorizeinto the ID token; #119) - Implement OIDC claims (sub, email, email_verified, name) — aligned across ID token, userinfo, and discovery (#120)
- Test with OIDC validator (self-contained conformance suite; #121)
OIDC Discovery Response:
{
"issuer": "https://auth.qauth.dev",
"authorization_endpoint": "https://auth.qauth.dev/oauth/authorize",
"token_endpoint": "https://auth.qauth.dev/oauth/token",
"userinfo_endpoint": "https://auth.qauth.dev/userinfo",
"jwks_uri": "https://auth.qauth.dev/.well-known/jwks.json",
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code", "refresh_token"],
"subject_types_supported": ["public"],
"id_token_signing_alg_values_supported": ["EdDSA"],
"scopes_supported": ["openid", "email", "profile"],
"token_endpoint_auth_methods_supported": ["client_secret_post", "client_secret_basic"],
"claims_supported": ["sub", "email", "email_verified", "name"]
}ID Token:
const idToken = await new SignJWT({
sub: userId,
email: user.email,
email_verified: user.emailVerified,
nonce: nonce, // From authorization request
})
.setProtectedHeader({ alg: 'EdDSA' })
.setIssuedAt()
.setExpirationTime('15m')
.setIssuer('https://auth.qauth.dev')
.setAudience(clientId)
.sign(privateKey);Acceptance Criteria:
- ✅ OIDC discovery endpoint works
- ✅ JWKS endpoint returns public keys
- ✅ ID tokens are issued correctly
- ✅ OIDC validator tests pass
- ✅ Nonce parameter is validated
Estimated Time: 1.5 weeks
Tasks:
- Set up structured logging (pino) — secret redaction, JSON in prod,
pino-prettyin dev (#122) - Add metrics endpoint (
/metrics) - Prometheus format viaprom-client(#123) - Log all authentication events (#124)
- Log failed login attempts (email hashed, never the password; #125)
- Monitor token generation rate (access/refresh issuance counters; #126)
- Set up basic alerts (optional) — documented Alertmanager rules in
docs/observability.md(#127) - Add request ID tracking (
X-Request-Idpropagation, in every log line; #128)
Structured Logging:
import pino from 'pino';
const logger = pino({
level: process.env.LOG_LEVEL || 'info',
transport: {
target: 'pino-pretty',
options: {
colorize: true,
},
},
});
logger.info(
{
event: 'user.login',
userId: user.id,
email: user.email,
ip: req.ip,
userAgent: req.headers['user-agent'],
},
'User logged in successfully'
);Metrics:
GET /metrics
# HELP auth_login_total Total number of login attempts
# TYPE auth_login_total counter
auth_login_total{status="success"} 1234
auth_login_total{status="failure"} 56
# HELP auth_token_issued_total Total number of tokens issued
# TYPE auth_token_issued_total counter
auth_token_issued_total{type="access"} 5678
auth_token_issued_total{type="refresh"} 3456Acceptance Criteria:
- ✅ Structured logs are written
- ✅ Metrics endpoint works
- ✅ All auth events are logged
- ✅ Failed attempts are tracked
- ✅ Request IDs are present
Estimated Time: 1 week
Status: ✅ Completed (2026-01-15)
Tasks:
- Create Dockerfile for auth-server
- Create Dockerfile for migration-runner (separate service for DB migrations)
- Create Dockerfile for developer-portal (prod + dev images, compose service; #129)
- Create docker-compose.yml (PostgreSQL 18 + Redis 7 + QAuth)
- Write deployment documentation (README.md, docs/docker.md)
- Environment variable configuration (.env.docker.example)
- Database migration strategy (migration-runner service using Nx targets)
- Add .dockerignore
- Multi-stage builds for optimization
- Health checks for all services
- Non-root user in containers
- JWT key management strategy (ADR-001)
docker-compose.yml (simplified, see actual file for full config):
services:
postgres:
image: postgres:18-alpine # PostgreSQL 18 for uuidv7() support
environment:
POSTGRES_DB: qauth
POSTGRES_USER: qauth
POSTGRES_PASSWORD: ${DB_PASSWORD}
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U qauth -d qauth']
volumes:
- postgres_data:/var/lib/postgresql/data
redis:
image: redis:7-alpine
healthcheck:
test: ['CMD', 'redis-cli', 'ping']
volumes:
- redis_data:/data
migration-runner:
build:
context: .
dockerfile: apps/migration-runner/Dockerfile
depends_on:
postgres:
condition: service_healthy
restart: 'no' # Run once and exit
auth-server:
build:
context: .
dockerfile: apps/auth-server/Dockerfile
environment:
# See .env.docker.example for all variables
JWT_PRIVATE_KEY: ${JWT_PRIVATE_KEY}
JWT_PUBLIC_KEY: ${JWT_PUBLIC_KEY}
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
migration-runner:
condition: service_completed_successfully
healthcheck:
test: ['CMD', 'node', '-e', "require('http').get('http://localhost:3000/health')"]
volumes:
postgres_data:
redis_data:Dockerfile (auth-server) - Multi-stage build with corepack:
# Stage 1: Dependencies
FROM node:24-alpine AS deps
WORKDIR /app
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml nx.json tsconfig.base.json ./
RUN corepack enable && pnpm install --frozen-lockfile
# Stage 2: Builder
FROM deps AS builder
COPY vitest.config.ts libs apps ./
RUN pnpm nx build auth-server --prod
# Stage 3: Runner (minimal production image)
FROM node:24-alpine AS runner
WORKDIR /app
RUN corepack enable
# Copy built application and dependencies
COPY --from=builder /app/dist/apps/auth-server ./
COPY --from=builder /app/dist/libs ./dist/libs
COPY --from=builder /app/package.json /app/pnpm-lock.yaml /app/node_modules ./
# Security: non-root user
RUN addgroup -g 1001 -S nodejs && adduser -S nodejs -u 1001
USER nodejs
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
CMD node -e "require('http').get('http://localhost:3000/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"
ENTRYPOINT ["/bin/sh", "/app/docker-entrypoint.sh"]Acceptance Criteria:
- ✅ Docker images build successfully
- ✅ docker-compose starts all services
- ✅ Health checks work in Docker
- ✅ Migrations run on startup
- ✅ Deployment documentation is complete
Estimated Time: 1 week
Total Estimated Time: 4-6 weeks
Deliverables:
- ✅ Production-ready security
- ✅ OIDC 1.0 compliance
- ✅ Monitoring and structured logging
- ✅ Docker deployment
- ✅ Deployment documentation
What You Can Do After Phase 3:
- Deploy to production
- Use QAuth for real applications
- Monitor system health
- Comply with OIDC 1.0 standards
Timeline: 6-8 weeks Status: Not Started — deferred (long-term platform per ADR-007); gated on the ADR-002 identifier-abstraction migration, which is the first task of this phase.
Objective: Enable QAuth to accept identity from standards-compliant VC wallets as an upstream source. Downstream applications receive standard OAuth 2.1 tokens with no changes required. The EU EUDI Wallet (eIDAS 2.0) is the primary integration test target, but the architecture is wallet-agnostic.
Tasks:
- OID4VP authorization request generation (JAR +
request_uri) - OID4VP response intake endpoint (
direct_posttoresponse_uri) — under HAIP the encrypteddirect_post.jwtvariant instead; see ADR-004 § Spec status (2026-07-20), #296/#298 - Trust anchor validation (extensible registry: EU Trusted List + others)
-
federation-corelibrary: normalise Verifiable Credentials → internal user model - Claims extraction from Verifiable Credentials (email, age, nationality, professional credentials) — the subject-identity basis is an open decision; OID4VP 1.0 provides no stable wallet identifier (ADR-004 § Spec status (2026-07-20), #296)
- Create or link internal user record from the presented credential — linking basis open, see #296
- Issue standard OAuth 2.1 access token + OIDC ID token after VC validation
- Wallet login UI flow in
auth-ui - Inverse direction: QAuth as Verifiable Credential issuer (OID4VCI)
- End-to-end integration tests against EUDI reference wallet
Acceptance Criteria:
- A user holding a standards-compliant VC wallet can authenticate to a QAuth-protected application
- The downstream application sees a normal OIDC token — no protocol changes required
- VC trust anchor validation passes for EU Trusted List member state credentials
- Existing email/password login is unaffected
Timeline: 4-6 weeks Status: Not Started
Objective: Introduce hybrid post-quantum JWT signing with a crypto-agile abstraction layer. No business logic changes when the underlying algorithm is swapped.
Key decisions from the 2026 PQC landscape assessment:
- Algorithm: ML-DSA-65 (NIST FIPS 204, Level 3) — minimum floor recommended by BSI and ANSSI
- Hybrid model: Composite ML-DSA-65 + Ed25519, following
draft-ietf-lamps-pq-composite-sigs(JOSE WG adoption Jan 2026) - Implementation: Native Node.js binding via napi-rs wrapping
aws-lc-rs(same pattern as@node-rs/argon2) - Dev/CI fallback:
@noble/post-quantum(pure TypeScript, audited — no native deps) - Token architecture: Reference tokens (RFC 7662 introspection) as default, to manage PQC JWT size (ML-DSA-65 signatures are 3,309 B vs. Ed25519's 64 B)
Tasks:
- Design algorithm-agnostic abstraction:
sign(payload, key)/verify(token, key)/generateKeyPair(alg) - Implement
@qauth-labs/cryptonapi-rs binding wrappingaws-lc-rs - ML-DSA-65 key generation, signing, and verification
- Hybrid composite signing: ML-DSA-65 + Ed25519 per IETF LAMPS draft
- JWKS endpoint: support mixed key types (
"kty": "AKP"for ML-DSA per JOSE draft) - Reference-token architecture: short-lived opaque tokens with introspection as default path
-
@noble/post-quantumdev/CI fallback wired through same abstraction - Security review of cryptographic implementation
Acceptance Criteria:
- JWT tokens are signed with hybrid ML-DSA-65 + Ed25519
- Tokens are verifiable by both classical (Ed25519 only) and PQC-capable verifiers
- Swapping the underlying crypto implementation requires zero changes to auth-server business logic
- PQC JWT size does not break HTTP header or cookie limits (reference-token architecture)
Future (Phase 5+):
- FN-DSA (NIST FIPS 206, pending finalization ~2027) evaluation — compact signatures (~666 B) may make self-contained PQC JWTs practical
libcruxas formally-verified upgrade path overaws-lc-rs
These features are NOT part of Phases 1–5:
- Social login (Google, GitHub, Microsoft)
- TOTP-based MFA
- WebAuthn / Passkeys
- Magic link authentication
- SAML 2.0 support
- LDAP / Active Directory integration
- Custom domains, advanced multi-tenancy
- Organizations & Teams
- Advanced RBAC
- Audit logs UI
- Webhook system
- GraphQL API
- Microservices extraction (Token service, Session service)
- gRPC communication between services
- Horizontal scaling, CDN integration
- Multi-region support
Phase 9: Agent Authentication & Authorization (pulled forward — see ADR-007)
Re-baselined to near-term. The OAuth 2.1 / MCP protocol foundation for this phase shipped early in PR #156 (discovery, dynamic client registration, resource-indicator audience binding, public-client PKCE, consent). The remaining substance below — agent client type, modes, granular scopes, step-up, agent-action audit — is now the T2 differentiation track, augmented with on-behalf-of delegation via OAuth Token Exchange (RFC 8693).
- "Agent" client type on QAuth (register and identify agents)
- Agent session state / mode (ReadOnly, Admin, Exec)
- Granular scopes (e.g.
fs:read,fs:write,exec:run) enforced per client/mode - Step-up auth (MFA/OTP) before critical operations
- QAuth-side audit log of agent actions by mode for compliance
| Phase | Duration | Status |
|---|---|---|
| Phase 0: Foundation Setup | 1-2 weeks | ✅ Completed |
| Phase 1: Core Auth Server | 6-8 weeks | ✅ Completed |
| Phase 2: Developer Portal | 3-4 weeks | In Progress (API ✅, UI ⏳) |
| Phase 3: Production Hardening | 4-6 weeks | ✅ Complete (T3) |
| Phase 4: Wallet Federation Bridge | 6-8 weeks | Not Started |
| Phase 5: Post-Quantum Crypto | 4-6 weeks | Not Started |
| Phase 6+: Enterprise & Scale | TBD | Not Started |
Technical Metrics:
- 100% OAuth 2.1 compliance (RFC 9700)
- 100% OIDC 1.0 compliance (OpenID Foundation conformance tests)
- <100ms token validation time
- <1s authorization flow completion
- Zero critical security vulnerabilities
- Hybrid ML-DSA-65 + Ed25519 JWT signing with no HTTP header regressions (Phase 5)
Developer Experience:
- Developer can register an OAuth client in <5 minutes
- Developer can integrate QAuth in <30 minutes
- Clear error messages for all auth failures
- Documentation covers core use cases
Federation:
- A VC wallet user can authenticate to a QAuth-protected application with no changes to the application (Phase 4)
- Trust anchor validation passes for at least one standards-compliant VC issuer registry (Phase 4)
- Ship Early, Ship Often - Each phase should be deployable
- Security First - Never compromise security for speed
- Documentation Later - Focus on working code first
- Test Critical Paths - Focus on auth flows
- Use Existing Tools - Don't reinvent the wheel
- Simplify - If complex, break down or defer
- No Gold Plating - MVP means minimum
- Ask for Help - Use communities when stuck
Backend:
- Fastify - Web framework
- Drizzle ORM - Database access
- PostgreSQL - Database
- Redis - Sessions and caching
- @node-rs/argon2 - Password hashing (Rust native binding)
- zxcvbn - Password strength validation
- jose - JWT generation (EdDSA)
- resend - Email delivery (production)
- nodemailer - SMTP email delivery
- @react-email/components - Email templates
- zod - Schema validation
Frontend (Developer Portal):
- TanStack Start - Full-stack React framework
- React 19 - UI library
- Tailwind CSS - Styling
- Radix UI - Accessible components
Testing:
- Vitest - Unit and integration testing
- Supertest - HTTP API testing
- @qauth-labs/shared-testing - Test utilities and fixtures
DevOps:
- Docker - Containerization
- docker-compose - Local development
- pino - Structured logging
- helmet - Security headers
- fastify-rate-limit - Rate limiting
- Decision: @node-rs/argon2 (Rust native binding)
- Rationale: Fast, secure, quantum-resistant, no WASM complexity
- Decision: EdDSA (Ed25519) for MVP
- Rationale: Fast, secure, simple. Hybrid PQC later (Phase 5)
- Decision: 15 minutes (access), 7 days (refresh)
- Rationale: Balance security and UX
- Decision: Required for production use
- Rationale: Prevent spam, improve security
- Decision: REST for MVP
- Rationale: Simple, standards-compliant. GraphQL in Phase 5+
- Decision: Redis-based token bucket
- Rationale: Fast, scalable, shared across instances
- README.md - Project overview
- Docker Guide - Local development with Docker
- ADR Index - Architecture Decision Records
Note: This is a simplified SQL representation reflecting the Phase 1 tables only. The actual implementation uses Drizzle ORM with TypeScript schemas and has since been extended with agent-auth (
is_agent,max_agent_mode), environment-aware authorization (environment,max_environment_laxity), API keys, refresh-token families (family_id,previous_token_hash,revoked_reason), RFC 8707resource, and per-agent audit attribution — none of which are shown here. For the complete, up-to-date schema, seelibs/infra/db/src/lib/schema/or the DBML file atlibs/infra/db/src/qauth-schema.dbml.
- UUIDv7 Primary Keys: Time-ordered UUIDs for better B-tree index performance and chronological sorting
- BIGINT Timestamps: Epoch milliseconds (not TIMESTAMP) for efficient storage and timezone-independent queries
- JSONB Columns: Flexible storage for metadata, arrays (grant_types, scopes), and policies (password_policy)
- PostgreSQL Enums: Type-safe enums for grant types, response types, token endpoint auth methods, etc.
- Multi-tenancy: All data scoped to realms for complete isolation
- Optimized Indexes: Composite indexes, partial indexes for active records, unique constraints at column level
-- PostgreSQL Enums (must be created first)
CREATE TYPE token_endpoint_auth_method AS ENUM ('client_secret_post', 'client_secret_basic', 'private_key_jwt', 'none');
CREATE TYPE ssl_required AS ENUM ('none', 'external', 'all');
CREATE TYPE code_challenge_method AS ENUM ('S256');
CREATE TYPE grant_type AS ENUM ('authorization_code', 'refresh_token', 'client_credentials');
CREATE TYPE response_type AS ENUM ('code');
CREATE TYPE audit_event_type AS ENUM ('auth', 'token', 'client', 'security', 'user', 'realm');
-- Realms (Multi-tenancy)
CREATE TABLE realms (
id UUID PRIMARY KEY DEFAULT uuidv7(),
name VARCHAR(255) UNIQUE NOT NULL,
enabled BOOLEAN DEFAULT TRUE,
access_token_lifespan BIGINT DEFAULT 900,
refresh_token_lifespan BIGINT DEFAULT 604800,
ssl_required ssl_required DEFAULT 'external',
password_policy JSONB,
created_at BIGINT NOT NULL DEFAULT (EXTRACT(EPOCH FROM NOW())::bigint * 1000),
updated_at BIGINT NOT NULL DEFAULT (EXTRACT(EPOCH FROM NOW())::bigint * 1000)
);
-- Users
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT uuidv7(),
realm_id UUID NOT NULL REFERENCES realms(id) ON DELETE CASCADE,
email VARCHAR(255) NOT NULL,
email_normalized VARCHAR(255) NOT NULL,
password_hash TEXT NOT NULL,
email_verified BOOLEAN DEFAULT FALSE,
enabled BOOLEAN DEFAULT TRUE,
created_at BIGINT NOT NULL DEFAULT (EXTRACT(EPOCH FROM NOW())::bigint * 1000),
updated_at BIGINT NOT NULL DEFAULT (EXTRACT(EPOCH FROM NOW())::bigint * 1000)
);
CREATE UNIQUE INDEX idx_users_realm_email_normalized_unique ON users(realm_id, email_normalized);
-- Email Verification Tokens
CREATE TABLE email_verification_tokens (
id UUID PRIMARY KEY DEFAULT uuidv7(),
token VARCHAR(255) UNIQUE NOT NULL,
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
expires_at BIGINT NOT NULL,
used BOOLEAN DEFAULT FALSE,
created_at BIGINT NOT NULL DEFAULT (EXTRACT(EPOCH FROM NOW())::bigint * 1000)
);
-- OAuth Clients
CREATE TABLE oauth_clients (
id UUID PRIMARY KEY DEFAULT uuidv7(),
realm_id UUID NOT NULL REFERENCES realms(id) ON DELETE CASCADE,
client_id VARCHAR(255) NOT NULL,
client_secret_hash TEXT NOT NULL,
name VARCHAR(255) NOT NULL,
redirect_uris JSONB NOT NULL,
grant_types JSONB NOT NULL DEFAULT '["authorization_code","refresh_token"]'::jsonb,
response_types JSONB NOT NULL DEFAULT '["code"]'::jsonb,
scopes JSONB NOT NULL DEFAULT '[]'::jsonb,
audience JSONB,
token_endpoint_auth_method token_endpoint_auth_method NOT NULL DEFAULT 'client_secret_post',
require_pkce BOOLEAN DEFAULT TRUE,
enabled BOOLEAN DEFAULT TRUE,
created_at BIGINT NOT NULL DEFAULT (EXTRACT(EPOCH FROM NOW())::bigint * 1000),
updated_at BIGINT NOT NULL DEFAULT (EXTRACT(EPOCH FROM NOW())::bigint * 1000)
);
CREATE UNIQUE INDEX idx_oauth_clients_realm_client_id_unique ON oauth_clients(realm_id, client_id);
-- Authorization Codes
CREATE TABLE authorization_codes (
id UUID PRIMARY KEY DEFAULT uuidv7(),
code VARCHAR(255) UNIQUE NOT NULL,
oauth_client_id UUID NOT NULL REFERENCES oauth_clients(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
redirect_uri TEXT NOT NULL,
code_challenge TEXT NOT NULL,
code_challenge_method code_challenge_method NOT NULL DEFAULT 'S256',
expires_at BIGINT NOT NULL,
used BOOLEAN DEFAULT FALSE,
created_at BIGINT NOT NULL DEFAULT (EXTRACT(EPOCH FROM NOW())::bigint * 1000)
);
-- Refresh Tokens
CREATE TABLE refresh_tokens (
id UUID PRIMARY KEY DEFAULT uuidv7(),
token_hash TEXT UNIQUE NOT NULL,
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
oauth_client_id UUID NOT NULL REFERENCES oauth_clients(id) ON DELETE CASCADE,
expires_at BIGINT NOT NULL,
revoked BOOLEAN DEFAULT FALSE,
created_at BIGINT NOT NULL DEFAULT (EXTRACT(EPOCH FROM NOW())::bigint * 1000)
);
-- Audit Logs
CREATE TABLE audit_logs (
id UUID PRIMARY KEY DEFAULT uuidv7(),
user_id UUID REFERENCES users(id) ON DELETE SET NULL,
oauth_client_id UUID REFERENCES oauth_clients(id) ON DELETE SET NULL,
event VARCHAR(100) NOT NULL,
event_type audit_event_type NOT NULL,
success BOOLEAN DEFAULT TRUE,
ip_address VARCHAR(45),
user_agent TEXT,
metadata JSONB,
created_at BIGINT NOT NULL DEFAULT (EXTRACT(EPOCH FROM NOW())::bigint * 1000)
);
-- Sessions (optional, can use Redis instead)
CREATE TABLE sessions (
id UUID PRIMARY KEY DEFAULT uuidv7(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
oauth_client_id UUID REFERENCES oauth_clients(id) ON DELETE SET NULL,
expires_at BIGINT NOT NULL,
revoked BOOLEAN DEFAULT FALSE,
created_at BIGINT NOT NULL DEFAULT (EXTRACT(EPOCH FROM NOW())::bigint * 1000)
);- All foreign keys use UUID primary keys (not VARCHAR client_id) for better performance
oauth_client_idreferencesoauth_clients.id(UUID), notoauth_clients.client_id(VARCHAR)- Cascade deletes for dependent records, SET NULL for optional relationships
| Endpoint | Method | Description | Phase |
|---|---|---|---|
/auth/register |
POST | Register new user | 1.2 |
/auth/resend-verification |
POST | Resend verification email | 1.3 |
/auth/verify |
GET | Verify email | 1.3 |
/auth/login |
POST | Login with email/password | 1.4 |
/auth/refresh |
POST | Refresh access token | 1.5 |
/auth/logout |
POST | Logout and revoke session | 1.5 |
/oauth/authorize |
GET | OAuth authorization endpoint | 1.6 |
/oauth/token |
POST | Token exchange — authorization_code (PKCE) and client_credentials; client_secret_post or client_secret_basic |
1.6 |
/oauth/introspect |
POST | Token introspection | 1.7 |
/userinfo |
GET | OIDC userinfo endpoint | 1.7 |
/health |
GET | Health check | 1.8 |
/.well-known/openid-configuration |
GET | OIDC discovery | 3.2 |
/.well-known/jwks.json |
GET | JWKS public keys | 3.2 |
/metrics |
GET | Prometheus metrics | 3.3 |
/api/clients |
GET | List OAuth clients (shipped) — scoped by developer, secret never returned | 2.2 |
/api/clients |
POST | Create OAuth client (shipped #86) | 2.2 |
/api/clients/:id |
GET | Get client details (shipped #87) | 2.2 |
/api/clients/:id |
PATCH | Update client (shipped #88) | 2.2 |
/api/clients/:id |
DELETE | Delete client (shipped #89) | 2.2 |
/api/clients/:id/regenerate-secret |
POST | Regenerate client secret (shipped #90) | 2.2 |
/api/clients/:id/api-keys |
POST | Mint developer API key (shipped #97, env-gated per ADR-008) | 2.2 |
/api/clients/:id/api-keys |
GET | List masked developer API keys (shipped #97) | 2.2 |
/api/clients/:id/api-keys/:keyId |
DELETE | Revoke developer API key (shipped #97) | 2.2 |
The QAuth monorepo is organized into the following libraries:
| Library | Package | Description |
|---|---|---|
config |
@qauth-labs/server-config |
Environment configuration with Zod schemas |
email |
@qauth-labs/server-email |
Email service with multiple providers (Resend, SMTP, Mock) |
password |
@qauth-labs/server-password |
Password hashing with Argon2id |
| Library | Package | Description |
|---|---|---|
db |
@qauth-labs/infra-db |
PostgreSQL database with Drizzle ORM, repositories |
cache |
@qauth-labs/infra-cache |
Redis connection and caching utilities |
| Library | Package | Description |
|---|---|---|
errors |
@qauth-labs/shared-errors |
Centralized error handling (auth, database, common) |
validation |
@qauth-labs/shared-validation |
Validation utilities (email, password strength) |
testing |
@qauth-labs/shared-testing |
Test helpers (Fastify, Supertest, fixtures) |
| Plugin | Package | Description |
|---|---|---|
db |
@qauth-labs/fastify-plugin-db |
Database plugin with repository injection |
cache |
@qauth-labs/fastify-plugin-cache |
Redis cache plugin |
password |
@qauth-labs/fastify-plugin-password |
Password hasher and validator injection |
email |
@qauth-labs/fastify-plugin-email |
Email service injection with provider selection |
End of PRD v2.0