Skip to content

fix(auth): verify sealed-session access tokens - #33

Open
gjtorikian wants to merge 5 commits into
mainfrom
workgraph/workos-ios-1vr-7fe40d63
Open

fix(auth): verify sealed-session access tokens#33
gjtorikian wants to merge 5 commits into
mainfrom
workgraph/workos-ios-1vr-7fe40d63

Conversation

@gjtorikian

Copy link
Copy Markdown
Collaborator

Description

Addresses VULN-1614.

  • Add client-bound authenticateVerified() and verified logout helpers that validate RS256 access tokens against the client's JWKS, require sub/exp, bind cookie user IDs to the signed subject, and omit unsigned impersonator data.
  • Require at least 32 characters for verified-session cookie passwords. Deprecate the unverified APIs while preserving source compatibility; legacy authentication now rejects missing or invalid expiration claims.
  • Emit randomized, AAD-bound wos2. seals using PBKDF2-HMAC-SHA256 (600,000 iterations, 16-byte salt). Continue reading legacy SHA-256/hex-key seals and upgrade them on reseal.
  • Add regression coverage for the reported unsigned/no-expiry forgery, JWT/JWKS attack matrices, subject mismatch, password validation, verified expired-token logout, seal tampering, legacy migration, and an independent Node.js crypto fixture.

Compatibility and migration

Migrate to try client.loadVerifiedSession(sessionData:cookiePassword:), then await session.authenticateVerified() and await session.getVerifiedLogoutUrl() (or the client-bound static verified authentication helper).

Deprecated authentication remains signature-unverified for source compatibility; mandatory expiration alone does not prevent forgeries with a future exp. Seal/refresh operations continue accepting legacy short passwords to avoid forced logouts, but verified authentication rejects them. Use a cryptographically random password of at least 32 characters. Cookie profile fields other than the matched user ID are not signed claims.

PBKDF2 adds CPU cost (roughly 100–300 ms depending on device); seal/unseal should run off the UI thread. Verified authentication fetches JWKS on every call without caching and fails closed on endpoint/verification errors. New seals cannot be read by older SDK versions.

Validation

./script/ci passes on current main: strict Swift formatting, build, and all 385 tests in 36 suites. No warnings originate in changed source files; existing generated-model deprecations and expected legacy-API test warnings remain. All five changed files are hand-maintained @oagen-ignore-file files.

Documentation

No WorkOS API-reference changes. Migration, trust boundaries, password guidance, and PBKDF2/network costs are documented in the helper API comments above.

constructAction deserialized the verified action request into the generic
EventSchema envelope ({object, id, event, data, created_at}). The real
Actions wire format is a flat context object discriminated by 'object'
(authentication_action_context / user_registration_action_context), so
parsing it into EventSchema silently dropped every useful field (user,
user_data, ip_address, device_fingerprint, issuer, ...) with no error.

Replace the EventSchema return with a typed ActionContext
(+ ActionUserData), mirroring workos-node's ActionContext and reusing the
generated User, Organization, OrganizationMembership, and Invitation models
for nested objects. Dispatch on 'object' to read the type-specific fields.

Tests now exercise the real wire format for both context types; the prior
test used a fabricated event envelope that never matched what WorkOS sends.
signResponse emitted a base64 {payload, sig} shape that no other SDK
produces. The real Actions response wire format (workos-node + python,
ruby, php, dotnet, kotlin) is {object, payload, signature}, where
signature = HMAC-SHA256(secret, "<timestamp>.<JSON(payload)>") and
payload = {timestamp, verdict, error_message?}.

Switch ActionSignedResponse to {object, payload, signature} and build
the response body by hand so the signed payload bytes are byte-identical
to the transmitted payload bytes (JSONEncoder reorders nested keys
non-deterministically, so re-serialization would break the signature).
Callers send bodyData as the response body. error_message is included
only on a Deny verdict with a non-empty message, matching workos-node.
A compromised cookie password must not make attacker-controlled JWT
claims authoritative. Add an opt-in verified path while retaining
source compatibility, and reject non-expiring legacy tokens to close
the reported forgery path.

Slow salted key derivation raises the cost of offline password guessing
without forcing existing sessions to log out during the upgrade.

Addresses VULN-1614.
The workflow started from a branch whose earlier changes were already
squash-merged. Include current main so the security PR has a focused
diff and validates against the current generated SDK surface.
@greptile-apps

greptile-apps Bot commented Sep 11, 2026

Copy link
Copy Markdown

RetriggerConfidence Score: 3/5

The PR should not merge until arbitrary forged cookies can no longer trigger expensive synchronous password derivation per request.

Findings

  1. P1 Security Forged Cookies Amplify CPU
Fix with agent prompt
### Issue 1
Sources/WorkOS/Helpers/SessionSealing.swift:68-69
Any attacker-supplied `wos2.` cookie that base64-decodes to more than 44 bytes reaches the 600,000-iteration PBKDF before authentication fails. Session cookies are processed on each request, and this synchronous derivation runs before `authenticateVerified()` reaches its first `await`. Repeated forged cookies can therefore consume roughly 100–300 ms of CPU each, starving the cooperative executor or exhausting server capacity. Avoid performing this expensive password derivation independently for every untrusted request-for example, prederive or cache server-side key material and use a cheap authenticated per-cookie derivation while bounding concurrent work.

**How this was verified:** A syntactically valid unauthenticated cookie reaches the synchronous 600,000-iteration PBKDF before AES-GCM authentication or JWT verification occurs.

---

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

Summary

  • Verifies RS256 access tokens against the configured client's JWKS and requires signed sub and exp claims.
  • Binds sealed user IDs to the signed subject and excludes unsigned impersonator data.
  • Introduces versioned wos2. seals while retaining read compatibility with legacy seals.
  • Adds token/JWKS attack-matrix, password, tampering, expiry, and migration tests.
  • The new 600,000-iteration derivation is reachable by arbitrary valid-looking cookies before authentication, enabling CPU amplification.

Diagram

sequenceDiagram
    participant Request as Incoming request
    participant Session as Session.authenticateVerified
    participant Seal as SessionSealing
    participant JWKS as WorkOS JWKS
    Request->>Session: Untrusted wos2 cookie
    Session->>Seal: Synchronous unseal
    Seal->>Seal: Base64 and minimum-length checks
    Seal->>Seal: PBKDF2-SHA256 (600,000 iterations)
    Seal->>Seal: AES-GCM authentication
    alt Valid sealed session
        Session->>JWKS: Fetch client-bound keys
        JWKS-->>Session: JWKS
        Session->>Session: Verify RS256, sub, exp, nbf
        Session-->>Request: Verified authentication result
    else Forged sealed session
        Seal-->>Session: Decryption failure
        Session-->>Request: invalid_session_cookie
    end
Loading

Reviews (1) · Last reviewed commit: "chore: sync main for sealed-session secu..."

Comment on lines +68 to +69
let key =
versioned ? try deriveKey(password, salt: Data(raw.prefix(16))) : deriveKey(password)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 security Forged Cookies Amplify CPU

Any attacker-supplied wos2. cookie that base64-decodes to more than 44 bytes reaches the 600,000-iteration PBKDF before authentication fails. Session cookies are processed on each request, and this synchronous derivation runs before authenticateVerified() reaches its first await. Repeated forged cookies can therefore consume roughly 100–300 ms of CPU each, starving the cooperative executor or exhausting server capacity. Avoid performing this expensive password derivation independently for every untrusted request—for example, prederive or cache server-side key material and use a cheap authenticated per-cookie derivation while bounding concurrent work.

How this was verified: A syntactically valid unauthenticated cookie reaches the synchronous 600,000-iteration PBKDF before AES-GCM authentication or JWT verification occurs.

Knowledge Base Used:

Prompt To Fix With AI
This is a comment left during a code review.
Path: Sources/WorkOS/Helpers/SessionSealing.swift
Line: 68-69

Comment:
**Forged Cookies Amplify CPU**

Any attacker-supplied `wos2.` cookie that base64-decodes to more than 44 bytes reaches the 600,000-iteration PBKDF before authentication fails. Session cookies are processed on each request, and this synchronous derivation runs before `authenticateVerified()` reaches its first `await`. Repeated forged cookies can therefore consume roughly 100–300 ms of CPU each, starving the cooperative executor or exhausting server capacity. Avoid performing this expensive password derivation independently for every untrusted request—for example, prederive or cache server-side key material and use a cheap authenticated per-cookie derivation while bounding concurrent work.

**How this was verified:** A syntactically valid unauthenticated cookie reaches the synchronous 600,000-iteration PBKDF before AES-GCM authentication or JWT verification occurs.

**Knowledge Base Used:**
- [Authentication and sessions](https://app.greptile.com/workos/-/custom-context/knowledge-base/workos/workos-ios/-/docs/authentication-and-sessions.md)
- [Security and data protection](https://app.greptile.com/workos/-/custom-context/knowledge-base/workos/workos-ios/-/docs/security-and-data-protection.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