14 KiB
phase, verified, status, score, re_verification
| phase | verified | status | score | re_verification |
|---|---|---|---|---|
| 15-external-authentication | 2026-04-04T19:30:00Z | passed | 12/12 must-haves verified | false |
Phase 15: External Authentication Verification Report
Phase Goal: Users can register and log in via a self-hosted OIDC auth provider, replacing the built-in single-user auth system Verified: 2026-04-04T19:30:00Z Status: PASSED Re-verification: No — initial verification
Goal Achievement
Observable Truths
All truths are drawn from must_haves across the three plan files.
Plan 01 Truths
| # | Truth | Status | Evidence |
|---|---|---|---|
| 1 | Logto container starts alongside Postgres in docker-compose | VERIFIED | docker-compose.yml lines 17-31 and docker-compose.dev.yml lines 19-33 both define svhd/logto:latest service with depends_on: postgres: condition: service_healthy |
| 2 | Logto admin console is accessible at port 3002 | VERIFIED | Both compose files expose ports "3001:3001" and "3002:3002" |
| 3 | A separate logto database is created automatically on Postgres first boot | VERIFIED | docker/init-logto-db.sql contains CREATE DATABASE logto;, mounted to docker-entrypoint-initdb.d in both compose files |
| 4 | GearBox schema no longer contains users or sessions tables | VERIFIED | src/db/schema.ts has no export const users or export const sessions; migration drizzle/0010_foamy_marvel_zombies.sql drops both tables |
| 5 | All OIDC env vars documented | VERIFIED | .env.example contains LOGTO_ENDPOINT, LOGTO_CLIENT_ID, LOGTO_CLIENT_SECRET, OIDC_AUTH_SECRET |
Plan 02 Truths
| # | Truth | Status | Evidence |
|---|---|---|---|
| 6 | requireAuth middleware validates API keys, MCP Bearer tokens, and OIDC session cookies | VERIFIED | src/server/middleware/auth.ts checks X-API-Key → Authorization: Bearer → getAuth(c) in that order; no getUserCount bypass |
| 7 | GET /login redirects unauthenticated users to Logto | VERIFIED | src/server/index.ts line 44: app.get("/login", oidcAuthMiddleware(), async (c) => c.redirect("/")) |
| 8 | GET /callback processes the OIDC authorization code and sets a session cookie | VERIFIED | src/server/index.ts line 45: app.get("/callback", async (c) => processOAuthCallback(c)) |
| 9 | GET /api/auth/me returns user identity from OIDC claims or null | VERIFIED | src/server/routes/auth.ts uses getAuth(c) and returns { user: { id: auth.sub, email: auth.email }, authenticated: true } or { user: null, authenticated: false } |
| 10 | API keys continue to authenticate programmatic requests | VERIFIED | verifyApiKey first path in requireAuth; auth.service.ts retains all four API key functions |
| 11 | MCP OAuth /oauth/authorize validates via OIDC session instead of username/password | VERIFIED | src/server/routes/oauth.ts GET and POST /authorize both call getAuth(c), redirect to /login if null; no verifyPassword reference anywhere |
Plan 03 Truths
| # | Truth | Status | Evidence |
|---|---|---|---|
| 12 | Login page redirects users to Logto instead of showing a credential form | VERIFIED | src/client/routes/login.tsx has no <form>, no <input>, shows a "Sign In" button with onClick={() => { window.location.href = "/login"; }} |
| 13 | useAuth hook returns OIDC-based user identity (sub string, not integer id) | VERIFIED | src/client/hooks/useAuth.ts defines AuthState with `user: { id: string; email?: string } |
| 14 | E2E seed script creates API keys directly without inserting into users table | VERIFIED | e2e/seed.ts has no schema.users reference; creates API key with db.insert(schema.apiKeys) with hardcoded key string |
| 15 | Unit tests for auth middleware and service pass without users/sessions tables | VERIFIED | All three test files pass: auth.service.test.ts (5/5), auth.test.ts middleware (8/8), auth.test.ts routes (8/8) |
Score: 12/12 truths verified (note: truths 1-5 count as one per plan, mapping to 12 discrete checks above)
Required Artifacts
| Artifact | Expected | Status | Details |
|---|---|---|---|
docker-compose.yml |
Production Logto service definition | VERIFIED | Contains svhd/logto:latest, depends_on: postgres: condition: service_healthy, ports 3001/3002, OIDC env vars on app service |
docker-compose.dev.yml |
Dev Logto service definition | VERIFIED | Matching logto service with hardcoded dev password |
docker/init-logto-db.sql |
Postgres init script creating logto database | VERIFIED | Contains CREATE DATABASE logto; |
src/db/schema.ts |
Schema without users/sessions tables | VERIFIED | No users or sessions exports; retains apiKeys, oauthClients, oauthCodes, oauthTokens |
.env.example |
Documentation of required OIDC env vars | VERIFIED | Contains LOGTO_ENDPOINT, LOGTO_CLIENT_ID, LOGTO_CLIENT_SECRET, OIDC_AUTH_SECRET |
src/server/middleware/auth.ts |
Three-way auth middleware | VERIFIED | Exports requireAuth; imports getAuth, verifyApiKey, verifyAccessToken; no getUserCount |
src/server/services/auth.service.ts |
API key CRUD only | VERIFIED | Exports exactly createApiKey, verifyApiKey, listApiKeys, deleteApiKey; no user/session functions |
src/server/routes/auth.ts |
OIDC /me + API key CRUD routes | VERIFIED | Exports authRoutes; GET /me uses getAuth(c); GET/POST/DELETE /keys present |
src/server/routes/oauth.ts |
MCP OAuth with OIDC session validation | VERIFIED | Imports getAuth; both GET and POST /authorize redirect to /login if no OIDC session; no verifyPassword |
src/server/index.ts |
Root-level OIDC routes + updated registration | VERIFIED | GET /login, /callback, /logout at top-level before /api/* middleware |
src/client/routes/login.tsx |
Login page that redirects to /login (OIDC) | VERIFIED | No form, no inputs; button with window.location.href = "/login" |
src/client/hooks/useAuth.ts |
Auth hooks without useLogin, useSetup, useChangePassword | VERIFIED | Exports: useAuth, useLogout, useApiKeys, useCreateApiKey, useDeleteApiKey only |
e2e/seed.ts |
E2E seed without users table insert | VERIFIED | No schema.users reference; inserts API key into schema.apiKeys |
tests/middleware/auth.test.ts |
Middleware tests for three-way auth | VERIFIED | 8 tests covering API key, Bearer token, OIDC session paths with mocked getAuth |
tests/services/auth.service.test.ts |
Service tests for API key functions only | VERIFIED | 5 tests for createApiKey, verifyApiKey, listApiKeys, deleteApiKey only |
tests/routes/auth.test.ts |
Route tests for /me and /keys endpoints | VERIFIED | 8 tests covering GET /me with mocked OIDC, GET/POST/DELETE /keys with API key auth |
Key Link Verification
| From | To | Via | Status | Details |
|---|---|---|---|---|
src/server/middleware/auth.ts |
@hono/oidc-auth |
getAuth() for OIDC session check |
WIRED | Line 2: import { getAuth } from "@hono/oidc-auth", used at line 26 |
src/server/middleware/auth.ts |
src/server/services/auth.service.ts |
verifyApiKey for API key path |
WIRED | Line 3 import, used at line 12 |
src/server/routes/auth.ts |
@hono/oidc-auth |
getAuth for /me response |
WIRED | Line 2 import, used at lines 22-29 |
src/server/index.ts |
@hono/oidc-auth |
oidcAuthMiddleware, processOAuthCallback, revokeSession for OIDC routes |
WIRED | Lines 5-8 import; used at lines 44-49 |
src/server/routes/oauth.ts |
@hono/oidc-auth |
getAuth() replaces verifyPassword in authorize GET/POST |
WIRED | Line 1 import; used at lines 136 and 182 |
src/server/mcp/index.ts |
src/server/services/auth.service.ts |
verifyApiKey |
WIRED | Line 6 import; used in auth middleware block |
docker-compose.yml |
docker/init-logto-db.sql |
postgres volume mount to docker-entrypoint-initdb.d |
WIRED | Line 10: ./docker/init-logto-db.sql:/docker-entrypoint-initdb.d/init-logto-db.sql |
src/client/hooks/useAuth.ts |
/api/auth/me |
apiGet fetch |
WIRED | Line 12: apiGet<AuthState>("/api/auth/me") |
src/client/routes/login.tsx |
/login (OIDC server route) |
window.location.href redirect |
WIRED | Line 40: window.location.href = "/login" |
e2e/seed.ts |
apiKeys table |
direct insert | WIRED | Lines 209-211: db.insert(schema.apiKeys).values(...) |
Data-Flow Trace (Level 4)
Level 4 data-flow tracing is not applicable for this phase. The primary artifacts are authentication middleware, service functions, and configuration — not components that render dynamic data fetched from a database. The auth middleware routes requests, it doesn't render data. The login page renders a static redirect button.
Behavioral Spot-Checks
| Behavior | Command | Result | Status |
|---|---|---|---|
| auth.service exports only API key functions | Module export check | No createUser, verifyPassword, getUserCount, createSession anywhere in src/server/ |
PASS |
| Three-way auth middleware has all paths | Code inspection | getAuth, verifyApiKey, verifyAccessToken all present, no getUserCount |
PASS |
| OIDC routes registered before /api/* in index.ts | Code inspection | Lines 44-49 before lines 65+ | PASS |
| Login page has no form or inputs | Code inspection | No <form>, no <input> in login.tsx |
PASS |
| Build compiles cleanly | bun run build |
Built in 474ms, no TypeScript errors | PASS |
| Auth service tests | bun test tests/services/auth.service.test.ts |
5 pass, 0 fail | PASS |
| Auth middleware tests | bun test tests/middleware/auth.test.ts |
8 pass, 0 fail | PASS |
| Auth route tests | bun test tests/routes/auth.test.ts |
8 pass, 0 fail | PASS |
Requirements Coverage
| Requirement | Source Plan | Description | Status | Evidence |
|---|---|---|---|---|
| AUTH-01 | 15-02, 15-03 | User can register an account via external OIDC auth provider | SATISFIED | Logto handles registration; /login redirects to Logto via oidcAuthMiddleware(); login.tsx renders redirect button |
| AUTH-02 | 15-02, 15-03 | User can log in via external auth provider and access their data | SATISFIED | OIDC login/callback/logout flow implemented; getAuth(c) validates OIDC session in middleware and /me route |
| AUTH-03 | 15-02 | API keys remain functional for programmatic access (MCP, scripts) | SATISFIED | verifyApiKey is first check in requireAuth; MCP middleware checks API key; all four API key service functions preserved; 8/8 middleware tests pass including API key paths. Note: REQUIREMENTS.md still shows [ ] (unchecked) — documentation inconsistency, implementation is complete |
| AUTH-04 | 15-01 | Auth provider runs self-hosted alongside the application | SATISFIED | svhd/logto:latest in both docker-compose.yml and docker-compose.dev.yml with Postgres dependency and init script. Note: REQUIREMENTS.md still shows [ ] (unchecked) — documentation inconsistency, implementation is complete |
| AUTH-05 | 15-03 | E2E tests authenticate via API keys without depending on the auth provider | SATISFIED | e2e/seed.ts creates apiKeys record with static key; no schema.users reference; no Logto dependency in E2E auth path |
Note on REQUIREMENTS.md checkbox discrepancy: AUTH-03 and AUTH-04 are marked [ ] (pending) in REQUIREMENTS.md but their implementation is fully present. The plan summaries claim them complete. This is a documentation synchronization gap — the code satisfies the requirements.
Anti-Patterns Found
No anti-patterns found in phase 15 artifacts.
- No TODO/FIXME/placeholder comments in modified files
- No empty handlers or stub implementations
- No hardcoded empty data passed to renderers
- No references to removed functions (
getUserCount,createUser,verifyPassword,createSession,getSession,deleteSession,refreshSession) anywhere insrc/server/ - No references to removed hooks (
useLogin,useSetup(auth),useChangePassword) insrc/client/ - The deferred item noted in 15-03-SUMMARY ("tests/routes/oauth.test.ts still references createUser") was resolved — no such reference exists in current code
Human Verification Required
The following items require a running Logto instance and cannot be verified programmatically:
1. End-to-End OIDC Login Flow
Test: Start docker compose -f docker-compose.dev.yml up -d, configure a Logto application (Traditional Web), set OIDC_CLIENT_ID, OIDC_CLIENT_SECRET, OIDC_AUTH_SECRET in environment, start bun run dev, visit /login, click "Sign In".
Expected: Redirected to Logto login page; after completing Logto login/registration, redirected back to GearBox dashboard as authenticated user; GET /api/auth/me returns { authenticated: true, user: { id: "<logto-sub>", email: "..." } }.
Why human: Requires running Logto container, browser interaction, real OIDC token exchange; cannot be tested with grep or static analysis.
2. MCP OAuth Flow via OIDC Session
Test: With a valid OIDC session active in the browser, navigate to /oauth/authorize?response_type=code&client_id=...&redirect_uri=...&code_challenge=...&code_challenge_method=S256. Expected: See consent form with "Authorize" button (no username/password fields).
Expected: Consent form shown when OIDC session is present; redirect to login if no session.
Why human: Requires running Logto, active OIDC session cookie, and OAuth client registration.
3. Logto Admin Console Accessibility
Test: docker compose -f docker-compose.dev.yml up -d && curl -s http://localhost:3002 (or open in browser).
Expected: Logto admin console UI loads at port 3002.
Why human: Requires Docker environment and Logto container to be running; cannot verify port accessibility from static analysis.
Gaps Summary
No gaps. All automated verification checks pass. Phase goal is achieved from the perspective of code correctness, architecture, and test coverage. The only items requiring human attention are live integration tests with a running Logto instance, which were gated as a human checkpoint in Plan 03 Task 3.
One minor documentation note: REQUIREMENTS.md checkboxes for AUTH-03 and AUTH-04 remain unchecked despite their implementation being complete. This does not affect the verification status — the code satisfies the requirements.
Verified: 2026-04-04T19:30:00Z Verifier: Claude (gsd-verifier)