Skip to content

feat(security): migrate FuzeKeys onto the FuzeFront Security API - #64

Open
github-actions[bot] wants to merge 1 commit into
masterfrom
claude/fuze-security-migration
Open

feat(security): migrate FuzeKeys onto the FuzeFront Security API#64
github-actions[bot] wants to merge 1 commit into
masterfrom
claude/fuze-security-migration

Conversation

@github-actions

@github-actions github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

What this does

Migrates FuzeKeys off self-owned authentication and onto the FuzeFront Security API (@fuzefront/security-client contract). FuzeKeys now holds no session signing key, stores no user password, and evaluates no authorization policy. Which identity provider or policy engine sits behind that API is FuzeFront's private implementation detail and is not named, reached, or configurable from this repo.

The finding that reframes the task

FuzeKeys never referenced Authentik at all, and its ~10 permit.io files are product domain, not auth.

backend/app/integrations/site/permit_io/ automates account signup, sign-in and API-key creation on permit.io as a third-party target site — sitting right next to app/integrations/google/, which does the same for Google. FuzeKeys is a credential-vault product; driving browser automation against SaaS sites is what it does. Those references are a capability. Deleting them would have removed a feature and fixed nothing.

The real coupling was the opposite of what the file inventory suggested — FuzeKeys was authenticating users itself:

Before Where
bcrypt password store on users.hashed_password app/utils/encryption.py, app/models/user.py
locally-minted HS256 JWT signed with SECRET_KEY app/routers/auth.py (create_access_token)
POST /auth/login + POST /auth/register app/routers/auth.py
/auth/logout a no-op — a stolen token stayed valid until expiry app/routers/auth.py
hand-rolled email + password + master-key sign-in form frontend/src/pages/Login.tsx
zero authorization — registration/policy.json roles enforced nowhere

The repo's own .semgrep/fuze-authz.yml already flagged both patterns (fuze-auth-self-minted-user-token, fuze-auth-local-password-store). Nothing was gating on it.

What changed

Backend

  • app/security/{contract,client,dependencies}.py — fail-closed client over GET/DELETE /v1/security/session, POST /v1/security/authz/check, /bulk-check, GET /authz/permissions. Every path taken verbatim from packages/security/openapi.yaml; no endpoint invented.
  • get_current_user keeps its name and return type, so all ~15 feature routers are untouched. Underneath it resolves the caller via FuzeFront and maps the subject onto the local users row — now a projection (the integer id every FK points at), not a credential store.
  • Authorization wired on for identities, accounts, automation, site_integrations via require_permission(), using the bare resource/action keys from registration/policy.json (Identity:read, SignupScript:run, ApiKey:create, …). This runs in addition to the existing per-row ownership filters — role and ownership answer different questions.
  • /login and /register deleted, along with create_access_token and SECRET_KEY. /logout delegates to DELETE /v1/security/session, so it now actually revokes.
  • users.hashed_password dropped, users.fuzefront_user_id added (alembic a6c3d1e45f23). A pre-migration row is adopted by email on its owner's first FuzeFront-authenticated request, so existing vaults survive the cutover.
  • hash_password / verify_password made private — they existed only to authenticate users locally, and a public password hasher in a repo that must not authenticate is a loaded gun.

The master key is preserved, not dropped

It used to be a third field on the login form, which conflated prove who you are (FuzeFront's job) with decrypt my vault (FuzeKeys' job, and the reason the product exists). It is a domain secret, never an auth factor. It now has explicitly authenticated endpoints (/api/v1/auth/vault/{setup,unlock}) and its own VaultGate UI. Without it the vault stays locked exactly as before — same guarantee, correct seam.

Frontend

  • services/securityClient.ts — typed client on the same-origin base (/v1/security/*), never an absolute host. SessionResult is narrowed on status so an MFA-enabled account cannot masquerade as a successful login with an undefined token.
  • Login / Register are redirects, not forms. FuzeKeys collects no credentials at all.
  • AuthContext rewritten around the FuzeFront session; AuthProvider is now actually mounted in App and MfeApp — previously the entire auth layer was dead code that nothing rendered.

Everything fails closed

Unreachable service, unparseable body, absent tenantId, or a bulk response whose length doesn't match the request → deny. A bulk length mismatch denies everything rather than index-aligning decisions onto the wrong resources. 33 new tests cover exactly these paths.

Contract gaps found (raised, not worked around)

  1. No published FuzeFront package exports a sign-in or sign-up component. The task named @fuzefront/identity-ui as "the sign-in/sign-up redirect surface", but its actual exports are member / invite / API-token management UI (IdentityPage, MembersTable, InviteModal, TokenList). account-security-ui is a post-login security hub; portal-branding-ui has WhiteLabelLoginCard, but it is FuzeFront-portal-internal and not exported for consumers. Login/Register therefore redirect to FuzeFront rather than hand-roll a form — the closest available approximation of "don't hand-roll an auth screen".
  2. The @fuzefront/* packages are not installable here. npm view @fuzefront/identity-ui --registry=https://npm.pkg.github.com → 404; the registry is restricted and this build authenticates against it with no credentials. The contract types in securityClient.ts and app/security/contract.py are therefore hand-mirrors of the published shapes, marked so they can be swapped for the real import in one commit.
  3. React major mismatch, pre-existing and unrelated to this PR but load-bearing. FuzeKeys is React 18 with MF requiredVersion: '^18.0.0'; every @fuzefront/* package peers on ^19.0.0 and the FuzeFront host shares ^19.0.0. Per FuzeFront's own CLAUDE.md this mismatch does not fail CI — it surfaces as "Invalid hook call" / a white screen at runtime. Adding any @fuzefront/* dependency is blocked until FuzeKeys moves to React 19. That is a separate stream and deliberately not attempted here.
  4. packages/security/package.json says 0.2.0 while SECURITY_CONTRACT_VERSION in src/types.ts says '0.3.0'. Cosmetic, but a consumer told to assert on the major has two answers.

Remaining permit.io / Authentik hits — each justified

Location Verdict
backend/app/integrations/site/permit_io/**, routers/site_integrations.py, tests/test_site_integrations.py, demo_site_integrations.py, scripts/populate_site_methods.py, README.md, SITE*_*.md, ci.yml Domain — stays. permit.io as an automation target site. A header comment now says so explicitly so the next reader doesn't "fix" it.
.claude/agents/fuzefront-expert.md Stays, flagged. A vendored copy of FuzeFront's agent file describing FuzeFront's backend. Editing it here would make it wrong about FuzeFront and desync it from its source. Candidate for FuzeFront to de-vendor.
.claude/agents/contract-designer.md, various permitted / permits English. False positives.
CLAUDE.md New section explaining the above classification.

Zero authentik references exist in FuzeKeys code, config, or CI — and none were added.

De-vendored along the way: .semgrep/fuze-authz.yml messages, .claude/agents/backend-engineer.md ("real authz stays in Permit" → the FuzeFront authz check), nightly-integration.yml mock matrix, registration/README.md, and the now-false "JWT-based authentication with bcrypt password hashing" claim in README.md.

Verification

Check Result
pytest tests/ 156 passed (was 123) — the 59 failures + 15 errors are byte-identical to the pre-change baseline. diff of the failure sets is empty.
New security tests 33 passed
tsc --noEmit Identical to baseline — 4 pre-existing errors in SitesDatabase.test.tsx and vite.config.ts, zero new
npm run build:mfe ✅ builds, 3419 modules
helm template deploy/helm/fuzekeys ✅ renders, FUZEFRONT_SECURITY_* present
Final grep inventory ✅ above

Not verified — stated plainly

  • No live login was exercised. There is no running FuzeFront in this environment, so GET /v1/security/session and POST /authz/check were tested against an httpx.MockTransport stub, not a real service. The wire shapes are asserted against the contract; the integration itself is unproven end-to-end.
  • The alembic migration was not run against Postgres. It is written with batch_alter_table and compiles, but only the SQLite-backed test path exercised the new model. DROP COLUMN hashed_password is destructive and irreversible — review it before it reaches an environment with real rows.
  • No browser run, so the mandated console-clean UI gate has not been satisfied for the new Login / Register / VaultGate surfaces.
  • The 59 pre-existing test failures were not investigated or fixed — out of scope, and left exactly as found so the regression comparison stays honest.
  • The React 18 → 19 upgrade (gap 3) is untouched.

FuzeKeys authenticated its own users: a local bcrypt password store, a
locally-minted HS256 JWT signed with SECRET_KEY, and a hand-rolled login
form that also collected the vault master key. It evaluated no
authorization at all — the roles and resources declared in
registration/policy.json were never enforced anywhere.

All authentication and authorization now go to the FuzeFront Security API
(@fuzefront/security-client contract). Which identity provider or policy
engine sits behind that API is FuzeFront's private implementation detail
and is not named, reached, or configurable from this repo.

Backend
- app/security/{contract,client,dependencies}.py — a fail-closed client
  over GET/DELETE /v1/security/session, POST /v1/security/authz/check,
  /bulk-check and GET /authz/permissions. No endpoint is invented; every
  path is taken verbatim from packages/security/openapi.yaml.
- get_current_user keeps its name and return type, so all ~15 feature
  routers are unchanged. Underneath, it resolves the caller from FuzeFront
  and maps the subject onto the local users row — which is now a
  PROJECTION (the integer id every FK points at), not a credential store.
- Authorization wired onto identities, accounts, automation and site
  integrations via require_permission(), using the BARE resource/action
  keys from registration/policy.json (Identity:read, SignupScript:run,
  ApiKey:create, ...). Never an engine-specific policy identifier.
- routers/auth.py: /login and /register deleted along with
  create_access_token and SECRET_KEY. /logout now delegates to
  DELETE /v1/security/session, so it actually revokes instead of asking
  the client to forget a token that stays valid until expiry.
- users.hashed_password DROPPED; users.fuzefront_user_id added (alembic
  a6c3d1e45f23). A pre-migration row is adopted by email on its owner's
  first FuzeFront-authenticated request, so existing vaults survive.
- encryption.hash_password/verify_password made private: they existed only
  to authenticate users locally, and a public password hasher in a repo
  that must not authenticate is a loaded gun.

The vault master key is preserved, not dropped. It is a DOMAIN secret that
decrypts the vault, never a login factor — it only looked like one because
the login form collected it. It now has its own explicitly authenticated
endpoints (/api/v1/auth/vault/{setup,unlock}) and its own UI gate. Without
it the vault stays locked exactly as before.

Frontend
- services/securityClient.ts — typed client for the contract on the
  SAME-ORIGIN base (/v1/security/*), covering session, exchange, signup,
  methods, email-available, social start, reset-request and authz.
  SessionResult is narrowed on `status` so an MFA-enabled account cannot
  masquerade as a successful login with an undefined token.
- Login/Register are now REDIRECTS to FuzeFront, not forms. FuzeKeys
  collects no credentials at all.
- AuthContext rewritten around the FuzeFront session; VaultGate carries
  the master key separately.
- AuthProvider is now actually mounted in App and MfeApp — previously the
  whole auth layer was dead code that nothing rendered.

Everything fails closed: unreachable service, unparseable body, absent
tenant, or a bulk response whose length does not match the request all
deny. 33 new tests cover exactly those paths.

NOT a coupling, deliberately kept: backend/app/integrations/site/permit_io
automates account signup on permit.io as a THIRD-PARTY TARGET SITE, like
google.com next to it. That is FuzeKeys' product domain — deleting it would
remove a capability, not a dependency.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GaPa3JgrVNtWrGvqQEAEqv
@izzywdev
izzywdev marked this pull request as ready for review August 2, 2026 17:08
@izzywdev
izzywdev self-requested a review as a code owner August 2, 2026 17:08
@izzywdev izzywdev added the auto-merge label Aug 2, 2026 — with Claude
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant