Skip to content

feat(tokens): add offline client-token verification - #71

Merged
AdirAmsalem merged 2 commits into
mainfrom
conductor/decart-python-offline-client-token-verify
Sep 24, 2026
Merged

AdirAmsalem merged 2 commits into
mainfrom
conductor/decart-python-offline-client-token-verify

Conversation

@AdirAmsalem

@AdirAmsalem AdirAmsalem commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Client tokens from client.tokens.create are EdDSA JWTs verifiable offline against the platform JWKS, but the SDK had no way to verify them. This adds it.

API

verified = await client.tokens.verify(token)   # or: await verify_client_token(token)
verified.service_tier, verified.pool, verified.user_id, verified.expires_at

claims = client.tokens.decode(token)            # or: decode_client_token(token) — no verification, untrusted
  • Checks the EdDSA signature against https://platform.decart.ai/api/auth/jwks (fetched once and cached via PyJWT's PyJWKClient), plus exp (30s leeway), iss and aud. jwks_url, issuer, audience, leeway are overridable.
  • Returns VerifiedClientToken; decode returns its untrusted parent ClientTokenClaims. Fields: user_id, organization_id, api_key_id (parent key, else token id), api_key_name, service_tier, pool ("free" for tier 0, else "paid"), allowed_models, allowed_origins, constraints, realtime_concurrent_session_limit, zero_data_retention, attribution, expires_at, raw claims.
  • Raises TokenVerifyError / TokenDecodeError.
  • Requires the new optional extra decart[verify] (pyjwt[crypto]), imported lazily so minting-only users pull no crypto dependency. decode needs no extra.

Note

Medium Risk
Introduces a security-sensitive JWT verification path backends may use for authorization; behavior is well-tested and optional, but incorrect adoption of decode without verify could weaken trust assumptions.

Overview
Adds offline verification and decoding for short-lived client JWTs minted via client.tokens.create, so backends can trust signed claims (tier, models, origins, expiry) without calling the platform.

New verify_client_token / client.tokens.verify validate EdDSA signatures against the platform JWKS (https://platform.decart.ai/api/auth/jwks, cached in-process), plus exp (30s leeway), iss, and aud. decode_client_token / client.tokens.decode parse claims with no signature check (untrusted). Results are typed as VerifiedClientToken / ClientTokenClaims with mapped fields including pool ("free" for tier 0). Failures raise TokenVerifyError / TokenDecodeError.

Verification is gated behind optional decart[verify] (pyjwt[crypto]), lazily imported so token creation stays lightweight. README documents the flow; a large test suite covers tampering, expiry, JWKS caching, and key rotation.

Reviewed by Cursor Bugbot for commit b3ab053. Bugbot is set up for automated code reviews on this repo. Configure here.

Client tokens from client.tokens.create are EdDSA (Ed25519) JWTs signed
by the platform and verifiable against its public JWKS, but the SDK had
no way to verify them, so integrators round-tripped to the platform to
read values already signed into the token (service_tier, allowed
models/origins, expiry, owner ids).

- client.tokens.verify(token) / verify_client_token(token): verify the
  signature against the platform JWKS (fetched once and cached), plus
  exp (30s leeway), iss and aud. Returns VerifiedClientToken.
- client.tokens.decode(token) / decode_client_token(token): claims
  without verification, no network. Returns ClientTokenClaims,
  documented as untrusted.
- Result fields: user_id, organization_id, api_key_id (parent key, else
  the token id), api_key_name, service_tier, pool ("free" for tier 0,
  else "paid"), allowed_models, allowed_origins, constraints,
  realtime_concurrent_session_limit, zero_data_retention, attribution,
  expires_at, and the raw claims.
- New TokenVerifyError and TokenDecodeError.
- PyJWT + cryptography ship as the optional `verify` extra and are
  imported lazily, so minting-only users pull no crypto dependency.
@AdirAmsalem
AdirAmsalem force-pushed the conductor/decart-python-offline-client-token-verify branch from 4408273 to 4021a62 Compare September 24, 2026 13:36

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 4021a62. Configure here.

Comment thread decart/tokens/verify.py Outdated
Convert the exp claim with datetime.fromtimestamp(..., tz=timezone.utc)
instead of relying on pydantic's int-to-datetime coercion, which is not
guaranteed to be timezone-aware across supported pydantic 2.x versions.
@AdirAmsalem
AdirAmsalem merged commit 4dd9ce1 into main Sep 24, 2026
9 checks passed
@AdirAmsalem
AdirAmsalem deleted the conductor/decart-python-offline-client-token-verify branch September 24, 2026 13:48
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