demo.scitokens.org — decode, verify and generate SciTokens.
This is a static frontend + a single Cloudflare Python Worker:
- Frontend (
public/): oneindex.html(precompiled TailwindCSS + CodeMirror 5 from a CDN) — a jwt.io-style debugger with the algorithm selector (RS256 / ES256), colour-coded encoded token, syntax-highlighted decoded JSON, hover tooltips that render epoch-timestamp claims (exp/iat/nbf) as locale-accurate dates, live signature verification, a claim-level validation panel, copy buttons, a dark/light theme toggle (system default + manual override), a responsive mobile nav, and the "known libraries" section. Served via Workers Static Assets. Tailwind is compiled topublic/app.cssfromstyles/input.css(npm run build:css) — no runtime CDN. - Backend (
src/): a Python Worker that signs/verifies tokens with the Pythonscitokenslibrary and implements the OAuth2 device-code flow,/protected, and/secret.
public/ static site (served by Workers Static Assets)
index.html single-page debugger UI
app.css compiled Tailwind (built from styles/input.css)
device-code/index.html device-code submission page
img/ favicon + library icons
styles/input.css Tailwind source (@tailwind directives + custom CSS)
tailwind.config.js Tailwind config (class dark mode, scans public/**/*.html)
src/
entry.py async on_fetch router (the Worker entry point)
tokens.py issue / verify / enforce / JWKS (scitokens)
pyproject.toml Python deps (scitokens, PyJWT, cryptography)
wrangler.jsonc Worker config (assets, secrets)
dev_server.py local-only dev server (no Cloudflare toolchain needed)
| Method | Path | Purpose |
|---|---|---|
| GET/POST | /issue |
Sign a SciToken ({payload, algorithm}) |
| POST | /verify |
Verify a token → {Success, Error} |
| GET | /oauth2/certs |
JWKS (RS256 + ES256 public keys) |
| GET | /.well-known/openid-configuration |
OIDC discovery |
| POST | /oauth2/oidc-cm |
Client registration |
| POST | /oauth2/device_authorization |
Device-code start (stateless) |
| POST | /submit-code |
Device-code submission (no-op → redirect) |
| POST | /oauth2/token |
Issue access + refresh tokens (always issues) |
| GET | /protected |
Resource requiring read:/protected |
| GET | /secret |
Resource requiring read:/secret — confirms a successful query |
Keys are supplied as Worker secrets (base64-encoded PEM, same as the old app):
base64 -i private.pem | npx wrangler secret put PRIVATE_KEY
base64 -i ec_private.pem | npx wrangler secret put EC_PRIVATE_KEYUses the uv-first Python Workers workflow.
Node version: use Node 20–22 (LTS).
workers-py's Pyodide shim passes--experimental-wasm-stack-switching, which was removed in Node 24+, so the very latest Node fails at venv creation.compatibility_dateis set to2025-11-02inwrangler.jsoncso the Pyodide runtime providescryptography(with its OpenSSL) as a built-in — older dates vendor a broken copy that can't findlibssl.
uv tool install workers-py
npm install # installs wrangler + tailwindcss (dev dependencies)
npm run build:css # compile public/app.css (rerun after editing markup/styles)
cp .dev.vars.example .dev.vars # fill in base64 keys
uv run pywrangler dev # local: http://localhost:8787
uv run pywrangler deployCSS build:
public/app.cssis committed, but rebuild it withnpm run build:csswhenever you change Tailwind classes inpublic/index.htmlor editstyles/input.css(usenpm run watch:csswhile developing). Tailwind only emits classes it finds while scanningpublic/**/*.html, including those built dynamically in the inline<script>.
Two workflows live in .github/workflows/:
ci.yml(on every PR + push tomaster):npm ci, builds the CSS, fails ifpublic/app.cssis stale, then runspywrangler deploy --dry-runto confirm the Worker bundles. Needs no Cloudflare credentials.deploy.yml(on push tomaster+ manualworkflow_dispatch): builds the CSS and runsuv run pywrangler deploy. Merging a PR tomasterships it.
One-time setup before the first deploy works:
- Create a Cloudflare API token (My Profile → API Tokens → Edit Cloudflare Workers
template, scoped to this account/zone) and add it as a repo secret
CLOUDFLARE_API_TOKEN. Optionally addCLOUDFLARE_ACCOUNT_ID(only needed if the token can see more than one account). Settings → Secrets and variables → Actions.deploy.ymluses aproductionGitHub Environment — either create that environment (and put the secrets there, where you can also require reviewers) or remove theenvironment: productionline. - Set the Worker secrets once (they persist in Cloudflare, so the Action doesn't carry
them):
PRIVATE_KEYandEC_PRIVATE_KEYvianpx wrangler secret put …(see Configuration).
dev_server.py reuses the exact src/tokens.py logic and the local *.pem files, so
you can iterate on the UI without wrangler/Node:
uv venv && uv pip install scitokens PyJWT cryptography
python3 dev_server.py # http://localhost:8787The committed public/app.css is served as-is, so this needs no Node. If you change
Tailwind classes, rebuild it with npm run build:css (or run npm run watch:css alongside).
- Verification uses the local public key. The
scitokenslibrary normally fetches the issuer JWKS over the network and caches it in on-disk sqlite — neither is available on the Workers (Pyodide) runtime. Instead the Worker reads the token header'salg/kidand passes the matching local public key toSciToken.deserialize(..., public_key=...). As a result only the demo issuer (https://demo.scitokens.org) is verifiable; external issuers (e.g.cilogon.org) are not. - The device-code flow is stateless. Because this is a demo whose only job is to sign
tokens,
/oauth2/tokenalways issues and the codes are constant — no Redis/KV needed. cryptographyis provided by the Pyodide runtime;scitokensandPyJWTare pure-Python and vendored automatically on deploy.
This project is forked from https://jwt.io/.