Skip to content

fix(session): Reject non-user access tokens in session cookies - #99

Merged
gjtorikian merged 2 commits into
mainfrom
fix/session-user-access-token-claims
Nov 6, 2026
Merged

gjtorikian merged 2 commits into
mainfrom
fix/session-user-access-token-claims

Conversation

@m0tzy

@m0tzy m0tzy commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

Summary

verifyAccessToken in session.ts only ran jwtVerify, which checks the signature, exp, and iss. It never checked that the token is a user session access token, so other token types signed by the same environment key could authenticate a session.

const { payload } = await jwtVerify(accessToken, JWKS, { issuer: getExpectedIssuer() });
return isUserAccessTokenPayload(payload);
// typeof sid === 'string' && typeof auth_time === 'number'
// && (sub_profile === undefined || sub_profile === 'user') && sub_type === undefined

This matches hasRequiredAccessTokenClaims in @workos-inc/node. sub_profile is optional on user access tokens, so a token without it is still accepted. Rejected tokens go down the existing invalid-token path, where the loader tries a refresh, the same as when verification fails.

withAuth previously decoded the cookie's access token and only checked exp, relying on a parent authkitLoader to have verified it. Parent and child loaders run in parallel, so it now verifies the token itself:

// after the existing exp check
if (!(await verifyAccessToken(session.accessToken))) {
  return { user: null };
}

withAuth still can't refresh, so a token that fails verification returns no user until the parent loader refreshes the session. The JWKS comes from the module-level getJWKS() cache, so this adds a signature check per call, not a fetch.

Tests: the new should not accept a verified access token with %s covers sub_profile: ai_agent, sub_type: authkit, a missing or string auth_time, and a missing or numeric sid. All 6 cases fail without the session.ts change. withAuth's should return NoUserInfo when the access token fails verification fails without the auth.ts change. The existing jwtVerify mocks now resolve a user-shaped payload.

Link to Devin session: https://app.devin.ai/sessions/7e123fc2545a49c282b043a4abd58f70
Open in Devin Desktop: https://app.devin.ai/desktop/session/7e123fc2545a49c282b043a4abd58f70?variant=devin
Requested by: @m0tzy

verifyAccessToken now requires sid and auth_time, a sub_profile that is absent
or user, and no sub_type, so other token types signed by the same environment
key do not authenticate a session.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration

Copy link
Copy Markdown
Contributor

I'll fix CI failures and address comments from users with write access. I'll skip comments containing "(aside)".

  • Disable automatic comment, CI, and merge conflict monitoring

Original prompt from madison.packer

What's the fix here? https://linear.app/workos/issue/VULN-4839/lt-app26-10-user-vs-agent-jwt-type-confusion

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no bugs or issues to report.

Devin Review

@devin-ai-integration

Copy link
Copy Markdown
Contributor

Devin Review

🟢 Devin Review found nothing to fix · View findings

@greptile-apps

greptile-apps Bot commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 5/5

[High impact] The PR appears safe to merge.

Summary

The PR limits session cookies to verified user access tokens.

  • Session checks now accept only user-shaped access tokens.

No actionable new issues were found. Tests were inspected but not run.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  Cookie[Read session cookie] --> Caller{Entry point}
  Caller --> Loader[authkitLoader]
  Caller --> Helper[withAuth]
  Loader --> Verify[Verify signature, issuer, and user claims]
  Helper --> Expiry{Token expired?}
  Expiry -->|Yes| Anonymous[Return user null]
  Expiry -->|No| VerifyHelper[Run the shared token check]
  VerifyHelper -->|Pass| User[Return user information]
  VerifyHelper -->|Fail| Anonymous
  Verify -->|Pass| User
  Verify -->|Fail| Refresh[Try the existing refresh flow]
Loading

Reviews (2) · Last reviewed commit: "fix(auth): Verify the access token in wi..." · Reviewed by Greptile

withAuth decoded the cookie's access token without verifying it, relying on a parent authkitLoader that may run in parallel. It now returns no user unless verifyAccessToken passes.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration

Copy link
Copy Markdown
Contributor

@greptileai review

@gjtorikian
gjtorikian merged commit a52eeb6 into main Nov 6, 2026
12 checks passed
@gjtorikian
gjtorikian deleted the fix/session-user-access-token-claims branch November 6, 2026 18:30
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.

2 participants