commits
github.repository_owner preserves case but Docker tags must be
lowercase. Use tr to normalize the image name.
Builds multi-arch Docker image (amd64 + arm64) and pushes to
ghcr.io/cache8063/atauth-gateway on version tag push.
- Remove DO-HANDOFF.md (deployment-specific ops document)
- Update HOMELAB.md clone URL to GitHub
- Update CHANGELOG.md comparison links to GitHub
- Add v2.3.0 changelog entry
- Add GitHub Actions CI workflow (test on push/PR)
Add passkey/WebAuthn support to the forward-auth proxy login flow,
previously only available in the OIDC authorize flow:
- New POST /auth/proxy/passkey endpoint
- Passkey button + WebAuthn JS on proxy login page
- 9 new tests for proxy passkey flow
Security fixes from audit:
- MFA verify endpoints require authentication (prevent unauthenticated brute-force)
- Session expiry enforced in database query
- OIDC logout redirect uses origin comparison (prevent open redirect)
- PKCE plain method rejected, S256 uses constant-time comparison
- Empty DID guard in token endpoint
- Handle format validation in OIDC authorize
- trust proxy configured for correct client IP
- user_id NaN validation
- Removed uuid package (use crypto.randomUUID())
Add GET /version returning service name, package version, and build
commit SHA. Pass BUILD_COMMIT as Docker build arg through CI pipeline.
Include deployment inventory and RCA docs from infrastructure
consolidation (single K8s atauth instance).
Namespace-scoped kubeconfig and RBAC already provisioned on
the cluster. Doc covers connection setup, deployment architecture,
env vars, common operations, CI/CD, and gotchas.
Admin dashboard CSS: gradient background, frosted glass sidebar,
card shadows with hover lifts, consistent 12px border-radius matching
the login modal theme.
README: add screenshots section with placeholders, update clone URL
to GitHub.
- Remove gateway/docker-compose.yaml (superseded by root compose)
- Remove gateway/DEPLOYMENT.md (references deleted k8s/helm dirs)
- Remove gateway/README.md (stale, root README is canonical)
- Remove root .env.example (gateway/.env.example is the complete reference)
- Fix clone URLs and CHANGELOG links: GitHub -> Gitea
- Trim CONTRIBUTING.md to reflect current gateway-focused workflow
The /auth/passkey/authenticate/options endpoint returns
{success, options: {...}} but the login page JS was reading
opts.challenge directly instead of opts.options.challenge,
causing the WebAuthn ceremony to fail with undefined values.
Add String() wrappers on all req.params usages to satisfy Express 5's
string | string[] union type. Restore .gitea/workflows/deploy.yml that
was accidentally removed during FOSS sanitization.
Major upgrades:
- @simplewebauthn/server 10.0.1 -> 13.2.3: migrate to v13 credential
API (nested credential object in registrationInfo, renamed authenticator
param to credential in verifyAuthenticationResponse), move type imports
from retired @simplewebauthn/types to @simplewebauthn/server
- better-sqlite3 11.10.0 -> 12.6.2: SQLite 3.50.2 engine, drop Node 18
Patch/minor updates (via npm update):
- @atproto/oauth-client-node 0.3.13 -> 0.3.17
- cors 2.8.5 -> 2.8.6
- otpauth 9.2.1 -> 9.5.0
- qrcode 1.5.3 -> 1.5.4
Also resolves all 4 npm audit vulnerabilities (qs, undici, ajv, minimatch).
All 394 tests pass.
Use single root-level node_modules/ instead of per-directory patterns.
Prevents stale node_modules from appearing as untracked.
- docker-compose.yml: use env_file instead of individual env vars,
fixing missing OIDC/passkey/MFA config that broke Docker deployments
- README: add prerequisites, verify step, app configuration step,
CORS_ORIGINS to domain config, forward-auth env var instructions,
passkey feature mention
- HOMELAB: update deploy section to match README flow, add all 20
presets, add forward-auth env setup, add CORS troubleshooting
- .env.example: note Docker Compose DB_PATH override
The Quick Start was missing the domain configuration step and how
to expose the service via a reverse proxy. Now walks through all
three steps: configure domain + secrets, start with docker compose,
set up TLS reverse proxy, then open the admin dashboard.
CLAUDE.md and .claude/ are local development tooling and should
not be in the public repository.
Extract project structure, testing guide, and code patterns into
.claude/commands/ skills (/structure, /testing, /patterns). Keep
CLAUDE.md as a lean quick-reference. Fix inflated test count
(394 actual, not 827 which included dist/ duplicates).
- Add CLAUDE.md development guide (project structure, testing, gotchas)
- Update README with 10 new OIDC presets (22 total)
- Add CHANGELOG v2.2.0 entry
- Update SECURITY.md supported versions for v2.x
- Add audit_log table and admin operation logging
- Add GET /admin/audit-log endpoint
- Validate CORS wildcard at startup, limit body size to 16kb
- Validate email format on removal, handle format on proxy auth
Add tests for OIDC revocation (RFC 7009), logout/end_session,
userinfo endpoint, and passkey service (WebAuthn registration,
authentication, credential management). Raises overall coverage
from ~50% to ~56% statements.
Use **/*.test.ts instead of *.test.ts to match test files in
subdirectories during Docker builds.
Bump gateway version for v2.2.0 release. Include unit tests for
rate limiting, session management, token handling, error utilities,
and HMAC verification.
Remove infrastructure-specific files (.claude/, CLAUDE.md, .gitea/,
SAFE-DEPLOY.md). Replace private domains with example.com in tests,
docs, and comments. Update clone URLs to GitHub.
OAuthResolverError was falling through to the generic 500 handler,
giving users no useful feedback when they mistyped their handle.
Now returns 400 with specific guidance about DNS records or handle
spelling.
Paperless-ngx, Vaultwarden, Miniflux, Mattermost, Vikunja,
Plane, GoToSocial, Stirling-PDF, Tandoor Recipes, FreshRSS.
Brings total wizard presets from 10 to 20. Each includes
verified redirect URI, PKCE/auth method settings, and
step-by-step setup instructions from official documentation.
Profile page operations (delete passkey + reload + register + reload)
easily exhaust 10 req/min when combined with normal page loads.
req.protocol returns 'http' behind reverse proxy, causing the
login redirect URL to fail the allowed origins check (403).
residentKey: 'preferred' → 'required' so Firefox stores passkeys
as discoverable credentials. The OIDC login page authenticates
without allowCredentials (user not identified yet), which requires
discoverable credentials to work.
Add self-service profile page at /auth/profile for passkey
registration/management and session management. Refactor login
pages to dark theme with gradient accents, no emojis.
Pins deployment to specific commit SHA tag rather than relying on
:latest tag with rollout restart, which fails when the deployment
image was previously set to a different tag.
- Prefix unused nonce params with _ in proxy-auth.ts render functions
- Remove unused imports (vi, beforeEach, afterEach) from test files
- Use bare catch in admin-dashboard.ts access rule handler
- Remove unused variable in authorize.test.ts
Users with a registered passkey can now sign in via WebAuthn on the
OIDC login page, skipping the Bluesky OAuth round-trip entirely.
- Add POST /oauth/authorize/passkey endpoint
- Wire PasskeyService through to OIDC authorize router
- Add passkey button with inline WebAuthn JS (feature-detected)
- 8 new tests for passkey authorize flow
- Remove hardcoded fallback values for OIDC_KEY_SECRET, MFA_ENCRYPTION_KEY,
and FORWARD_AUTH_SESSION_SECRET; fail-fast validation at startup when
features are enabled
- Escape all user-controlled template interpolations in OIDC login page
(clientName, state, authCode, errorMessage) to prevent XSS
- Replace URL query param secret display with one-time flash tokens
(in-memory Map with 60s TTL, consumed on first access)
- Update nodemailer from v6 to v7 (SES transport security fix)
- Update docker-compose.yaml to use required variable syntax
- Update .env.example with all current configuration options
- Bump version to 2.0.2
Fix unused variable errors: prefix catch vars with _, remove unused
imports, add caughtErrorsIgnorePattern to eslint config.
Instead of a generic 500, catch OAuthResolverError and return a clear
message explaining why the handle couldn't be resolved (missing DNS
TXT record, handle not found, or PDS unreachable).
Move deployment, admin API, and troubleshooting procedures from
CLAUDE.md into .claude/commands/ skills (on-demand context). Rewrite
README.md and HOMELAB.md to reflect current OIDC provider + forward-auth
proxy architecture. Bump package.json to 2.0.1.
- Fix setup wizard double-protocol bug: strip scheme prefix (https://)
from domain input before template substitution. Affected both GET
/clients/new pre-fill and POST /clients/wizard/:preset creation.
Added stripScheme() helper and regression test.
- Remove stale directories with zero active dependencies:
gateway/admin-ui/ (replaced by server-rendered dashboard)
gateway/tekton/ (old k3s CI, replaced by Gitea Actions)
gateway/helm/ (unused Helm chart)
gateway/k8s/ (old k3s kustomize manifests, CI uses kubectl directly)
gateway/.gitea/workflows/build.yaml (stale k3s deploy workflow)
.github/ (GitHub Actions, repo is on Gitea)
- Dockerfile: remove admin-ui-builder stage and COPY, renumber stages
- .dockerignore: remove references to deleted dirs
263 tests pass (262 existing + 1 new regression test).
- CLAUDE.md: Update test counts (262), add OIDC key files, dashboard
routes, ABS integration details, fixed-bug troubleshooting entries,
accurate pending work list
- CHANGELOG.md: Add v2.0.0 entry covering OIDC provider, forward-auth
proxy, admin dashboard, setup wizard, CI/CD migration, and bug fixes
- SAFE-DEPLOY.md: Rewrite for DigitalOcean (was stale k3s references)
- .gitignore: Add *.db-shm, *.db-wal, *.db-journal patterns
Three bugs discovered during Audiobookshelf OIDC integration:
1. authorize.ts: Pass explicit redirect_uri to AT Protocol OAuth callback
handler. The @atproto/oauth-client library falls back to
clientMetadata.redirect_uris[0] (/auth/callback) when no redirect_uri
is provided, but the authorization request used /oauth/callback.
2. token.ts, revoke.ts: Hash incoming client secret with SHA-256 before
comparing against stored hash. The dashboard stores hashed secrets but
the token/revoke endpoints were comparing raw secrets against hashes.
3. userinfo.ts: Resolve DID to handle via AT Protocol API
(app.bsky.actor.getProfile) when no cached mapping exists. Previously
returned empty handle, causing clients to reject the callback.
Add admin dashboard pages for OIDC client CRUD (list, create, edit, delete,
rotate secret), a setup wizard with 10 app presets (Audiobookshelf, Jellyfin,
Nextcloud, Gitea, Immich, Grafana, WikiJS, Portainer, Outline, Mealie), and a
forward-auth proxy quick-setup wizard with nginx/k8s config snippets.
Add integration tests for /auth/init redirect_uri handling per RCA-2026-02-22
to prevent regression of the bug where downstream app callbacks were passed
as OAuth redirect_uri to the AT Protocol PDS.
262 tests passing, type check clean.
Documents the root cause of the 500 errors affecting all OIDC-dependent
apps. Two bugs: OIDC callbacks not registered in NodeOAuthClient, and
legacy auth flow passing downstream app callbacks as OAuth redirect_uri.
The /auth/init route was passing the downstream app's callback URL
(e.g. https://md.bkb.cx/api/auth/callback) as the OAuth redirect_uri
to @atproto/oauth-client. This is wrong -- the PDS must redirect back
to ATAuth's own callback, not the downstream app.
Now generateAuthUrl accepts a separate appRedirectUri parameter that
is stored in the OAuth state for post-auth redirect, while the OAuth
redirect_uri defaults to ATAuth's registered callback.
The NodeOAuthClient validates that redirect_uri is in its registered
redirect_uris before sending to the PDS. The /client-metadata.json
endpoint correctly listed OIDC and forward-auth callback URIs, but
the internal NodeOAuthClient instance only had the primary auth
callback and proxy callback registered.
This caused "TypeError: Invalid redirect_uri" when the OIDC authorize
flow called generateAuthUrl with the OIDC callback URI, resulting in
500 errors for all OIDC-dependent apps (mdeditor, mydrawings).
Fix: Pass additional redirect URIs (OIDC callback, forward-auth proxy
callback) to OAuthService.initialize() so they're registered in the
NodeOAuthClient's clientMetadata.redirect_uris array.
Per-user/domain access rules for the forward-auth proxy with deny-overrides
evaluation. Default-deny when rules exist, backward-compatible open access
when no rules are configured. Admin dashboard with cookie-based auth for
managing origins, access rules, sessions, and dry-run access checks.
The oauth_states table has a FK constraint on apps(id). The forward-auth
proxy uses app_id 'proxy-auth' which didn't exist, causing
SQLITE_CONSTRAINT_FOREIGNKEY errors on login.
Implements nginx auth_request compatible endpoints so any service behind
the ingress can be gated behind ATAuth login without code changes.
New endpoints:
- GET /auth/verify - nginx auth_request target (checks cookie or ticket)
- GET/POST /auth/proxy/login - login entry point with silent SSO
- GET /auth/proxy/callback - AT Proto OAuth callback for proxy flow
- GET /auth/proxy/logout - centralized session logout
Admin API additions:
- CRUD for allowed origins (proxy/origins)
- List/revoke proxy sessions (proxy/sessions)
Also fixes CSP nonce on OIDC login page inline scripts.
Chrome enforces form-action 'self' on redirect targets, not just
the form action URL. When the login form POSTs to /oauth/authorize/login
and the response is a 302 to bsky.social, Chrome silently blocks
the redirect. Firefox only checks the action attribute, so it works.
Removing the form-action directive fixes Chrome login. The form still
only POSTs to self (hardcoded in HTML), so this doesn't reduce security.
bsky.social intermittently fails to fetch our client-metadata.json,
returning invalid_client_metadata. This adds a 1-second retry for
that specific error, a clearer error message, and prevents the
form from being submitted multiple times (users were rapid-clicking
5-6 times when it appeared stuck).
Add colon-to-dot conversion in handle sanitization to fix typos
like "bkb:arcnode.xyz" (: is adjacent to . on keyboards).
Also update error message to mention custom domains, not just Bluesky.
Add passkey/WebAuthn support to the forward-auth proxy login flow,
previously only available in the OIDC authorize flow:
- New POST /auth/proxy/passkey endpoint
- Passkey button + WebAuthn JS on proxy login page
- 9 new tests for proxy passkey flow
Security fixes from audit:
- MFA verify endpoints require authentication (prevent unauthenticated brute-force)
- Session expiry enforced in database query
- OIDC logout redirect uses origin comparison (prevent open redirect)
- PKCE plain method rejected, S256 uses constant-time comparison
- Empty DID guard in token endpoint
- Handle format validation in OIDC authorize
- trust proxy configured for correct client IP
- user_id NaN validation
- Removed uuid package (use crypto.randomUUID())
- Remove gateway/docker-compose.yaml (superseded by root compose)
- Remove gateway/DEPLOYMENT.md (references deleted k8s/helm dirs)
- Remove gateway/README.md (stale, root README is canonical)
- Remove root .env.example (gateway/.env.example is the complete reference)
- Fix clone URLs and CHANGELOG links: GitHub -> Gitea
- Trim CONTRIBUTING.md to reflect current gateway-focused workflow
Major upgrades:
- @simplewebauthn/server 10.0.1 -> 13.2.3: migrate to v13 credential
API (nested credential object in registrationInfo, renamed authenticator
param to credential in verifyAuthenticationResponse), move type imports
from retired @simplewebauthn/types to @simplewebauthn/server
- better-sqlite3 11.10.0 -> 12.6.2: SQLite 3.50.2 engine, drop Node 18
Patch/minor updates (via npm update):
- @atproto/oauth-client-node 0.3.13 -> 0.3.17
- cors 2.8.5 -> 2.8.6
- otpauth 9.2.1 -> 9.5.0
- qrcode 1.5.3 -> 1.5.4
Also resolves all 4 npm audit vulnerabilities (qs, undici, ajv, minimatch).
All 394 tests pass.
- docker-compose.yml: use env_file instead of individual env vars,
fixing missing OIDC/passkey/MFA config that broke Docker deployments
- README: add prerequisites, verify step, app configuration step,
CORS_ORIGINS to domain config, forward-auth env var instructions,
passkey feature mention
- HOMELAB: update deploy section to match README flow, add all 20
presets, add forward-auth env setup, add CORS troubleshooting
- .env.example: note Docker Compose DB_PATH override
- Add CLAUDE.md development guide (project structure, testing, gotchas)
- Update README with 10 new OIDC presets (22 total)
- Add CHANGELOG v2.2.0 entry
- Update SECURITY.md supported versions for v2.x
- Add audit_log table and admin operation logging
- Add GET /admin/audit-log endpoint
- Validate CORS wildcard at startup, limit body size to 16kb
- Validate email format on removal, handle format on proxy auth
Users with a registered passkey can now sign in via WebAuthn on the
OIDC login page, skipping the Bluesky OAuth round-trip entirely.
- Add POST /oauth/authorize/passkey endpoint
- Wire PasskeyService through to OIDC authorize router
- Add passkey button with inline WebAuthn JS (feature-detected)
- 8 new tests for passkey authorize flow
- Remove hardcoded fallback values for OIDC_KEY_SECRET, MFA_ENCRYPTION_KEY,
and FORWARD_AUTH_SESSION_SECRET; fail-fast validation at startup when
features are enabled
- Escape all user-controlled template interpolations in OIDC login page
(clientName, state, authCode, errorMessage) to prevent XSS
- Replace URL query param secret display with one-time flash tokens
(in-memory Map with 60s TTL, consumed on first access)
- Update nodemailer from v6 to v7 (SES transport security fix)
- Update docker-compose.yaml to use required variable syntax
- Update .env.example with all current configuration options
- Bump version to 2.0.2
- Fix setup wizard double-protocol bug: strip scheme prefix (https://)
from domain input before template substitution. Affected both GET
/clients/new pre-fill and POST /clients/wizard/:preset creation.
Added stripScheme() helper and regression test.
- Remove stale directories with zero active dependencies:
gateway/admin-ui/ (replaced by server-rendered dashboard)
gateway/tekton/ (old k3s CI, replaced by Gitea Actions)
gateway/helm/ (unused Helm chart)
gateway/k8s/ (old k3s kustomize manifests, CI uses kubectl directly)
gateway/.gitea/workflows/build.yaml (stale k3s deploy workflow)
.github/ (GitHub Actions, repo is on Gitea)
- Dockerfile: remove admin-ui-builder stage and COPY, renumber stages
- .dockerignore: remove references to deleted dirs
263 tests pass (262 existing + 1 new regression test).
- CLAUDE.md: Update test counts (262), add OIDC key files, dashboard
routes, ABS integration details, fixed-bug troubleshooting entries,
accurate pending work list
- CHANGELOG.md: Add v2.0.0 entry covering OIDC provider, forward-auth
proxy, admin dashboard, setup wizard, CI/CD migration, and bug fixes
- SAFE-DEPLOY.md: Rewrite for DigitalOcean (was stale k3s references)
- .gitignore: Add *.db-shm, *.db-wal, *.db-journal patterns
Three bugs discovered during Audiobookshelf OIDC integration:
1. authorize.ts: Pass explicit redirect_uri to AT Protocol OAuth callback
handler. The @atproto/oauth-client library falls back to
clientMetadata.redirect_uris[0] (/auth/callback) when no redirect_uri
is provided, but the authorization request used /oauth/callback.
2. token.ts, revoke.ts: Hash incoming client secret with SHA-256 before
comparing against stored hash. The dashboard stores hashed secrets but
the token/revoke endpoints were comparing raw secrets against hashes.
3. userinfo.ts: Resolve DID to handle via AT Protocol API
(app.bsky.actor.getProfile) when no cached mapping exists. Previously
returned empty handle, causing clients to reject the callback.
Add admin dashboard pages for OIDC client CRUD (list, create, edit, delete,
rotate secret), a setup wizard with 10 app presets (Audiobookshelf, Jellyfin,
Nextcloud, Gitea, Immich, Grafana, WikiJS, Portainer, Outline, Mealie), and a
forward-auth proxy quick-setup wizard with nginx/k8s config snippets.
Add integration tests for /auth/init redirect_uri handling per RCA-2026-02-22
to prevent regression of the bug where downstream app callbacks were passed
as OAuth redirect_uri to the AT Protocol PDS.
262 tests passing, type check clean.
The /auth/init route was passing the downstream app's callback URL
(e.g. https://md.bkb.cx/api/auth/callback) as the OAuth redirect_uri
to @atproto/oauth-client. This is wrong -- the PDS must redirect back
to ATAuth's own callback, not the downstream app.
Now generateAuthUrl accepts a separate appRedirectUri parameter that
is stored in the OAuth state for post-auth redirect, while the OAuth
redirect_uri defaults to ATAuth's registered callback.
The NodeOAuthClient validates that redirect_uri is in its registered
redirect_uris before sending to the PDS. The /client-metadata.json
endpoint correctly listed OIDC and forward-auth callback URIs, but
the internal NodeOAuthClient instance only had the primary auth
callback and proxy callback registered.
This caused "TypeError: Invalid redirect_uri" when the OIDC authorize
flow called generateAuthUrl with the OIDC callback URI, resulting in
500 errors for all OIDC-dependent apps (mdeditor, mydrawings).
Fix: Pass additional redirect URIs (OIDC callback, forward-auth proxy
callback) to OAuthService.initialize() so they're registered in the
NodeOAuthClient's clientMetadata.redirect_uris array.
Implements nginx auth_request compatible endpoints so any service behind
the ingress can be gated behind ATAuth login without code changes.
New endpoints:
- GET /auth/verify - nginx auth_request target (checks cookie or ticket)
- GET/POST /auth/proxy/login - login entry point with silent SSO
- GET /auth/proxy/callback - AT Proto OAuth callback for proxy flow
- GET /auth/proxy/logout - centralized session logout
Admin API additions:
- CRUD for allowed origins (proxy/origins)
- List/revoke proxy sessions (proxy/sessions)
Also fixes CSP nonce on OIDC login page inline scripts.
Chrome enforces form-action 'self' on redirect targets, not just
the form action URL. When the login form POSTs to /oauth/authorize/login
and the response is a 302 to bsky.social, Chrome silently blocks
the redirect. Firefox only checks the action attribute, so it works.
Removing the form-action directive fixes Chrome login. The form still
only POSTs to self (hardcoded in HTML), so this doesn't reduce security.