feat(core): API-key login auth mode (module-validated) - #643
Conversation
✅ Deploy Preview for friggframework-org canceled.
|
|
| GitGuardian id | GitGuardian status | Secret | Commit | Filename | |
|---|---|---|---|---|---|
| 36570312 | Triggered | Username Password | 45b1fbc | packages/core/logs/logger.test.js | View secret |
🛠 Guidelines to remediate hardcoded secrets
- Understand the implications of revoking this secret by investigating where it is used in your code.
- Replace and store your secret safely. Learn here the best practices.
- Revoke and rotate this secret.
- If possible, rewrite git history. Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data.
To avoid such incidents in the future consider
- following these best practices for managing and storing secrets including API keys and other credentials
- install secret detection on pre-commit to catch secret before it leaves your machine and ease remediation.
🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.
Add a first-class, opt-in `apiKey` auth mode (ADR-034): let a browser end
user log in with their own product API key, validated through the api-module
itself, and land in an authenticated, tenant-scoped Frigg session — removing
the mandatory BFF/token-broker for "log in with your product key" apps.
The existing `POST /user/login` becomes polymorphic and dispatches on the
credential shape against the app's enabled authModes: `{ username, password }`
keeps the unchanged friggToken path; `{ apiKey }` runs the new mode. Both may
be enabled at once (disjoint bodies).
LoginWithApiKey use case: validate + identify via the module's Requester
(testAuthRequest, then getEntityDetails), derive a provider-authoritative
identity (never from client input), find-or-create the user via the existing
x-frigg-headers path, create the Credential + Entity by reusing
ProcessAuthorizationCallback({ api_key }), then mint an ordinary short-lived
app-user session token.
Security invariants (ADR-034 normative): provider-authoritative identity only;
ordinary app-user session (never admin); per-IP + global rate limiting with a
key-length cap and generic errors; key persisted only as the encrypted
Credential (never logged/returned/in a JWT); 401 (bad key) vs 503 (provider
outage) split, with no session/cookie side effects on outage; httpOnly +
secure (non-local) + SameSite cookie with an Origin/Referer allowlist.
Default-off and additive: apps that do not declare authModes.apiKey are
unchanged. Config is validated against registered modules at wiring time.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JDh45c1vm91ySYtvVv9Z65
Address confirmed security findings on the api-key login auth mode: - logging: redact credential-bearing request-body/header fields (apiKey, api_key, password, token, authorization, refresh_token, access_token) in initDebugLog before the Lambda event is buffered, so a raw key/password cannot leak via the 5xx debug dump or DEBUG_VERBOSE=1 (ADR-034 req #4). JSON bodies parsed+masked; non-JSON/oversized handled gracefully; logger never throws. - rate-limit: derive the per-IP bucket from a trusted X-Forwarded-For position (rightmost by default, or authModes.apiKey.rateLimit. trustedProxyDepth hops from the right) instead of the client-controlled leftmost hop, so a spoofed XFF can no longer mint a fresh bucket. - identity: namespace the find-or-create identity as `${moduleName}:${externalId}` so two modules in a multi-module allowlist returning the same externalId map to distinct Frigg tenants. - validation: reject a bare-string allowedOrigins, non-positive/NaN rateLimit knobs, and align the resolver's allowlist (union of modules + module) with the wiring-time validator. - login gate: require a strict boolean-true testAuthRequest pass, reject a non-scalar externalId instead of coercing it, and assert a credential was persisted before minting a session. - cookie: align frigg_session Max-Age/Expires to the session token TTL. - csrf: keep the Origin allowlist opt-in but warn once at wiring time when apiKey mode is enabled without allowedOrigins. Adds unit + handler-level tests for each finding (mutation-verified) and updates ADR-034 accordingly. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JDh45c1vm91ySYtvVv9Z65
35eaec0 to
45b1fbc
Compare
Reviewer context: stacked PR + post-review hardeningThis PR is stacked on #640 (base branch is A four-lens adversarial security pass (identity/impersonation, auth-bypass/token-scope, provider-validation fail-open, secret-handling/no-regression) ran against the original implementation. Net verdict: no auth bypass, no token-scope escalation, the password (
Each fix has a mutation-verified test. ADR-034 updated for the One behavior note for consumers: the module-namespaced identity means an already-live single-module instance would re-key existing users on first login after this change (invisible for a fresh deploy). Frigg CI is independently red on Generated by Claude Code |
|
ADR numbering — FYI (see #644)Your number does not change — The only ask: #644 moves every unmerged ADR onto Separately, note this PR is based on |



Summary
Implements ADR-034: API-key login auth mode (module-validated). Frigg apps that ship a browser SPA can now let an end user log in with their own product API key and land in an authenticated, tenant-scoped session — validated through the api-module itself — removing the mandatory BFF/token-broker that "log in with your product key" apps otherwise need.
Default-off and additive: apps that don't declare
user.authModes.apiKeyare completely unchanged, and the mode composes with the existingfriggToken/sharedSecretmodes.The auth mode & flow
POST /user/loginbecomes polymorphic, dispatching on the credential shape in the body against the app's enabledauthModes(the bodies are disjoint, so dispatch is unambiguous):{ username, password }→ friggToken — existing behavior, unchanged.{ apiKey }(optionally{ module, apiKey }for multi-identity apps) → the new apiKey mode.The
LoginWithApiKeyuse case:testAuthRequest(validity) thengetEntityDetails(identity). The api-module — not a bespoke validator — is the source of truth.appOrgId/appUserIdcome only from the provider'sgetEntityDetailsresponse, never from client input. A login whose module returns no stable identifier is rejected. The key is never hashed into an identity.GetUserFromXFriggHeaderspath. The principal is an ordinary app user, never admin.ProcessAuthorizationCallback(userId, module, { api_key })— the same path/api/authorizeuses.Security invariants (ADR-034 normative)
5xx/timeout →503with no session created and no cookie cleared; only a definitive401/403→ generic invalid-credentials (401). The 401-vs-503 split is mutation-tested.httpOnly,secure(non-local stages),SameSite=Strict, plus an optional Origin/Referer allowlist (CSRF) on the cookie-bearing route.authModes.apiKey.module/modulesmust name registered modules; validated fail-fast at wiring time (no-op when unset).Reuse (no reinvention)
ProcessAuthorizationCallback— credential/entity creation, called with{ api_key }.GetUserFromXFriggHeaders— find-or-create user from the provider-derived identity.CreateTokenForUserId— session-token minting.Files changed
docs/architecture-decisions/034-api-key-login-auth-mode.md— the ADR.packages/core/user/use-cases/login-with-api-key.js— newLoginWithApiKeyuse case (+classifyProviderError).packages/core/user/use-cases/validate-api-key-auth-mode.js— app-definition config validation.packages/core/handlers/rate-limiter.js— dependency-free per-IP + global fixed-window limiter.packages/core/handlers/routers/user-router.js— side-effect-freebuildUserRouterfactory (polymorphic/user/login, cookie/CSRF/rate-limit).packages/core/handlers/routers/user.js— thinned to production wiring that calls the factory.packages/core/index.js— exportsLoginWithApiKey,validateApiKeyAuthMode.Tests (38 new, all green when run directly)
login-with-api-key.test.js(19) — valid key → user+credential+entity created and token returned (spy + realProcessAuthorizationCallbackover in-memory doubles); impersonation guard (hostile client org id ignored; mutation check); invalid key → generic 401, no session; provider outage → 503, no session; no stable identifier → rejected; length cap; module allowlist; session is an ordinary app-user token (resolves back to the same tenant, no admin).validate-api-key-auth-mode.test.js(6) — default-off no-op; valid single/allowlist; missing/unregistered module throws.rate-limiter.test.js(4) — per-key trip, key independence, global trip, window reset.user-router.test.js(9, supertest) —{ apiKey }dispatch + hardened cookie; allowlisted{ module }; disabled-mode generic 401; 503 passthrough with no cookie; rate-limit → 429; CSRF origin allow/deny; password path unchanged with both modes enabled; missing-field 400.Config + route contract:
POST /user/loginbody{ apiKey }→201 { token }+Set-Cookie: frigg_session=…; HttpOnly; SameSite=Strict[; Secure].Notes for review
next(aprisma:generatestep and a MongoMemoryServer global-setup that needslibcrypto.so.1.1, unrelated to this feature). The 38 new tests are pure unit/route tests with mocked module Requester and repositories — run directly they pass with no DB/provider. Two pre-existing failures inget-user-from-adopter-jwt.test.jsandget-user-from-x-frigg-headers.test.jsare unrelated (stub-message/validation drift; those files are untouched).validateAndIdentifystep is factored so a future refresh re-validates the stored key viatestAuthRequest.🤖 Generated with Claude Code
Generated by Claude Code
📦 Published PR as canary version:
2.0.0--canary.643.45b1fbc.0✨ Test out this PR locally via:
npm install @friggframework/admin-scripts@2.0.0--canary.643.45b1fbc.0 npm install @friggframework/core@2.0.0--canary.643.45b1fbc.0 npm install @friggframework/devtools@2.0.0--canary.643.45b1fbc.0 npm install @friggframework/eslint-config@2.0.0--canary.643.45b1fbc.0 npm install @friggframework/prettier-config@2.0.0--canary.643.45b1fbc.0 npm install @friggframework/schemas@2.0.0--canary.643.45b1fbc.0 npm install @friggframework/serverless-plugin@2.0.0--canary.643.45b1fbc.0 npm install @friggframework/test@2.0.0--canary.643.45b1fbc.0 npm install @friggframework/ui@2.0.0--canary.643.45b1fbc.0 # or yarn add @friggframework/admin-scripts@2.0.0--canary.643.45b1fbc.0 yarn add @friggframework/core@2.0.0--canary.643.45b1fbc.0 yarn add @friggframework/devtools@2.0.0--canary.643.45b1fbc.0 yarn add @friggframework/eslint-config@2.0.0--canary.643.45b1fbc.0 yarn add @friggframework/prettier-config@2.0.0--canary.643.45b1fbc.0 yarn add @friggframework/schemas@2.0.0--canary.643.45b1fbc.0 yarn add @friggframework/serverless-plugin@2.0.0--canary.643.45b1fbc.0 yarn add @friggframework/test@2.0.0--canary.643.45b1fbc.0 yarn add @friggframework/ui@2.0.0--canary.643.45b1fbc.0