Files
windmill/backend/windmill-api-assets
Ruben FiszelandClaude Opus 4.8 9cb8e991eb feat: guest JWT entry for embedded apps
A second way in for a guest (companion to windmill#10929): a JWT the embedding
customer's own backend mints and signs, carried on the app's share link and
verified against a key the workspace admin configured. It needs no
identity-provider round-trip, so it works inside an iframe where popups and
third-party cookies do not. Bearer prefix jwt_guest_, stateless: verified per
request, cached until exp, no token row.

A JWT guest is the same identity as a signed-in guest: no usr row, no password
row, no seat, confined to the one app app_path names. Every guest gate applies:
the plan, the workspace switch (enforced once at the auth door via the sentinel),
the app mode (guest_app_admits), and "no account at all" (has_any_account). The
claim's workspace_id must equal the route's workspace, and a workspace-less route
never accepts it.

Claims honoured: email, workspace_id, app_path, exp (mandatory); nbf/iat
validated when present; the accepted lifetime is capped at 24h. Algorithms:
RS256/384/512, PS256/384/512, ES256/384; HS* is refused. The key is a
per-workspace setting, a PEM public key or a JWKS URL (at most one, a DB CHECK
enforces it), Enterprise-plan gated like the guest switch. The JWKS URL is
validated against private ranges and the fetch is pinned to the validated
address.

Counting: a JWT guest is recorded in guest_activity (once per email, workspace
and day, cached), marked jwt_entry, and not in unique_ext_jwt_token. A first-seen
users.login_guest audit carries the entry kind.

Narrower than jwt_ext_ by design: that key is instance-level and can assert
admin, groups and folders; a guest key is scoped to one workspace and only ever
mints guests. An app-only user a customer routes through jwt_ext_ today is
counted; through a guest JWT they become a free guest, the intended pricing
change, split out as guest_jwt_count in the telemetry so it can be measured.

Changes on the parent branch, additive: ApiAuthed.credential_expiry (a
credential's own expiry when it has no token row); guest_derived_token_constraints
caps on it; guest_session_scopes moved to windmill-api-auth::scopes and
has_any_account to windmill-common::users so the mint and the JWT arm share one
copy; the signed-in mint's login_guest audit now carries entry=idp.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VF3v6LA9399gNphmZaHYG3
2026-09-03 08:00:25 +00:00
..