fix: seal session client-side in Session#refresh#470
Conversation
Session#refresh was sending a `session` param to the API expecting it to return a `sealed_session`, but the API does not populate that field for refresh-token grants. This left `sealed_session` as an empty string. Seal the refreshed tokens client-side via `SessionManager#seal_session_from_auth_response`, consistent with how every other v7 auth flow handles sealing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Greptile SummaryThis PR fixes a broken Confidence Score: 4/5Safe to merge — the core bug fix is correct and well-tested; remaining findings are P2 pre-existing concerns. No P0 or P1 issues in the changed code. The fix is minimal, targeted, and consistent with how other auth flows work in v7. The two P2 observations (state mutation order before decode_jwt, and iss/aud JWT claim validation) are pre-existing in the codebase and not introduced by this PR. Test coverage for the new behaviour is thorough. lib/workos/session_manager.rb — decode_jwt skips iss/aud validation (pre-existing, not changed in this PR)
|
| Filename | Overview |
|---|---|
| lib/workos/session.rb | Removes server-side sealing (which was broken — the API never populated sealed_session) and replaces it with client-side sealing via SessionManager#seal_session_from_auth_response, consistent with all other v7 auth flows. Minor: state is mutated before decode_jwt, so a nil/malformed API access_token yields an unhandled exception with partial state; and decode_jwt still skips iss/aud validation. |
| test/workos/test_session.rb | Adds 5 well-structured tests covering the happy path, round-trip unsealing, subsequent authenticate after refresh, error cases (invalid cookie, missing refresh token), and the explicit assertion that seal_session is no longer sent in the request body. |
Sequence Diagram
sequenceDiagram
participant App
participant Session
participant SessionManager
participant WorkOS API
App->>Session: refresh(organization_id, cookie_password)
Session->>SessionManager: unseal_data(seal_data, password)
SessionManager-->>Session: access_token, refresh_token, user
Session->>WorkOS API: POST /user_management/authenticate
Note over Session,WorkOS API: grant_type=refresh_token (no cookie_password)
WorkOS API-->>Session: new access_token, refresh_token, user
Session->>SessionManager: seal_session_from_auth_response(tokens, user)
SessionManager-->>Session: sealed_cookie
Session->>Session: update seal_data and cookie_password
Session->>SessionManager: decode_jwt(access_token)
SessionManager-->>Session: decoded claims
Session-->>App: RefreshSuccess with sealed_session
Comments Outside Diff (2)
-
lib/workos/session.rb, line 107-110 (link)Unhandled JWT errors after state mutation
@seal_dataand@cookie_passwordare mutated on lines 107–108 beforedecode_jwtis called. If the API returns HTTP 200 with a nil or malformedaccess_token,JWT.decodewill raiseJWT::DecodeError(orArgumentErrorfor nil) — neither is covered by therescue WorkOS::AuthenticationError, WorkOS::InvalidRequestErrorclause. The session object is then left with partially-updated state while the caller receives an unhandled exception. A nil-guard onauth_response["access_token"]before the state mutation, or broadening the rescue to includeJWT::DecodeError, would prevent this. -
lib/workos/session.rb, line 110 (link)issandaudclaims not validated on the refresh JWTThe
decode_jwtcall here (and inSession#authenticate) usesverify_aud: falseand does not configureverify_iss. Both the issuer and audience claims are therefore never checked, meaning a JWT issued by a different provider or for a different audience would be accepted as valid. Per the project's JWT policy, bothissandaudmust be validated before use. This is pre-existing but is now also exercised by the new refresh code path.Rule Used: JWTs should always be validated before use and the... (source)
Reviews (1): Last reviewed commit: "fix: seal session client-side in Session..." | Re-trigger Greptile
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Summary
Fixes #469.
Session#refreshwas sending asessionparam to the API expecting it to return a pre-sealed cookie, but the API does not populatesealed_sessionfor refresh-token grants — so it always came back as"".sessionbody parameter and now seals the refreshed tokens client-side viaSessionManager#seal_session_from_auth_response, consistent with how every other v7 auth flow handles sealing.sessionparam is no longer sent to the API.Test plan
test_refresh_seals_session_client_side_and_returns_refresh_success— verifiesRefreshSuccesswith validsealed_sessionthat round-trips through unsealtest_refresh_updates_internal_seal_data_for_subsequent_authenticate— verifies a subsequentsession.authenticateuses the refreshed tokentest_refresh_returns_error_on_invalid_cookie— garbage input returnsRefreshErrortest_refresh_returns_error_when_no_refresh_token— missing refresh token returnsRefreshErrortest_refresh_does_not_send_session_param_to_api— assertsseal_sessionis not in the request body🤖 Generated with Claude Code