OAuth 2.1 authorization server: oidc-provider, storage adapter, consent #151
Labels
No labels
bug
duplicate
enhancement
help wanted
invalid
question
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
lz/agent-nexus#151
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
The authorization server half of MCP support. Nexus issues its own tokens by default so a fresh install stays one container and one compose file.
Depends on nothing but #148 (zod v4). Blocks the resource server and every tool.
Why a library
PKCE verification, single-use codes, exact redirect-URI matching, refresh rotation and audience binding is precisely the code that should not be novel.
oidc-provideris OpenID Certified and implements RFC 6749, 7636, 7591, 8414, 8707, 7662 and 7009. We supply configuration, a storage adapter and the consent UI — never a token, a code, or a comparison.Work
Mount.
oidc-provideris Koa-shaped and exposes a plain Node(req, res)callback. Mount at/oauthinnexus/src/server.ts, alongside the WebSocket upgrade listener.This is a surface that bypasses
hooks.server.ts, which AGENTS.md fact #23 treats as a known hazard. It is defensible and the reasons must stay written down: the protocol endpoints are meant to sit outside the cookie gate, and the one endpoint needing the operator's identity — consent — redirects back into an ordinary SvelteKit page that runs the normal gate. Nothing authenticated is decided in the bypassing half. Note this is the only such surface MCP adds;/api/mcpstays inside the gate.Storage. Migration
0021(next free —migration-numbers.test.tsguards collisions, after the 0017 collision that crash-looped production). Kysely adapter foroidc-provider.Tokens stored as unsalted SHA-256, for the reasons AGENTS.md fact #24 argues for
nxs_: a CSPRNG credential is unguessable at any hash speed, Argon2id would cost ~100 ms on every authenticated request, and a salted hash cannot be looked up by digest.Nothing here is sealed under the vault DEK — a client registration is not a secret Nexus keeps from itself, and the token store holds digests. So no
encrypted_*column, and nothing forupgradeLegacyVaultto cover (fact #28).sealed-columns.test.tsalready enforces this direction.Consent through interactions.
nextonly ever carries a short/oauth/consent/<uid>, never a long authorization query string, because oidc-provider already holds the request.The consent page must display the redirect URI hostname, and show an additional warning when the registered redirect URIs are loopback addresses — the MCP authorization spec requires the first and recommends the second, because a client metadata document cannot prove which local process holds a port. Mocks B and C are the same page; the hostname and warning are the only differences.
DCR ships certified in oidc-provider. Keep it disabled by default behind a setting, labelled deprecated by the MCP spec, with a cap on stored clients — enabling it opens an unauthenticated registration endpoint on a public host.
Watch for
The metadata must advertise
code_challenge_methods_supported: ["S256"]and"none"intoken_endpoint_auth_methods_supported. Claude requires both and silently drops to DCR when either is missing.Issuer path placement. With an issuer of
https://a.lck.sh/oauth, RFC 8414 §3.1 puts metadata at/.well-known/oauth-authorization-server/oauth— the path is inserted after the well-known segment, not appended to the issuer. The intuitive-and-wrong placement returns a correct-looking document and fails silently: the MCP server receives the first request and the authorization server sees no traffic at all. The SDK'soauthMetadataResponseis path-aware and should serve it (#152's sibling issue) rather than a hand-rolled route.Configuration surface is the risk here, not cryptography. Ship one grant type, one response type, two scopes (
nexus:read,nexus:write), and assert the emitted metadata rather than trusting the config.Done when
An authorization code flow with PKCE S256 completes end to end against a real client, a refresh rotates, a dead refresh returns
invalid_grant(notinvalid_request— Claude's refresh handling depends on the RFC 6749 code), and the live metadata document carries both required fields.