Skip to content

fix: Fix Session.refresh() KeyError by sealing session client-side#673

Open
devin-ai-integration[bot] wants to merge 1 commit into
mainfrom
devin/1781735372-fix-session-refresh-sealing
Open

fix: Fix Session.refresh() KeyError by sealing session client-side#673
devin-ai-integration[bot] wants to merge 1 commit into
mainfrom
devin/1781735372-fix-session-refresh-sealing

Conversation

@devin-ai-integration

Copy link
Copy Markdown
Contributor

Description

Session.refresh() (and AsyncSession.refresh()) always failed with KeyError('sealed_session'), which was swallowed by the blanket except Exception and surfaced as reason="'sealed_session'" — not a valid AuthenticateWithSessionCookieFailureReason.

Root cause: The refresh method sent a session: {seal_session: True, cookie_password: ...} parameter to the /user_management/authenticate API, expecting a sealed_session field in the response. The API does not support server-side session sealing and never returns this field. The hard key access auth_response["sealed_session"] raised KeyError.

Fix: Seal the session client-side after receiving the API response, using the existing seal_session_from_auth_response() helper. This matches the approach in the Node SDK, which strips the session param before sending the request and seals locally via sealSessionDataFromAuthenticationResponse().

Changes:

  • Remove the session body param from both sync and async refresh() (the API ignores it)
  • After a successful refresh, call seal_session_from_auth_response(access_token, refresh_token, user, impersonator, cookie_password) to produce the sealed cookie
  • Add tests for the refresh success path (sync + async), impersonator handling, and a regression test verifying session is not sent to the API

Fixes #666

Documentation

[ ] Yes

No docs changes needed — this is a bug fix to match the documented behavior of Session.refresh().

Link to Devin session: https://app.devin.ai/sessions/1f32a32e77304ede864acc4f6790bc29

The refresh method was sending a session/seal_session parameter to the
/user_management/authenticate API endpoint expecting it to return a
sealed_session field. The API does not support server-side sealing and
never includes sealed_session in the response, causing a KeyError that
was swallowed by the except block and surfaced as a bogus failure reason.

Seal the session client-side after receiving the API response using the
existing seal_session_from_auth_response helper, matching the approach
used by the Node SDK.

Fixes #666
@devin-ai-integration devin-ai-integration Bot requested a review from a team as a code owner June 17, 2026 22:29
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author
Original prompt from garen.torikian

please deal with fixing this area of the SDK code: #666

@devin-ai-integration devin-ai-integration Bot requested a review from a team as a code owner June 17, 2026 22:29
@devin-ai-integration devin-ai-integration Bot requested a review from mattgd June 17, 2026 22:29
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@devin-ai-integration devin-ai-integration Bot changed the title Fix Session.refresh() KeyError by sealing session client-side fix: Fix Session.refresh() KeyError by sealing session client-side Jun 17, 2026
@greptile-apps

greptile-apps Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a long-standing bug where Session.refresh() and AsyncSession.refresh() always raised KeyError('sealed_session') because they sent an unsupported session: {seal_session: True} body param to the API and then tried to read a sealed_session field that was never returned. The fix removes that body param and calls the existing seal_session_from_auth_response() helper client-side after receiving the API response, matching the approach already used in the Node SDK.

  • src/workos/session.py: Strips the session body param from both sync and async refresh() and replaces the hard auth_response["sealed_session"] key access with a local call to seal_session_from_auth_response(); the sealed cookie is assigned to both self.session_data and the returned RefreshWithSessionCookieSuccessResponse.sealed_session.
  • tests/test_session.py: Adds sync and async success tests, an impersonator test, and a regression test that asserts the session param is never sent to the API.

Confidence Score: 4/5

The fix is correct and the core logic matches the Node SDK reference implementation; the refresh path is now functional and well-tested.

The bug fix is straightforward and the new tests cover the happy path, impersonator case, and the regression scenario. The only open question is that both Session.refresh() and AsyncSession.refresh() decode the new access token with verify_aud=False and no iss check — a pattern also present in authenticate(), but now newly activated in the refresh path.

The JWT decode options in src/workos/session.py (lines 368–374 and 551–557) are worth a second look before shipping.

Important Files Changed

Filename Overview
src/workos/session.py Core fix: removes the unsupported server-side session body param and seals the cookie client-side via seal_session_from_auth_response() after receiving the API response — applied consistently to both Session.refresh() and AsyncSession.refresh().
tests/test_session.py Adds four new tests for the refresh success path (sync + async), impersonator handling, and a regression test asserting the session param is not sent to the API; test structure is correct and keys align with production code.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant C as Caller
    participant S as Session.refresh()
    participant API as WorkOS API
    participant Fernet as Fernet (local)

    C->>S: refresh(organization_id?, cookie_password?)
    S->>Fernet: unseal_data(session_data, cookie_password)
    Fernet-->>S: "{refresh_token, user, ...}"
    S->>API: POST /user_management/authenticate
    API-->>S: "{access_token, refresh_token, user, impersonator?}"
    S->>Fernet: seal_session_from_auth_response(...)
    Fernet-->>S: new_sealed
    S->>S: "self.session_data = new_sealed"
    S->>S: "jwt.decode(access_token, signing_key, verify_aud=False)"
    S-->>C: RefreshWithSessionCookieSuccessResponse
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant C as Caller
    participant S as Session.refresh()
    participant API as WorkOS API
    participant Fernet as Fernet (local)

    C->>S: refresh(organization_id?, cookie_password?)
    S->>Fernet: unseal_data(session_data, cookie_password)
    Fernet-->>S: "{refresh_token, user, ...}"
    S->>API: POST /user_management/authenticate
    API-->>S: "{access_token, refresh_token, user, impersonator?}"
    S->>Fernet: seal_session_from_auth_response(...)
    Fernet-->>S: new_sealed
    S->>S: "self.session_data = new_sealed"
    S->>S: "jwt.decode(access_token, signing_key, verify_aud=False)"
    S-->>C: RefreshWithSessionCookieSuccessResponse
Loading

Comments Outside Diff (2)

  1. src/workos/session.py, line 368-374 (link)

    P2 JWT iss and aud claims not validated on refresh

    verify_aud is explicitly disabled and there is no iss check. With this PR, Session.refresh() is now a fully functional code path for the first time, so a token returned by a rogue or misconfigured endpoint would be accepted as valid — the signature alone does not authenticate the token's intended audience or issuer. The same pattern exists in authenticate(), but it was pre-existing there; the refresh path is newly activated here.

    Rule Used: JWTs should always be validated before use and the... (source)

  2. src/workos/session.py, line 548-557 (link)

    P2 JWT iss and aud claims not validated on async refresh

    Same concern as the sync path above: verify_aud is disabled and iss is not checked in AsyncSession.refresh(). Since both refresh paths are now reachable for the first time, both inherit this gap.

    Rule Used: JWTs should always be validated before use and the... (source)

Reviews (1): Last reviewed commit: "Fix Session.refresh() KeyError by sealin..." | Re-trigger Greptile

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.

Session.refresh() raises KeyError('sealed_session') and reports reason "'sealed_session'"

0 participants