Skip to content

fix: persist the mfa session cookie outside the browser - #52

Open
lakhansamani wants to merge 1 commit into
mainfrom
fix/node-mfa-session-cookie
Open

fix: persist the mfa session cookie outside the browser#52
lakhansamani wants to merge 1 commit into
mainfrom
fix/node-mfa-session-cookie

Conversation

@lakhansamani

Copy link
Copy Markdown
Contributor

The bug

authorizer-js has the same class of bug the Go SDK had: no cookie persistence outside the browser.

Every call goes through graphqlQuery / restQuery, both of which pass credentials: 'include'. That is a browser-only mechanism — node's fetch has no cookie store, so every Set-Cookie the server returns is silently dropped.

Since server 2.4.0 MFA is on by default. signup/login withhold the access token, return "Proceed to mfa setup" with should_show_totp_screen: true, and identify the pending user by an mfa_session cookie. skipMfaSetup, verifyOtp and the webauthn MFA-setup path resolve that offer only if the cookie comes back.

So on node every one of them failed with invalid session, while the methods existed and read as correct.

User-visible impact

Any server-side (node) integration — SSR, a BFF, CLI tooling, tests — could sign a user up or log them in and then had no way to finish authentication. The token was withheld and the only method that releases it always failed. The whole MFA surface was unreachable from node.

Browsers were unaffected (the browser owns the cookie).

Reproduced against a real 2.4.0+ server on localhost:8280, both protocols:

=== protocol=graphql ===
signup data: {"message":"Proceed to mfa setup","access_token":null,"should_show_totp_screen":true,...}
skipMfaSetup errors: [ 'Error: invalid session' ]
RESULT: FAIL - no access_token from skipMfaSetup

=== protocol=rest ===
skipMfaSetup errors: [ 'Error: invalid session' ]
RESULT: FAIL - no access_token from skipMfaSetup

After the fix, same script, signup -> MFA offer -> skipMfaSetup -> getProfile:

=== protocol=graphql ===
signup errors: []            # message: "Proceed to mfa setup"
skipMfaSetup errors: []      # access_token issued
getProfile errors: []        # correct user
login errors: [] has token: true
RESULT: PASS

=== protocol=rest ===
... RESULT: PASS

SUMMARY graphql=true rest=true

The fix

One choke point, fetchWithCookies, used by both graphqlQuery and restQuery. In a browser it is a plain fetch (the browser owns cookies and Cookie is a forbidden request header anyway); elsewhere it replays the stored MFA session and records what the response sets. Caller-supplied headers still win.

Scoped to the mfa_session* cookies on purpose, not a general cookie jar. The server resolves a request's identity from the cookie session before the Authorization header (GetUserIDFromSessionOrAccessToken), so a general jar would make a stored login session silently override a bearer token the caller passed explicitly. That is not theoretical — a first pass at this fix used a full jar and broke three existing integration tests, including one where getProfile over REST returned a different user id than over GraphQL. The MFA session is bound to one user id server-side and consumed on use, so it cannot be traded for another user's token.

Cookies are stored per Authorizer instance (same model as the Go SDK's jar), and dropped when the server expires them (Max-Age<=0).

Tests

  • Existing suite: 101 passed before, 102 passed after (10 suites, no regressions).
  • New regression test in __test__/mfaMethods.test.ts: asserts the mfa_session cookie set by signup is replayed on the follow-up skipMfaSetup, that the login session cookie is not replayed, and that an expired cookie is dropped rather than resent.

`credentials: 'include'` is browser-only, so node dropped every
Set-Cookie the server returned. Since server 2.4.0 MFA is on by
default: signup/login withhold the access token, return "Proceed to
mfa setup", and identify the pending user by an mfa_session cookie.
skipMfaSetup / verifyOtp / the webauthn MFA-setup path resolve it only
if that cookie comes back, so all of them failed with "invalid
session" — the entire MFA surface was unreachable from node while the
methods existed and read as correct.

Store the mfa_session cookies per instance and replay them on the
graphql/rest choke point. Scoped to those cookies rather than a
general jar on purpose: the server resolves identity from the `cookie`
session before the Authorization header, so replaying a login session
would silently override a caller-supplied bearer token.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant