Skip to content

feat(user-management): Add verifyAccessToken - #1733

Open
gjtorikian wants to merge 1 commit into
mainfrom
feat/1315-verify-access-token
Open

gjtorikian wants to merge 1 commit into
mainfrom
feat/1315-verify-access-token

Conversation

@gjtorikian

Copy link
Copy Markdown
Contributor

Description

The SDK already verifies access tokens against the environment's JWKS inside authenticateWithSessionCookie, but only via the private isValidJwt. Applications that receive an access token outside a sealed session cookie, for example from an Authorization: Bearer header on an API route, had to wire up jose, getJwksUrl() and the issuer check themselves.

This adds a public userManagement.verifyAccessToken(accessToken):

  • Verifies the signature against the cached remote JWKS and the expiry, plus the issuer when one is configured on the client (same checks as the session helpers).
  • Resolves to the token's claims, typed as the new exported AccessTokenClaims (sub, iss, exp, iat, auth_time, plus the existing UserManagementAccessToken session claims and an index signature for custom claims).
  • Resolves to null when the token is invalid; errors that are not JWT validation failures, such as a network error fetching the JWKS, propagate. Throws when no client ID was configured.
  • isValidJwt now delegates to it, so the session helpers keep their behavior.
  • Added to PublicUserManagementMethods, since verification needs only a client ID.
const claims = await workos.userManagement.verifyAccessToken(accessToken);
if (!claims) {
  // reject
}
claims.sub; // user ID
claims.org_id;
claims.permissions;

The README gains a short "Verifying access tokens" section next to the issuer validation notes.

Fixes #1315

Documentation

Does this require changes to the WorkOS Docs? E.g. the API Reference or code snippets need updates.

[ ] Yes

If yes, link a related docs PR and add a docs maintainer as a reviewer. Their approval is required.

The SDK already verified access tokens against the environment's JWKS
inside `authenticateWithSessionCookie`, but only through a private
helper, so applications that receive an access token outside a sealed
session cookie (for example from an `Authorization: Bearer` header) had
to wire up jose and the JWKS URL themselves.

Expose `userManagement.verifyAccessToken(accessToken)`. It checks the
signature, expiry and, when configured, the issuer, and resolves to the
token's claims typed as `AccessTokenClaims`, or `null` when the token is
invalid. Non-JWT errors such as a failed JWKS fetch still propagate, the
same contract the session helpers use. `isValidJwt` now delegates to it,
and the method is part of the public-client surface since it needs only
a client ID.

Fixes #1315
@gjtorikian
gjtorikian requested review from a team as code owners October 20, 2026 21:17
@gjtorikian
gjtorikian requested a review from mattgd October 20, 2026 21:17
@greptile-apps

greptile-apps Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 4/5

[Medium impact] The change reaches applications that verify WorkOS access tokens through the User Management SDK.

The PR appears safe to merge, with non-blocking improvements to required-claim checks and the README example.

Findings

  1. P2 Required claims go unchecked ▶
  2. P2 Example reads rejected claims ▶
Fix with agent prompt
### Issue 1
src/user-management/user-management.ts:771-776
`verifyAccessToken` returns the payload as `AccessTokenClaims` without checking that `sub`, `sid`, `iss`, `exp`, and `iat` exist. The type argument does not check these fields at runtime. If a signed token for another purpose lacks them, callers receive missing values where TypeScript promises values.

Add a required-claims check before returning, as `hasRequiredAgentClaims` already does for agent tokens.

### Issue 2
README.md:95-97
The example continues to `claims.sub` after `verifyAccessToken` returns `null`. The comment inside `if (!claims)` does not stop execution, so the pasted example fails TypeScript's null check and throws for an invalid token. Use a `throw` or `return` in that branch.

```suggestion
if (!claims) {
  throw new Error('Invalid access token');
}
```

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Summary

Adds public userManagement.verifyAccessToken and the exported AccessTokenClaims type.

  • Applications can verify access tokens and read their claims.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Access token] --> B[verifyAccessToken]
  B --> C[Cached remote JWKS]
  C --> D[Check signature, time claims, and configured issuer]
  D --> E[Return claims]
  D --> F[JWT or JWS failure: return null]
  D --> G[Other error: throw]
  H[isValidJwt] --> B
  E --> I[true]
  F --> J[false]
Loading

Reviews (1) · Last reviewed commit: "feat(user-management): Add verifyAccessT..." · Reviewed by Greptile

Comment on lines +771 to +776
const { payload } = await jwtVerify<AccessTokenClaims>(
accessToken,
jwks,
this.issuer ? { issuer: this.issuer } : undefined,
);
return true;
return payload;

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.

P2 Required claims go unchecked

verifyAccessToken returns the payload as AccessTokenClaims without checking that sub, sid, iss, exp, and iat exist. The type argument does not check these fields at runtime. If a signed token for another purpose lacks them, callers receive missing values where TypeScript promises values.

Add a required-claims check before returning, as hasRequiredAgentClaims already does for agent tokens.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/user-management/user-management.ts
Line: 771-776

Comment:
**Required claims go unchecked**

`verifyAccessToken` returns the payload as `AccessTokenClaims` without checking that `sub`, `sid`, `iss`, `exp`, and `iat` exist. The type argument does not check these fields at runtime. If a signed token for another purpose lacks them, callers receive missing values where TypeScript promises values.

Add a required-claims check before returning, as `hasRequiredAgentClaims` already does for agent tokens.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Comment thread README.md
Comment on lines +95 to +97
if (!claims) {
// Reject the request.
}

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.

P2 Example reads rejected claims

The example continues to claims.sub after verifyAccessToken returns null. The comment inside if (!claims) does not stop execution, so the pasted example fails TypeScript's null check and throws for an invalid token. Use a throw or return in that branch.

Suggested change
if (!claims) {
// Reject the request.
}
if (!claims) {
throw new Error('Invalid access token');
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: README.md
Line: 95-97

Comment:
**Example reads rejected claims**

The example continues to `claims.sub` after `verifyAccessToken` returns `null`. The comment inside `if (!claims)` does not stop execution, so the pasted example fails TypeScript's null check and throws for an invalid token. Use a `throw` or `return` in that branch.

```suggestion
if (!claims) {
  throw new Error('Invalid access token');
}
```

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

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.

Add method for verifying JWT token

1 participant