fix(mfa): return the spec's auth-factor enrollment envelope - #114
Merged
Merged
Conversation
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
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.
Summary
Fixes #110. Verified against production (
userland-user-authentication-factors.controller.ts), the published OpenAPI spec, and all 11 backend SDKs.POST /user_management/users/:id/auth_factorsreturned a bare factor where the spec'sUserlandUserAuthenticationFactorEnrollResponsewraps it as{ authentication_factor, authentication_challenge }. The Node SDK threw aTypeError; Python, Kotlin, Android, Rust, Swift and PHP refused to deserialize; Go, Ruby, .NET and Elixir silently returnednilfor both halves. No SDK could drive TOTP enrollment through the emulator.totp.secret,totp.qr_codeandtotp.uri, and the same SDKs fail one level down without them. Enrollment now mints a 32-character Base32 secret (honoring a caller-suppliedtotp_secret, validated as production does,422 invalid_totp_secretotherwise) and is the only response that shows it — GET and LIST return the spec's secretlessAuthenticationFactor, as production does. Also fixestotp_userbeing ignored.authentication_challengeon the wire (challenge routes, the password grant'smfa_challengestep, legacy MFA) carried the store'sfactor_id/user_idwhere the spec hasauthentication_factor_idand no user. Seven SDKs hard-fail on this. Changed at the single formatter every route already goes through; the store keeps its columns.text/plainon 204. Not the handler's doing —@hono/node-server1.x stamped a text/plain default on every response without one, empty body included, so all 27 no-content routes were affected. 2.x skips the default for a null body; bumping fixes them all with no adapter workaround. (Onlyserveis used; the only 2.x breaking change is dropping Node 18, andenginesalready requires ≥22.)Notes
qr_codeis a valid 1×1 PNG rather than a scannable code: the field is required and typed as a string, the emulator never verifies a real TOTP code, andurialready carries everything the code would. Marked with aponytail:comment naming the upgrade path.response-envelopes.spec.tsalready hits each cataloged route live and diffs the body against the OpenAPI spec, but no MFA operation or resource was in the curated catalogs — a coverage gap, not a design one. The last commit adds the six MFA operations with a JSON body and the two resources to the catalogs, regenerates them, and adds the live cases. Run againstmainwithout the fix, those cases fail six times, each naming one of the drifts above. The loops diff top-level keys only, so the nestedtotp.secret/qr_code/urirequirement is pinned by the newauth-factors.spec.tsinstead.