Skip to content

fix(lib): add verify_user_token helper with remote JWKS caching#45

Merged
jjantschulev merged 1 commit intonextfrom
jordan/add-verify-user-token
Apr 20, 2026
Merged

fix(lib): add verify_user_token helper with remote JWKS caching#45
jjantschulev merged 1 commit intonextfrom
jordan/add-verify-user-token

Conversation

@jjantschulev
Copy link
Copy Markdown
Contributor

Summary

Adds a verify_user_token helper that mirrors the TypeScript SDK's verify-user-token.ts, so app-proxy-issued x-whop-user-token JWTs can be verified in Python apps with the same key-rotation-friendly semantics (no SDK release needed when keys roll).

  • src/whop_sdk/lib/verify_user_token.py (new):
    • verify_user_token() — validates against the canonical Whop JWKS at https://api.whop.com/.well-known/jwks.json by default, with a module-level cache (12h TTL, 30s cooldown-guarded refetch on kid miss) mirroring jose's createRemoteJWKSet.
    • try_verify_user_token() — returns None on failure instead of raising.
    • Accepts a raw token string OR a headers mapping (case-insensitive).
    • Options mirror TS: public_key (PEM or JWK JSON static override), jwks_url (override the endpoint), header_name, app_id.
    • Handles legacy kid-less tokens by trying each key in the current JWKS snapshot.
  • src/whop_sdk/lib/__init__.py (new): marks lib/ as a package.
  • pyproject.toml: adds optional user-tokens extra — pip install 'whop-sdk[user-tokens]' pulls in pyjwt[crypto]>=2.8,<3 for ES256/ECDSA support.

Coordinated with matching changes in the TypeScript and Ruby SDKs (landing in their respective next branches concurrently) so all three ship at the same version.

Test plan

  • Static public_key path verifies (PEM and JWK JSON forms)
  • Remote JWKS fetch verifies tokens end-to-end against live api.whop.com
  • Module-level cache reduces subsequent calls to ~0ms
  • Case-insensitive header lookup works
  • Forged tokens rejected

Mirrors the TypeScript SDK's verify-user-token.ts so app-proxy-issued
x-whop-user-token JWTs can be verified in Python apps, with the same
key-rotation-friendly semantics (no SDK release needed when keys roll).

- src/whop_sdk/lib/verify_user_token.py (new):
  * verify_user_token() — validates against the canonical Whop JWKS at
    https://api.whop.com/.well-known/jwks.json by default, with a
    module-level cache (12h TTL, 30s cooldown-guarded refetch on kid
    miss) mirroring jose's createRemoteJWKSet.
  * try_verify_user_token() — returns None on failure instead of raising.
  * Accepts a raw token string OR a headers mapping (case-insensitive).
  * Options mirror TS: public_key (PEM or JWK JSON static override),
    jwks_url (override the endpoint), header_name, app_id.
  * Handles legacy kid-less tokens by trying each key in the current
    JWKS snapshot.
- src/whop_sdk/lib/__init__.py (new): marks lib/ as a package.
- pyproject.toml: adds optional user-tokens extra
    pip install 'whop-sdk[user-tokens]'
  which pulls in pyjwt[crypto]>=2.8,<3 for ES256 / ECDSA support.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@jjantschulev jjantschulev force-pushed the jordan/add-verify-user-token branch from 126ed07 to 4c06227 Compare April 20, 2026 16:29
@jjantschulev jjantschulev merged commit 2ffb934 into next Apr 20, 2026
6 checks passed
@stainless-app stainless-app Bot mentioned this pull request Apr 20, 2026
jjantschulev added a commit that referenced this pull request Apr 21, 2026
* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* fix: ensure file data are only sent as 1 parameter

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* feat(api): api update

* codegen metadata

* feat(api): api update

* codegen metadata

* fix(lib): add verify_user_token helper with remote JWKS caching (#45)

Mirrors the TypeScript SDK's verify-user-token.ts so app-proxy-issued
x-whop-user-token JWTs can be verified in Python apps, with the same
key-rotation-friendly semantics (no SDK release needed when keys roll).

- src/whop_sdk/lib/verify_user_token.py (new):
  * verify_user_token() — validates against the canonical Whop JWKS at
    https://api.whop.com/.well-known/jwks.json by default, with a
    module-level cache (12h TTL, 30s cooldown-guarded refetch on kid
    miss) mirroring jose's createRemoteJWKSet.
  * try_verify_user_token() — returns None on failure instead of raising.
  * Accepts a raw token string OR a headers mapping (case-insensitive).
  * Options mirror TS: public_key (PEM or JWK JSON static override),
    jwks_url (override the endpoint), header_name, app_id.
  * Handles legacy kid-less tokens by trying each key in the current
    JWKS snapshot.
- src/whop_sdk/lib/__init__.py (new): marks lib/ as a package.
- pyproject.toml: adds optional user-tokens extra
    pip install 'whop-sdk[user-tokens]'
  which pulls in pyjwt[crypto]>=2.8,<3 for ES256 / ECDSA support.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* codegen metadata

* codegen metadata

* feat(api): api update

* fix(types): add BannerImage to forum_update_params (#46)

forums.py resource references forum_update_params.BannerImage for the
banner_image parameter on PATCH /forums/{id}, but the type file was
missing the class and the banner_image field (a codegen-side artifact
of the recent merge conflict resolution where forums.py was taken from
the generated branch but forum_update_params.py was taken from the
integrated branch). Both fields are in the live public API (verified
against api.whop.com/openapi.json).

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(api): api update

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* feat(api): api update

* perf(client): optimize file structure copying in multipart requests

* codegen metadata

* codegen metadata

* codegen metadata

* fix(tests): pass required company_id to invoices.list() calls (#47)

`invoices.list()` (resource) requires `company_id: str` (no Omit), but
the bare `test_method_list` / `test_raw_response_list` / `test_streaming_response_list`
variants (sync + async) call `list()` with no args. Pyright fails with
"Argument missing for parameter company_id". Fallout from the recent
merge conflict resolution on PR #884 where the test file was taken from
codegen but the resource file had `company_id` as required from a
different codegen run.

Added `company_id="biz_xxxxxxxxxxxxxx"` (matches the pattern used by
`test_method_list_with_all_params`) to the six affected list-variant
calls.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* codegen metadata

* feat(api): api update

* codegen metadata

* feat(api): manual updates

* release: 0.0.38

---------

Co-authored-by: stainless-app[bot] <142633134+stainless-app[bot]@users.noreply.github.com>
Co-authored-by: Jordan Jantschulev <j.jantschulev@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant