Skip to content

MFA session should be resumable without a cookie (backend SDKs) #784

Description

@lakhansamani

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 onlycookie.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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions