feat(security): migrate FuzeKeys onto the FuzeFront Security API - #64
Open
github-actions[bot] wants to merge 1 commit into
Open
feat(security): migrate FuzeKeys onto the FuzeFront Security API#64github-actions[bot] wants to merge 1 commit into
github-actions[bot] wants to merge 1 commit into
Conversation
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this does
Migrates FuzeKeys off self-owned authentication and onto the FuzeFront Security API (
@fuzefront/security-clientcontract). 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.iofiles are product domain, not auth.backend/app/integrations/site/permit_io/automates account signup, sign-in and API-key creation onpermit.ioas a third-party target site — sitting right next toapp/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:
users.hashed_passwordapp/utils/encryption.py,app/models/user.pySECRET_KEYapp/routers/auth.py(create_access_token)POST /auth/login+POST /auth/registerapp/routers/auth.py/auth/logouta no-op — a stolen token stayed valid until expiryapp/routers/auth.pyfrontend/src/pages/Login.tsxregistration/policy.jsonroles enforced nowhereThe repo's own
.semgrep/fuze-authz.ymlalready 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 overGET/DELETE /v1/security/session,POST /v1/security/authz/check,/bulk-check,GET /authz/permissions. Every path taken verbatim frompackages/security/openapi.yaml; no endpoint invented.get_current_userkeeps 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 localusersrow — now a projection (the integer id every FK points at), not a credential store.identities,accounts,automation,site_integrationsviarequire_permission(), using the bare resource/action keys fromregistration/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./loginand/registerdeleted, along withcreate_access_tokenandSECRET_KEY./logoutdelegates toDELETE /v1/security/session, so it now actually revokes.users.hashed_passworddropped,users.fuzefront_user_idadded (alembica6c3d1e45f23). 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_passwordmade 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 ownVaultGateUI. 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.SessionResultis narrowed onstatusso an MFA-enabled account cannot masquerade as a successful login with anundefinedtoken.Login/Registerare redirects, not forms. FuzeKeys collects no credentials at all.AuthContextrewritten around the FuzeFront session;AuthProvideris now actually mounted inAppandMfeApp— 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)
@fuzefront/identity-uias "the sign-in/sign-up redirect surface", but its actual exports are member / invite / API-token management UI (IdentityPage,MembersTable,InviteModal,TokenList).account-security-uiis a post-login security hub;portal-branding-uihasWhiteLabelLoginCard, 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".@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 insecurityClient.tsandapp/security/contract.pyare therefore hand-mirrors of the published shapes, marked so they can be swapped for the real import in one commit.requiredVersion: '^18.0.0'; every@fuzefront/*package peers on^19.0.0and the FuzeFront host shares^19.0.0. Per FuzeFront's ownCLAUDE.mdthis 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.packages/security/package.jsonsays0.2.0whileSECURITY_CONTRACT_VERSIONinsrc/types.tssays'0.3.0'. Cosmetic, but a consumer told to assert on the major has two answers.Remaining
permit.io/ Authentik hits — each justifiedbackend/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.ymlpermit.ioas an automation target site. A header comment now says so explicitly so the next reader doesn't "fix" it..claude/agents/fuzefront-expert.md.claude/agents/contract-designer.md, variouspermitted/permitsCLAUDE.mdZero
authentikreferences exist in FuzeKeys code, config, or CI — and none were added.De-vendored along the way:
.semgrep/fuze-authz.ymlmessages,.claude/agents/backend-engineer.md("real authz stays in Permit" → the FuzeFront authz check),nightly-integration.ymlmock matrix,registration/README.md, and the now-false "JWT-based authentication with bcrypt password hashing" claim inREADME.md.Verification
pytest tests/diffof the failure sets is empty.tsc --noEmitSitesDatabase.test.tsxandvite.config.ts, zero newnpm run build:mfehelm template deploy/helm/fuzekeysFUZEFRONT_SECURITY_*presentgrepinventoryNot verified — stated plainly
GET /v1/security/sessionandPOST /authz/checkwere tested against anhttpx.MockTransportstub, not a real service. The wire shapes are asserted against the contract; the integration itself is unproven end-to-end.batch_alter_tableand compiles, but only the SQLite-backed test path exercised the new model.DROP COLUMN hashed_passwordis destructive and irreversible — review it before it reaches an environment with real rows.Login/Register/VaultGatesurfaces.