Skip to content

feat(authn): provider-agnostic Security AuthN backend (/api/v1/security) - #253

Merged
izzywdev merged 7 commits into
masterfrom
claude/authn-backend
Jul 15, 2026
Merged

feat(authn): provider-agnostic Security AuthN backend (/api/v1/security)#253
izzywdev merged 7 commits into
masterfrom
claude/authn-backend

Conversation

@izzywdev

Copy link
Copy Markdown
Owner

AuthN backend slice — provider-agnostic Security API

Implements the AuthN surface under `/api/v1/security` against the frozen contract (PR #243: `packages/security/openapi.yaml` + `@fuzefront/security-client`), purely through the neutral `IdentityProvider` interface (env-driven factory). Authentik is named only inside the concrete provider.

Included

  • `AuthentikIdentityProvider` absorbing `oidc.ts` (keeps `id_token_signed_response_alg: 'HS256'`), `authentikPassword.ts`, and a self-contained `machine-identity` (M2M provision/introspect) copied into the security package so it compiles within its `rootDir`.
  • Routes: session CRUD + exchange, server-brokered social start/callback (302 to same-host `/api/auth/idp/` — never `auth.fuzefront.com`, opaque code back to the app, never a token in the URL), signup, `/methods`, `/mfa/` (factors, activate, recovery codes, challenge/verify), `/verify/*` (email + phone), M2M `/tokens` + introspect (fail-closed).
  • Unit tests (`security-routes.test.ts` + provider tests) asserting status codes, the `SessionResult` discriminator, the `{ items }` envelope, bearer enforcement, and fail-closed mapping.

Typecheck fixes from prior WIP

  • machine-identity no longer crosses tsconfig `rootDir` (local copy on native fetch).
  • security tsconfig `types: ["node"]` (a backend service should not pull React ambient types; also sidesteps a corrupted hoisted `@types/react`).
  • Removed unused `appBaseUrl`/`socialCallbackPath`/`isProduction`.

Scope

AuthN backend only. AuthZ (`/authz/`, `/tenants/`) is a separate later stream. UI, independent acceptance tests, and deploy wiring are NOT in this PR.

🤖 Generated with Claude Code

fuzeone-bot Bot and others added 5 commits July 14, 2026 13:42
Co-Authored-By: Claude claude-opus-4-8 <noreply@anthropic.com>
Claude-Session-Id: cf830721-b1ef-4fe0-a024-035ad280dcf7
…s, TOTP, migration [skip ci]

Co-Authored-By: Claude claude-opus-4-8 <noreply@anthropic.com>
Claude-Session-Id: cf830721-b1ef-4fe0-a024-035ad280dcf7
… [skip ci]

Co-Authored-By: Claude claude-opus-4-8 <noreply@anthropic.com>
Claude-Session-Id: cf830721-b1ef-4fe0-a024-035ad280dcf7
…d vars [skip ci]

Co-Authored-By: Claude claude-opus-4-8 <noreply@anthropic.com>
Claude-Session-Id: cf830721-b1ef-4fe0-a024-035ad280dcf7
…pt @types/react) [skip ci]

Co-Authored-By: Claude claude-opus-4-8 <noreply@anthropic.com>
Claude-Session-Id: cf830721-b1ef-4fe0-a024-035ad280dcf7
@izzywdev izzywdev added the auto-merge Enable squash auto-merge once CI passes label Jul 14, 2026
@github-actions
github-actions Bot enabled auto-merge (squash) July 15, 2026 06:22
Comment on lines +174 to +176
const token = jwt.sign({ userId: user.id, sessionId }, jwtSecret(), {
expiresIn: '24h',
})
if (existing) throw new ConflictError()

const id = uuidv4()
const passwordHash = await bcrypt.hash(input.password, 10)
expect(session.token).toBeTruthy()
expect(session.user.id).toBe('u1')
expect(db.__tables.sessions.length).toBe(1)
const decoded = jwt.verify(session.token, 'test-secret') as any
})
// Self-heal provisioning in the background — never blocks/fails the response.
runInternalProvision(user.id).catch(err =>
console.error(`Login self-heal provisioning failed for ${user.id}:`, err)
@github-actions

Copy link
Copy Markdown
Contributor

Automated code review (gate-code-review)

Reviewing for runtime-correctness bugs only.

  • backend/security/src/providers/authentik/AuthentikIdentityProvider.ts:141socialStates, pendingCodes, and mfaChallenges are per-process in-memory Maps, but the social-start route (routes/security.ts) sets a state cookie commented "so the callback is replica-agnostic." brokerCallback/exchangeCode/verifyMfa validate only against the in-process map, never the cookie — so any start→callback (or login→MFA-verify) that lands on a different replica, or survives a restart, throws UnauthorizedError and auth fails non-deterministically.
  • backend/security/src/providers/authentik/AuthentikIdentityProvider.ts:658confirmPhoneVerification(phone, code) runs db('users').where({ phone }).update({ phone_verified: true }) with no user/session scoping; it flips phone_verified on every user row sharing that phone number (and requires no bearer token at the route), verifying the wrong account.
  • backend/security/src/providers/authentik/AuthentikIdentityProvider.ts:628confirmEmailVerification matches code_hash globally (else q.andWhere({ code_hash: sha256(code) })) with no email/user scoping and no orderBy; a 6-digit code can select another user's pending row, marking the wrong row.user_id's email_verified true.
  • backend/security/src/providers/authentik/AuthentikIdentityProvider.ts:601confirmEmailVerification picks an arbitrary .first() among unconsumed rows for a given token/code hash with no expiry filter in the query; if multiple pending verifications exist (resends), it can non-deterministically return an expired row and reject a still-valid one.

Note: the in-memory-map issue is the highest-impact and will manifest in any multi-replica deploy (the chart in this repo runs the backend with replicas).

Report-only — this check never blocks merge.

@github-actions

Copy link
Copy Markdown
Contributor

CI fix pushed — branch claude-auto-fix-ci-claude/authn-backend-29394246721

Root cause: backend/security/tests/tsconfig.json was missing "types": ["node", "jest"]. The jest.config.js in backend/security uses ts-jest with tsconfig: 'tests/tsconfig.json', but without jest types, TypeScript couldn't resolve the global it/describe/expect identifiers, producing TS2593/TS2304 errors for the entire api-token test suite.

Fix: Added "types": ["node", "jest"] to backend/security/tests/tsconfig.json — same pattern as backend/tests/tsconfig.json.

The fix is committed and pushed to claude-auto-fix-ci-claude/authn-backend-29394246721. A PR could not be auto-created (Actions cannot create PRs in this repo), but the branch is ready to review and merge into claude/authn-backend.

#253 set the src tsconfig `types: [node]` (to expose URL/fetch globals), which
tests/tsconfig.json inherits — dropping @types/jest so the security jest suites
lost jest/describe/expect. Override types to [node, jest] in the TEST tsconfig
only, leaving the src build node-only.

Co-Authored-By: Claude claude-opus-4-8 <noreply@anthropic.com>
Claude-Session-Id: cf830721-b1ef-4fe0-a024-035ad280dcf7
@github-actions

Copy link
Copy Markdown
Contributor

Automated code review (gate-code-review)

Reviewing the diff for runtime-correctness bugs only.

  • backend/security/src/providers/authentik/AuthentikIdentityProvider.ts:96-98socialStates, pendingCodes, and mfaChallenges are per-process in-memory Maps, but this is a multi-replica k8s service (trust proxy is set). startSocialLogin/brokerCallback/exchangeCode/verifyMfa will intermittently fail with UnauthorizedError whenever the start and callback/exchange requests land on different replicas. The sec_social_state cookie is set in routes/security.ts (/social/:provider/start) with the comment "so the callback is replica-agnostic," but brokerCallback never reads the cookie — it looks the state up in the in-memory map, so the replica-agnostic guarantee is false and the cookie is dead code.

  • backend/security/src/providers/authentik/AuthentikIdentityProvider.ts:315-320introspectToken returns { active: true } for any validly-signed session JWT purely from verifySessionToken, never checking the sessions table. After logout deletes the session row, the same token still introspects as active until JWT expiry, so revocation is not honored on the introspection path (unlike getUserInfo).

  • backend/security/src/providers/authentik/AuthentikIdentityProvider.ts:449-453confirmPhoneVerification(phone, code) runs db('users').where({ phone }).update({ phone_verified: true }), scoped only by phone number, with no authenticated user context. It marks every user row sharing that phone as verified, and the route (/verify/phone/confirm) requires no bearer token — a caller who knows a valid OTP verifies phones on accounts that aren't theirs.

  • backend/security/src/providers/authentik/AuthentikIdentityProvider.ts:456-464getVerificationStatus decodes the token with verifySessionToken instead of getUserInfo, so it skips the sessions-table existence/expiry check and returns status for tokens whose session has already been revoked via logout.

  • backend/security/src/providers/authentik/AuthentikIdentityProvider.ts:189-192startSocialLogin's open-redirect guard rejects ^https?:// and //-prefixed targets but not backslash-prefixed paths (e.g. /\evil.com or \/\/evil.com), which browsers normalize to protocol-relative navigations, so redirectTo can still escape same-origin at the final res.redirect in /social/callback.

Report-only — this check never blocks merge.

@izzywdev
izzywdev merged commit 9cabc6f into master Jul 15, 2026
49 checks passed
@izzywdev
izzywdev deleted the claude/authn-backend branch July 15, 2026 06:44
izzywdev added a commit that referenced this pull request Jul 15, 2026
* fix(authn): route Authentik native paths under app host — restore OIDC (prod outage)

#247's reverse-proxy stripped /api/auth/idp and relied on X-Forwarded-Prefix,
but Authentik IGNORES it — so discovery advertised app.fuzefront.com/application/o/authorize/
(no prefix), an unrouted path → the browser authorize redirect 404'd → prod login broke.

Fix: route Authentik's native root paths (/application,/if,/source,/flows,/ws,/-,
/outpost.goauthentik.io,/static/dist,/static/authentik) straight to authentik-server
under app.fuzefront.com (no strip); issuer -> https://app.fuzefront.com/application/o/fuzefront/.
Boundary intact: browser stays on app.fuzefront.com, never sees the IdP host.
Chart-only — deploys via Argo, no image build. helm template renders clean.

Co-Authored-By: Claude claude-opus-4-8 <noreply@anthropic.com>
Claude-Session-Id: cf830721-b1ef-4fe0-a024-035ad280dcf7

* ci(release): add workflow_dispatch — enable manual image builds

Auto-merged PRs push to master under GITHUB_TOKEN, which GitHub does not let
trigger push-workflows — so release.yml never ran after #247/#253/#256 and the
security-service/frontend images never built. workflow_dispatch gives a reliable
manual build+GitOps-bump path (durable fix: switch auto-merge to a PAT — needs a
RELEASE_PAT secret, tracked separately).

Co-Authored-By: Claude claude-opus-4-8 <noreply@anthropic.com>
Claude-Session-Id: cf830721-b1ef-4fe0-a024-035ad280dcf7

---------

Co-authored-by: fuzeone-bot[bot] <fuzeone-bot[bot]@users.noreply.github.com>
Co-authored-by: Claude claude-opus-4-8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

auto-merge Enable squash auto-merge once CI passes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants