Skip to content

feat: Add optional jwt_issuer option for session token validation - #725

Merged
gjtorikian merged 3 commits into
mainfrom
devin/1788638290-optional-issuer
Sep 17, 2026
Merged

gjtorikian merged 3 commits into
mainfrom
devin/1788638290-optional-issuer

Conversation

@m0tzy

@m0tzy m0tzy commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds an opt-in iss claim check to Session / AsyncSession access-token validation (both the authenticate() and refresh() jwt.decode calls). When jwt_issuer is configured on the client it is passed to PyJWT; when unset, behavior is unchanged (no iss check — PyJWT skips _validate_iss when issuer is None).

WorkOSClient(..., jwt_issuer: Optional[Union[str, Sequence[str]]] = None)
AsyncWorkOSClient(..., jwt_issuer=...)
# falls back to WORKOS_ISSUER, comma-separated for a list

jwt.decode(access_token, key, algorithms=..., options={"verify_aud": False},
           leeway=self._client._jwt_leeway,
           issuer=self._client._jwt_issuer)   # None -> not validated

jwt_issuer accepts a single issuer or a list so an app can accept tokens from more than one issuer (e.g. during an issuer migration); PyJWT's issuer parameter already accepts str | Container[str]. A WORKOS_ISSUER env value is split on commas (trimmed, empties dropped; a single value stays a str). Named jwt_issuer to sit alongside the existing jwt_leeway option.

Only hand-maintained (@oagen-ignore-file) files are touched: _base_client.py, session.py.

Opt-in rather than defaulted because the API does not mint a single issuer shape — it varies by environment (https://api.workos.com for legacy environments, https://api.workos.com/user_management/<clientId> for environments created since mid-2025, custom auth domains, and flag-gated path variants). Mirrors workos/workos-node#1694.

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

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@m0tzy
m0tzy requested review from a team as code owners September 5, 2026 19:58
@m0tzy
m0tzy requested a review from dandorman September 5, 2026 19:58
@devin-ai-integration

Copy link
Copy Markdown
Contributor

🤖 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

Original prompt from madison.packer

can we patch this SDK so that the issuer can be either by default (if not passed) or passed a specific issuer?

const issuer = opts.issuer ?? https://${getConfig('apiHostname')}

workos/authkit-react-router#83

@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 found 1 potential issue.

Devin Review

Comment thread src/workos/_base_client.py Outdated
@greptile-apps

greptile-apps Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 5/5

The PR appears safe to merge; no outstanding correctness, security, or repository-rule issues were identified.

Summary

Adds optional issuer validation for session access tokens.

  • Introduces jwt_issuer configuration for synchronous and asynchronous clients, with comma-separated WORKOS_ISSUER environment-variable fallback.
  • Applies the configured issuer constraint during both session authentication and token refresh.
  • Adds coverage for matching, mismatched, multiple, absent, and empty issuer configurations across synchronous and asynchronous paths.
  • The previously reported refresh-test gap is fixed, and its thread was resolved.
Diagram
%%{init: {'theme': 'neutral'}}%%
flowchart LR
    A[Client configuration] --> B{jwt_issuer supplied?}
    B -->|Yes| C[Use configured issuer or issuer list]
    B -->|No| D{WORKOS_ISSUER set?}
    D -->|Yes| E[Parse comma-separated issuers]
    D -->|No| F[Issuer validation disabled]
    C --> G[Session authenticate or refresh]
    E --> G
    F --> G
    G --> H[Verify signature, lifetime, and configured issuer]
    H -->|Valid| I[Return session success]
    H -->|Invalid| J[Return INVALID_JWT]
Loading

Reviews (3) · Last reviewed commit: "Merge branch 'main' into devin/178863829..."

Comment thread src/workos/session.py
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

@gjtorikian gjtorikian 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.

Thanks!

@gjtorikian
gjtorikian merged commit 70987c7 into main Sep 17, 2026
11 checks passed
@gjtorikian
gjtorikian deleted the devin/1788638290-optional-issuer branch September 17, 2026 16:42
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