Skip to content

fix(mfa): remove three MFA routes the API does not have - #115

Merged
gjtorikian merged 6 commits into
mainfrom
fix/remove-invented-mfa-routes
Sep 15, 2026
Merged

gjtorikian merged 6 commits into
mainfrom
fix/remove-invented-mfa-routes

Conversation

@gjtorikian

Copy link
Copy Markdown
Collaborator

Summary

Stacked on #114 (GitHub retargets to main when it merges; the two touch the same files).

  • Removes POST /user_management/auth_factors/:id/challenges, POST /user_management/auth_challenges/:id/verify and DELETE /user_management/auth_factors/:id. They date from the emulator's first commit and exist in none of: the OpenAPI spec (checked at the pinned 0.80.0 and the newest published 0.95.0), the production controllers (the only challenge controller is auth/challenges; the only factor delete is DELETE /auth/factors/:id), or any of the twelve backend SDKs (Node, Python, Go, .NET, Kotlin, Android, PHP, Ruby, Elixir, Rust, Swift; php-laravel wraps php).
  • WorkOS challenges and verifies factors through POST /auth/factors/:id/challengePOST /auth/challenges/:id/verify (or the mfa-totp grant on /user_management/authenticate), and deletes them through DELETE /auth/factors/:id. The emulator already serves all of those with the same logic, so the removed handlers were a duplicate copy of it behind paths no client calls — auth-challenges.ts is deleted outright.
  • No coverage lost: auth-challenges.spec.ts (correct-code success, wrong code, expired challenge, challenge wire shape, 404) is repointed at the real routes, and the two e2e.spec.ts cleanups now use DELETE /auth/factors/:id. SUPPORTED.md is unchanged — the invented routes never counted toward spec coverage.

Notes

  • Not marked breaking: the routes were never in SUPPORTED.md, the README, the spec or an SDK, so the only possible caller is a hand-written request against a guessed path. Happy to retitle with ! if you'd rather bump major.

Every no-content reply — deleting a user, a factor, an organization —
went out as `Content-Type: text/plain; charset=UTF-8`, which
production never sends. The header was not ours: @hono/node-server 1.x
stamped a text/plain default onto any response lacking one, empty
body included. 2.x skips the default when the body is null, so the
bump fixes all 27 routes at once with no adapter-level workaround.

Refs #110
Every authentication_challenge on the wire — from the challenge
routes, the password grant's mfa_challenge step and the legacy MFA
API — carried the store's join columns, `factor_id` and `user_id`,
where the spec's AuthenticationChallenge has `authentication_factor_id`
and no user. Every generated SDK reads the spec's name: Kotlin, Rust,
Swift, Python and PHP refuse to deserialize without it, and Go, Ruby,
.NET and Elixir hand back an empty factor id. The store keeps its
columns; only the formatter, where every route already meets, changes.

Refs #110
`POST /user_management/users/:id/auth_factors` answered with a bare
factor, where the spec's UserlandUserAuthenticationFactorEnrollResponse
wraps it as `{ authentication_factor, authentication_challenge }`.
Every backend SDK reads that envelope: Node throws a TypeError before
returning, Python, Kotlin, Rust, Swift and PHP refuse to deserialize,
and Go, Ruby, .NET and Elixir hand back nil for both halves — so no
SDK could drive TOTP enrollment through the emulator.

The wrapper alone would not have been enough. The spec's enrolled
factor requires `totp.secret`, `totp.qr_code` and `totp.uri`, and the
same SDKs fail one level down without them; the emulator stored only a
hex secret buried in the URI. Enrollment now mints a Base32 secret
(honoring a caller-supplied `totp_secret`, validated as production
does) and is the only response that shows it — GET and LIST return
the spec's secretless AuthenticationFactor, as production does.

The `qr_code` is a valid 1x1 PNG rather than a scannable code: the
field is required and typed as a string, the emulator never verifies
a real TOTP code, and `uri` already carries everything the code would.

Fixes #110
Issue #110 shipped in several releases because neither conformance
loop looked at MFA: the enrollment envelope, the factor and the
challenge were all outside the curated catalogs, so the bare factor
and the challenge's `factor_id` never met the spec. Both loops already
do exactly the check that was needed — the gap was coverage, not
design. The six spec operations with a JSON body and the two resources
join the catalogs; run against main without the fix, the new cases
fail six times, each naming one of the drifts.

The loops diff top-level keys, so they cannot see the enrolled
factor's `totp.secret`, `qr_code` and `uri`; the route tests added
with the fix pin those.

Refs #110
`RegExp.test` coerces its argument, and the digits 2–7 are Base32, so
a JSON number like 234567 passed validation and was stored and
returned as a number — a `totp.secret` the strictly typed SDKs would
refuse to deserialize.

Refs #110
`POST /user_management/auth_factors/:id/challenges`,
`POST /user_management/auth_challenges/:id/verify` and
`DELETE /user_management/auth_factors/:id` date from the first commit
and exist in neither the OpenAPI spec (checked through 0.95.0), the
production controllers, nor any of the twelve backend SDKs. WorkOS
challenges and verifies factors through `/auth/factors/:id/challenge`
and `/auth/challenges/:id/verify`, and deletes them through
`DELETE /auth/factors/:id` — all of which the emulator already serves
with the same logic, so the removed handlers were a second copy of it
behind paths no client calls. Their tests move to the real routes;
the coverage they gave (correct code, expired challenge, wire shape)
is kept.
@greptile-apps

greptile-apps Bot commented Sep 15, 2026

Copy link
Copy Markdown

RetriggerConfidence Score: 5/5

The PR appears safe to merge because the retained canonical routes provide equivalent behavior for every removed route exercised by the repository.

Summary

This PR removes three unsupported duplicate MFA routes and redirects existing tests and end-to-end cleanup to the retained canonical WorkOS routes.

  • Unregisters and deletes the duplicate user-management challenge handlers.
  • Removes the duplicate user-management authentication-factor deletion handler.
  • Repoints challenge coverage and MFA cleanup to /auth/factors and /auth/challenges.
  • Preserves the tested status codes, response envelopes, validation, expiration, and deletion behavior.

Reviews (1) · Last reviewed commit: "fix(mfa): remove three MFA routes the AP..."

Base automatically changed from fix/mfa-auth-factor-envelope to main September 15, 2026 15:11
@gjtorikian
gjtorikian merged commit a780aa0 into main Sep 15, 2026
10 checks passed
@gjtorikian
gjtorikian deleted the fix/remove-invented-mfa-routes branch September 15, 2026 15:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant