Repository navigation
feat(core): adopter-token (JWT) authentication - #678
seanspeaks wants to merge 2 commits into
Conversation
Implement GetUserFromAdopterJwt with jose: JWKS (remote, cached once per process) or HMAC key, iss/aud checks, exp/nbf with tolerance, algorithm allow-list, and stable 401 reason codes. Find or create the individual and organization users; link only when the individual has no organization, and refuse a second organization with 403 multi_org_unsupported (ADR-056 §3.6). loadAppDefinition now fails at startup when authModes.adopterJwt is on without a usable jwtConfig. The schema's jwtConfig gains jwksUri, issuer, audience, algorithms, clockToleranceSec and roleClaim, and requires one of secret or jwksUri. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HyrF4oqcTdK2AtvcrocxE6
… stricter jwtConfig Classify verification failures by inversion: only known token failures are 401; anything else (refused connection, DNS failure, non-200 JWKS, generic JOSE error) is 503 jwks_unavailable. Validate jwksUri at load: https, or http only for loopback. Require audience with jwksUri and warn when issuer is unset. Extract AdopterTokenVerifier (config resolved once, JWKS held per verifier; module-global cache removed), a frozen error table with one fail() helper, assertSingleOrganization, and a find-or-create helper shared with GetUserFromXFriggHeaders. Add setOrgRole, UserConfig/AdopterJwtConfig types, schema oneOf and dependencies, and consolidate the tests under user/tests/adopter-jwt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HyrF4oqcTdK2AtvcrocxE6
There was a problem hiding this comment.
If your organization's extra usage balance is empty, an organization admin can add extra usage credits at claude.ai/admin-settings/usage. If its monthly spend limit was reached, an admin can raise it on the same page. If neither applies, contact Anthropic support.
Once extra usage is available, someone with write access to this repository can comment @claude review on this pull request to trigger a review.
|
|
ADR reconciliation needed after Stack A/B — drafted with Claude Code at Sean's request Severity: must-reconcile (text only; the code looks consistent with the intent of ADR-056 §2).
Merge order: #671 → this PR. #644's ADR-034 edit is independent but should land before #643's code. |
Frigg 2.0.1 review (Stack A) — COMMENT onlyVerdict (would): changes needed Reviewed against the adopter-JWT checklist (signature, issuer, audience, expiry, algorithm allow-list; no BlockingNone that are clear auth bypasses on their own — gaps below are checklist / production foot-guns. Should-fix
Nits
No formal approve / request-changes on this review. |
| function assertAdopterJwtConfig(userConfig) { | ||
| if (userConfig?.authModes?.adopterJwt !== true) return; | ||
| const config = resolveAdopterJwtConfig(userConfig.jwtConfig); | ||
| if (config.jwksUri && !config.issuer) { |
There was a problem hiding this comment.
should-fix — require issuer when jwksUri is set
Checklist: verify signature, issuer, audience, expiry, algorithm allow-list. Audience is already required for JWKS; issuer is only warned here, and adopter-token-verifier passes issuer: undefined through to jose (claim not checked).
Scenario: App enables adopterJwt with jwksUri + audience: "frigg-api" and no issuer. IdP also mints tokens for another API that share this JWKS and include the same aud → those tokens authenticate as Frigg users.
Please throw from resolveAdopterJwtConfig / assertAdopterJwtConfig when jwksUri is set and issuer is missing (same posture as audience).
| } | ||
| let { individual: individualUserData, organization: organizationUserData } = | ||
| await findAppUsers(this.userRepository, this.userConfig, { | ||
| appUserId, |
There was a problem hiding this comment.
should-fix — organizationUser vs organizationId (unchanged lines just below)
This refactor correctly centralizes find/create, but the mismatch check a few lines down still reads individualUserData.organizationUser?.toString(). Persisted individuals expose organizationId (see JWT hasOrganization / assertSingleOrganization on this branch).
Scenario: strictUserValidation: true + correctly linked (appUserId, appOrgId) → always looks mismatched → 400. Default auto-link never short-circuits and re-enters linkIndividualToOrganization every time.
Please read organizationId here (same as the adopter-JWT path). Later memberships work may rewrite this file; on this branch the field name is still wrong.



Summary
This implements the adopter-token auth mode from ADR-056 §2, which until now was a stub that returned 501. Frigg can now verify tokens issued by an adopter's own identity provider directly, so a caller needs no shared secret and no relay. The first consumer is Left Hook's internal MCP server: admin.lefthook.com issues ES256 tokens, and frigg-internal verifies them against admin's JWKS. See lefthook.com ADR-0016 D6 and the build plan in lefthook.com PR #758.
This PR is stacked on #671 (
feat/management-api-v2). Canaries are published from this branch for development use until it merges down.What it does
user/adopter-token-verifier.jsis a new adapter that owns the config, the JWKS and the mapping from failures to reason codes. It is built once per router or dependency set, not once per request.jwksUri, and HS256 withsecret. Thejku/jwkheaders are never followed.expis required.issandaudare checked, andclockToleranceis bounded.jwks_unavailable, so a key-server outage does not log users out.GetUserFromAdopterJwttakes the user fromuserIdClaim(defaultsub) and the active org fromorgIdClaim, and exposes the role throughUser#getOrgRole().multi_org_unsupported. This check runs before anything is created. It is interim untilOrganizationMembershiplands in F4 (ADR-056 §3.6).adopter-jwt-config.js):secretorjwksUrimay be set.jwksUrimust be https; http is allowed only to loopback.audienceis required whenjwksUriis set, and a warning is logged whenissueris unset.oneOf, adependenciesrule and anotrule.user/app-user-helpers.jsholds the find-or-create andbuildUserlogic that both this path andGetUserFromXFriggHeadersuse. Each keeps its own linking policy.types/user/index.d.tsaddsUserConfig,AdopterJwtConfigandUser#getOrgRole/setOrgRole.jose@^5.10.0.Known, not fixed here
GetUserFromXFriggHeaderscomparesindividual.organizationUser, but the repositories returnorganizationId, so its mismatch check is always true. That will be tracked separately.Test plan
packages/corerun, with the no-op global setup because Mongo binaries can't run in CI-less containers: 15 suites and 63 tests fail. The failing tests match base by name, minus the 2 from the deleted stub suite. 2931 pass.packages/schemas: 154 pass.alg: none, HS256 signed with the public key, forgedjkuandjwkheaders, issuer mismatch.🤖 Generated with Claude Code
https://claude.ai/code/session_01HyrF4oqcTdK2AtvcrocxE6
Generated by Claude Code
📦 Published PR as canary version:
2.0.0--canary.678.58db4b2.0✨ Test out this PR locally via:
npm install @friggframework/admin-scripts@2.0.0--canary.678.58db4b2.0 npm install @friggframework/core@2.0.0--canary.678.58db4b2.0 npm install @friggframework/devtools@2.0.0--canary.678.58db4b2.0 npm install @friggframework/eslint-config@2.0.0--canary.678.58db4b2.0 npm install @friggframework/prettier-config@2.0.0--canary.678.58db4b2.0 npm install @friggframework/schemas@2.0.0--canary.678.58db4b2.0 npm install @friggframework/serverless-plugin@2.0.0--canary.678.58db4b2.0 npm install @friggframework/test@2.0.0--canary.678.58db4b2.0 npm install @friggframework/ui@2.0.0--canary.678.58db4b2.0 # or yarn add @friggframework/admin-scripts@2.0.0--canary.678.58db4b2.0 yarn add @friggframework/core@2.0.0--canary.678.58db4b2.0 yarn add @friggframework/devtools@2.0.0--canary.678.58db4b2.0 yarn add @friggframework/eslint-config@2.0.0--canary.678.58db4b2.0 yarn add @friggframework/prettier-config@2.0.0--canary.678.58db4b2.0 yarn add @friggframework/schemas@2.0.0--canary.678.58db4b2.0 yarn add @friggframework/serverless-plugin@2.0.0--canary.678.58db4b2.0 yarn add @friggframework/test@2.0.0--canary.678.58db4b2.0 yarn add @friggframework/ui@2.0.0--canary.678.58db4b2.0