Skip to content

fix(session): require valid JWT expiration - #442

Open
gjtorikian wants to merge 3 commits into
mainfrom
workgraph/workos-php-37e-5013d0d2
Open

fix(session): require valid JWT expiration#442
gjtorikian wants to merge 3 commits into
mainfrom
workgraph/workos-php-37e-5013d0d2

Conversation

@gjtorikian

Copy link
Copy Markdown
Contributor

Summary

  • Fail closed after signature verification when exp is missing, null, or non-numeric, preventing signed session tokens from bypassing expiration checks.
  • Reject expired tokens, including the exact exp == time() boundary, while preserving numeric-string compatibility.
  • Cover 15 expiration cases with signed JWTs and an isolated, deterministic test clock; invalid tokens retain the public invalid_jwt response.
  • Keep this fix exp-only: iss/aud/nbf validation is intentionally deferred. Issuer validation is tracked separately in feat: Add optional issuer check to SessionManager authenticate #440, and the existing issuer/audience TODO remains unchanged.

Validation

composer ci passes: PHP-CS-Fixer reports 0 fixable files, PHPStan reports 0 errors, and PHPUnit reports 366 tests and 1,708 assertions. The two original missing-fixture skips are pre-existing and unrelated; current main adds a third (OrganizationsTest::testListItContacts). All 15 expiration cases pass.

Current main was merged without conflicts to exclude an inherited, already-merged PKCE change from the PR diff. Only lib/SessionManager.php, tests/SessionManagerTest.php, and tests/Fixtures/session_expiration_clock.php differ from main.

gjtorikian and others added 3 commits July 27, 2026 10:54
PKCEHelper's four flows (AuthKit/SSO authorization URL and code
exchange) required callers to re-pass a clientId the WorkOS client
already carries (constructor arg / WORKOS_CLIENT_ID). Make the
parameter optional with a fallback to requireClientId(), aligning PHP
with the other backend SDKs' override-with-fallback pattern. Explicit
arguments still win; an unconfigured client now throws
ConfigurationException instead of a TypeError.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed session tokens without a numeric expiration could bypass expiry
validation indefinitely. Fail closed while preserving numeric-string
compatibility, and reject tokens at the expiration boundary.

Addresses VULN-1270.
@gjtorikian
gjtorikian requested review from a team as code owners September 10, 2026 20:49
@gjtorikian
gjtorikian requested a review from mattgd September 10, 2026 20:49
@greptile-apps

greptile-apps Bot commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 4/5

The PR appears safe to merge with a non-blocking fractional-expiration boundary correction recommended.

Findings

  1. P2 Fractional expirations end early
Fix with agent prompt
### Issue 1
lib/SessionManager.php:445-447
Casting `exp` to an integer before comparing it rejects valid fractional expirations during their final second. For example, when `time()` is `1700000000`, an `exp` of `1700000000.5` is still in the future but is truncated to `1700000000` and returned as `invalid_jwt`. This matters because the test matrix explicitly treats floating-point expiration claims as supported.

```suggestion
        if ((float) $decoded['exp'] <= time()) {
            throw new \InvalidArgumentException('JWT has expired');
        }
```

---

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

Summary

  • Requires a numeric expiration after signature verification.
  • Treats exp == time() as expired.
  • Adds isolated-clock coverage for malformed, boundary, numeric-string, and floating-point claims.
  • One fractional-expiration boundary remains incorrect because the value is truncated before comparison.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Open sealed session] --> B[Decode JWT]
    B --> C[Verify allowed algorithm and signature]
    C --> D{exp exists and is numeric?}
    D -- No --> E[Return invalid_jwt]
    D -- Yes --> F{exp is after current time?}
    F -- No --> E
    F -- Yes --> G[Return authenticated session context]
Loading

Reviews (1) · Last reviewed commit: "chore: merge main for isolated expiry fi..."

Comment thread lib/SessionManager.php
Comment on lines +445 to 447
if ((int) $decoded['exp'] <= time()) {
throw new \InvalidArgumentException('JWT has expired');
}

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 Fractional expirations end early

Casting exp to an integer before comparing it rejects valid fractional expirations during their final second. For example, when time() is 1700000000, an exp of 1700000000.5 is still in the future but is truncated to 1700000000 and returned as invalid_jwt. This matters because the test matrix explicitly treats floating-point expiration claims as supported.

Suggested change
if ((int) $decoded['exp'] <= time()) {
throw new \InvalidArgumentException('JWT has expired');
}
if ((float) $decoded['exp'] <= time()) {
throw new \InvalidArgumentException('JWT has expired');
}

Knowledge Base Used:

Prompt To Fix With AI
This is a comment left during a code review.
Path: lib/SessionManager.php
Line: 445-447

Comment:
**Fractional expirations end early**

Casting `exp` to an integer before comparing it rejects valid fractional expirations during their final second. For example, when `time()` is `1700000000`, an `exp` of `1700000000.5` is still in the future but is truncated to `1700000000` and returned as `invalid_jwt`. This matters because the test matrix explicitly treats floating-point expiration claims as supported.

```suggestion
        if ((float) $decoded['exp'] <= time()) {
            throw new \InvalidArgumentException('JWT has expired');
        }
```

**Knowledge Base Used:**
- [SSO and session management](https://app.greptile.com/workos/-/custom-context/knowledge-base/workos/workos-php/-/docs/sso-and-session-management.md)
- [Authentication and sessions](https://app.greptile.com/workos/-/custom-context/knowledge-base/workos/workos-php/-/docs/authentication.md)

---

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.

1 participant