oidcdoctor
v0.1.0
Published
Agent-native CLI that wires OIDC login into your app AND proves it works against Keycloak. Scaffolds login code, reconciles your app config against the actual Keycloak client, then runs a REAL authorization-code + PKCE handshake over plain HTTP (no browse
Maintainers
Readme
oidcdoctor
Wire OIDC login into your app — and prove it actually works against Keycloak.
oidcdoctor is an agent-native CLI (and MCP server) that scaffolds your OIDC login, reconciles your app config against the real Keycloak client, then runs a real authorization-code + PKCE handshake over plain HTTP — no browser — to assert a token is issued. When it fails, it maps the exact error to a precise fix on both sides (app and Keycloak) and loops.
scaffold → analyze (reconcile) → verify (real handshake) → sync (fix Keycloak) → verify ✓Why
The #1 Keycloak pain is the redirect_uri mismatch — and its cousins: PKCE
required-but-missing, public-vs-confidential confusion, standard flow disabled,
missing web origins. These only fail at runtime, in a browser, with an
opaque error. AI agents are great at writing login code, but they can't tell if
it works — there's no feedback loop. oidcdoctor is that loop.
The moat is verification, not codegen. Anything can emit a Next.js auth route. oidcdoctor proves the route gets a token, and if it doesn't, tells you exactly which of the two sides to change.
The redirect_uri mismatch, concretely
Your app sends http://localhost:3000/api/auth/callback. Keycloak has
http://localhost:3000/api/auth/callback/ registered (trailing slash), or
https://… (scheme), or port 3001. Keycloak matches literally (apart from
a single trailing *), so login dies with invalid_redirect_uri. oidcdoctor's
reconciler pinpoints which component differs — scheme, host, port, path, or
trailing slash — and gives you the fix.
Install
npm install -g oidcdoctor # or: npx -y oidcdoctor <command>Then wire it into your agent:
oidcdoctor install # writes the MCP entry for Claude Code + CursorThis adds oidcdoctor to .mcp.json (Claude Code) and .cursor/mcp.json
(Cursor), launching npx -y oidcdoctor mcp. For Codex (TOML), the command
prints the snippet for ~/.codex/config.toml. Manual entry:
{
"mcpServers": {
"oidcdoctor": { "command": "npx", "args": ["-y", "oidcdoctor", "mcp"] }
}
}Quick start
oidcdoctor init # write a starter oidcdoctor.json
oidcdoctor scaffold --framework next --out . # generate login code
oidcdoctor sync # create/patch the Keycloak client to match
oidcdoctor verify # run the REAL handshake; assert a token is issued
oidcdoctor analyze # static reconcile, anytimeoidcdoctor.json
{
"framework": "next",
"intent": {
"issuer": "https://keycloak.example.com/realms/myrealm",
"clientId": "my-app",
"redirectUri": "http://localhost:3000/api/auth/callback",
"postLogoutRedirectUri": "http://localhost:3000/",
"clientType": "public",
"usePkce": true,
"pkceMethod": "S256",
"scopes": ["openid", "profile", "email"]
},
"keycloak": {
"auth": { "grant": "password", "username": "admin", "password": "admin", "clientId": "admin-cli" }
},
"testUser": { "username": "testuser", "password": "testpassword" }
}The Keycloak issuer is {baseUrl}/realms/{realm}; oidcdoctor derives the OIDC
endpoints (/protocol/openid-connect/{auth,token,logout}) and Admin REST base
(/admin/realms/{realm}/clients) from it.
How it works: the reconcile + verify loop
analyze(reconcile) — pure, fully unit-tested. Compares the app intent against the live Keycloak client and detects every classic failure class with acode, a human message, and a fix for both sides:| code | what it catches | |------|-----------------| |
redirect_uri_mismatch| URI not matched (exact + Keycloak*wildcard; flags scheme/host/port/trailing-slash) | |client_type_mismatch| app uses a secret but client is public (or vice-versa) | |pkce_required_missing| client requires PKCE, app doesn't send it | |pkce_method_mismatch| app's method differs from the required one | |standard_flow_disabled| auth-code flow turned off on the client | |web_origins_missing| public/browser client origin not in Web Origins (CORS) | |missing_openid_scope|openidabsent — no id_token, not OIDC | |post_logout_mismatch| post-logout URI not registered |verify(the capstone) — runs the real flow withfetch: builds the authorize URL, GETs the Keycloak login page, scrapes the form action, POSTs the test user's credentials (carrying cookies), catches the302toredirect_uri?code=…&state=…, assertsstatematches, exchanges the code at the token endpoint (withcode_verifier, andclient_secretfor confidential clients), and asserts a token came back. On any failure it maps the symptom to the sameFindingvocabulary as the reconciler — so "it didn't work" always ships with the precise fix.sync— creates the Keycloak client if missing, otherwise applies the minimal patch fromplanClientPatchto make it consistent. Re-runverifyto confirm.
The verification harness (the headline)
The reason to trust oidcdoctor is that its own test suite proves the loop —
hermetically, with no network, no Docker, no real Keycloak, and no browser.
An in-process fake Keycloak (test/helpers.ts, built on node:http) implements
the authorization, token, logout, and Admin REST endpoints, validates PKCE
(S256) and redirect_uri, and can be configured broken. The heal-loop test:
- stands up a broken client (redirect_uri unregistered + standard flow off),
verifyLoginfails with aredirect_uri_mismatchdiagnosis,- reads the live client, plans the patch, syncs it via the Admin API,
- re-runs
verifyLogin→tokenIssued: true.
That's the whole product, proven end to end in milliseconds.
Programmatic API
import { reconcile, planClientPatch, verifyLogin, KeycloakAdmin, scaffold } from "oidcdoctor";
const report = reconcile(intent, keycloakClient); // { ok, findings }
if (!report.ok) {
const patch = planClientPatch(intent, keycloakClient);
await new KeycloakAdmin(opts).syncClient(intent, patch);
}
const result = await verifyLogin(intent, { username, password }); // { ok, tokenIssued, step, diagnosis? }
const files = scaffold("next", intent); // GeneratedFile[]MCP tools
When run as an MCP server (oidcdoctor mcp), it exposes four agent-facing tools,
all of which nudge the agent to verify, not just generate:
analyze— reconcile intent vs. a described/live Keycloak client.verify_login— run the real auth-code + PKCE handshake.sync_client— create/patch the Keycloak client to match the app.scaffold— return generated login files for a framework.
Supported frameworks
- Next.js (App Router) — route handlers under
app/api/auth/..., auth-code- PKCE, callback that exchanges the code and sets a session cookie.
- FastAPI — Authlib (Starlette OAuth), login/callback/logout.
- Django — mozilla-django-oidc settings + URLs.
The core (reconcile + verify + sync) works for any stack — the scaffolders are a convenience; point oidcdoctor at any app's intent and it'll prove the login regardless of language.
Limitations
- Keycloak-first. The reconciler models Keycloak's client semantics exactly. Generic OIDC providers (Auth0, Okta, Entra, …) are on the roadmap.
- No browser flows. Verification drives the handshake over HTTP with
fetch; flows that require real JavaScript/WebAuthn in a browser are out of scope. - Password-grant test user.
verifysubmits the Keycloak login form with a username/password; social/IdP-brokered logins aren't covered yet.
License
MIT © 2026 Abdulmunim Jemal
