# Kit launch-token verification examples

Backend modules for native WebView Kits. They verify platform-signed identity and
consume a server-issued, pre-session-bound nonce atomically before returning claims.
They are not a complete HTTP application. You must supply the HTTP/CSRF/cookie and
session integration below. Unsigned `user.getProfile` data is display data, never
a credential. These examples do not implement independent Account Sign-in.

## Run offline tests

Node 22.13 or later (uses `node:sqlite`, experimental on Node 22):

```sh
npm ci
npm test
```

Python 3.10 or later with SQLite 3.35 or later:

```sh
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt
.venv/bin/python -m unittest -v test_verification.py
```

Tests generate temporary EC keys and temporary SQLite databases; no production
secrets, network, or account are used. Dependencies must be installed first.

## Integrate with your backend

Choose a fixed product issuer (`https://ims.buko.app` or `https://ims.koee.app`), your
Kit ID and exact HTTPS origins in backend configuration. Never accept those values,
a key, or a JWKS URL from the request or JWT. Instantiate one verifier per Kit per
process so the JWKS cache is reused. The configured URL is
`<issuer>/kit-keys/<kit_id>/jwks.json`; HTTPS redirects fail closed.

Node:

```js
import { NonceStore, createKitVerifier } from './verification.mjs';
const nonces = new NonceStore('/private-data/kit-login.sqlite');
const verifier = createKitVerifier({
  issuer: 'https://ims.koee.app',
  kitId: 'com.example.shop',
  allowedOrigins: ['https://shop.example.com'],
  nonceStore: nonces,
});
// In the nonce route, after checking origin/CSRF and creating/reading your secure cookie:
const nonce = nonces.issue(preSessionIdFromServerCookie);
// In the exchange route, after checking origin/CSRF and reading the same cookie:
const identity = await verifier.verify(tokenFromRequestBody, preSessionIdFromServerCookie);
// Now create your own new authenticated session keyed by [identity.iss, identity.sub].
```

Python:

```python
from verification import NonceStore, KitVerifier
nonces = NonceStore('/private-data/kit-login.sqlite')
verifier = KitVerifier('https://ims.koee.app', 'com.example.shop',
                       ['https://shop.example.com'], nonces)
nonce = nonces.issue(pre_session_id_from_server_cookie)
identity = verifier.verify(token_from_request_body, pre_session_id_from_server_cookie)
# Only now create a new authenticated session keyed by (identity['iss'], identity['sub']).
```

For another product use its configured issuer. Never use name, handle or profile
`sub` supplied separately by the browser to choose the signed-in account.

Implement these route responsibilities:

1. Serve your site and both backend routes on your configured HTTPS origin. Use
   POST, verify the exact HTTP `Origin` and your framework's CSRF protection,
   reject cross-origin requests, rate-limit, and return `Cache-Control: no-store`.
   Limit request body/token size; redact tokens/nonces from logs and telemetry.
2. Create a fresh cryptographically random pre-session ID (at least 32 random
   bytes), set it in an HttpOnly, Secure, SameSite cookie, and bind issuance and
   exchange to that cookie. Never take the pre-session ID from the request body,
   query string or an attacker-chosen existing identifier. Avoid accepting a
   shared/global nonce: that permits login CSRF. Rotate the pre-session after
   successful authentication; generate a new authenticated session ID.
3. The nonce route returns `nonces.issue(cookieValue)`. The page sends that nonce
   to `KitBridge.call('auth.getLaunchToken', { nonce })`, then posts the returned
   `token` to your exchange route in the same pre-session. Do not put credentials
   in a URL. The examples require their own 43-character random nonces even
   though the bridge accepts other nonce formats.
4. Call `verify` and only on success create your own server session. Never catch
   verification/database/network errors and continue authentication. Use a
   generic unauthorized response; do not return exception diagnostics or token
   contents to clients. Render signed display fields as text, not HTML.
5. Store the SQLite file in a private persistent directory. The example uses a
   single SQL `DELETE ... RETURNING` to atomically verify the pre-session binding,
   deadline and consume the nonce. Multiple connections/processes using **the
   same file** are safe. For multiple hosts/serverless instances replace it with
   one shared transactional SQL store or an atomic Redis script; independent
   local files or a read-then-delete sequence are not safe. Securely clean up
   expired rows and protect the database file; do not expose it as a web asset.

The verifiers enforce ES256, dedicated `typ`, nonempty `kid`, exact issuer and
single audience, allowed origin, mandatory profile scope/claims, integer JWT
seconds, positive lifetime no greater than 300 seconds, and strict expiry
(`now_ms >= exp * 1000` rejects). Only `iat` allows up to 60 seconds into the future;
there is no global clock leeway. They check expiry after key fetching and after
nonce-store contention. Keep backend clocks synchronized. The bridge's
`expiresAt` is milliseconds and is exactly `exp * 1000`; never parse it as JWT seconds.

JWKS whole-set cache is at most 300 seconds. Unknown `kid` causes at most one
refresh per lookup (Node also rate-limits refreshes with a 30-second cooldown).
Only the configured endpoint is fetched. Python per-key permanent caching is
explicitly disabled. On a key-revocation notice call `refreshKeys()` (Node) or
`refresh_keys()` (Python) in all verifier processes. Key refresh/network failures
reject authentication; they never enable stale-key fallback past cache lifetime.
A cached revoked key may remain usable up to five minutes. Normal rotation keeps
old public keys for 24 hours; this does not lengthen token expiry.

These examples choose nonce binding. An alternative protocol may instead consume
`(iss, aud, jti)` atomically in a shared replay store with expiry at or after token
`exp`, rejecting duplicates. Do not simply omit nonce checks from these examples;
request binding/login-CSRF and replay rules must be designed for that protocol.

Platform consent/access revocation stops **new** profile and launch-token reads;
already signed tokens may verify for the rest of their 300-second lifetime.
Offline verification cannot inspect the user's current platform consent. Your
sessions do not automatically expire on platform revocation or `auth.changed`;
handle that event in the page, clear your browser state and call your own logout
route, and require a fresh platform token before sensitive actions. An event is
best-effort notification, not a backend revocation webhook or security guarantee.

Library references: [jose remote JWKS](https://github.com/panva/jose/blob/main/docs/jwks/remote/functions/createRemoteJWKSet.md)
and [PyJWT usage](https://pyjwt.readthedocs.io/en/2.10.1/usage.html).
