Raised while reviewing authorizerdev/authorizer-go#27 and authorizerdev/authorizer-py#11, both of which add cookie jars so the MFA session survives between SDK calls.
The problem
On a default-config server (no --enforce-mfa), a plain signup already withholds the token — verified live:
access_token issued : False
should_show_totp : True
message : Proceed to mfa setup
Recovering it requires skip_mfa_setup or verify_otp, and both read the session from a cookie only — cookie.GetMfaSession(gc) at verify_otp.go:58 and skip_mfa_setup.go:28. There is no header or body alternative; SkipMfaSetupRequest.state is for resuming the OAuth code flow, not for carrying the session.
A cookie is a browser mechanism. Both backend SDKs ship signup, login, verify_otp and skip_mfa_setup, so today those methods cannot complete without an HTTP cookie jar — which is what both PRs add, and which is a workaround for a browser-shaped API rather than a fix.
What the rest of the industry does
Neither major competitor uses a cookie for this on the API surface:
| Provider |
Mechanism |
| Auth0 |
mfa_token returned in the response body, passed as a parameter to /mfa/challenge and the token endpoint |
| Okta |
stateToken / stateHandle in the response body, sent in the request body to verify |
Both are opaque, short-lived, single-use handles — the same thing our mfa_session already is, just carried where an API client can reach it.
Proposal
Return the MFA session handle in the auth response alongside should_show_totp_screen, and accept it as an optional field on VerifyOTPRequest / SkipMfaSetupRequest / the *_mfa_setup inputs.
- Cookie stays the default — the login UI is unaffected, and nothing about the browser flow changes.
- Body parameter is additive — the server prefers the cookie when present, so this is not a breaking change.
- Same store, same TTL, same single-use semantics; only the transport differs.
- The SDKs could then drop the cookie-jar workaround entirely.
Why it is worth doing beyond tidiness
The Python jar needed a deliberate loopback shim, because http.cookiejar predates Secure Contexts and drops a Secure cookie on http://localhost. Getting that shim right took two attempts and one self-inflicted cookie-injection hole (a remote origin could plant Domain=localhost). Every future SDK — PHP, Java, Rust — inherits that same trap. A body parameter has none of it.
Raised while reviewing authorizerdev/authorizer-go#27 and authorizerdev/authorizer-py#11, both of which add cookie jars so the MFA session survives between SDK calls.
The problem
On a default-config server (no
--enforce-mfa), a plain signup already withholds the token — verified live:Recovering it requires
skip_mfa_setuporverify_otp, and both read the session from a cookie only —cookie.GetMfaSession(gc)atverify_otp.go:58andskip_mfa_setup.go:28. There is no header or body alternative;SkipMfaSetupRequest.stateis for resuming the OAuth code flow, not for carrying the session.A cookie is a browser mechanism. Both backend SDKs ship
signup,login,verify_otpandskip_mfa_setup, so today those methods cannot complete without an HTTP cookie jar — which is what both PRs add, and which is a workaround for a browser-shaped API rather than a fix.What the rest of the industry does
Neither major competitor uses a cookie for this on the API surface:
mfa_tokenreturned in the response body, passed as a parameter to/mfa/challengeand the token endpointstateToken/stateHandlein the response body, sent in the request body to verifyBoth are opaque, short-lived, single-use handles — the same thing our
mfa_sessionalready is, just carried where an API client can reach it.Proposal
Return the MFA session handle in the auth response alongside
should_show_totp_screen, and accept it as an optional field onVerifyOTPRequest/SkipMfaSetupRequest/ the*_mfa_setupinputs.Why it is worth doing beyond tidiness
The Python jar needed a deliberate loopback shim, because
http.cookiejarpredates Secure Contexts and drops aSecurecookie onhttp://localhost. Getting that shim right took two attempts and one self-inflicted cookie-injection hole (a remote origin could plantDomain=localhost). Every future SDK — PHP, Java, Rust — inherits that same trap. A body parameter has none of it.