Zum Inhalt springen

Login-Token: Spezifikation

A punchout launch redirects the browser to {shop_origin}{login_path}?token=<token>. The plugin verifies the token, logs the browser into the named customer account and redirects to next. This file is the contract; core/login_token.py is the reference implementation and verify_login_token the reference verifier.

pbl1.<b64url(payload)>.<b64url(signature)>

b64url is RFC 4648 §5 (-/_) without padding — reject =, +, / and any other character. payload is UTF-8 JSON; signature is 64 bytes of Ed25519 over the ASCII bytes of "pbl1." + <b64url(payload)> (the first two token parts including the dot, not the decoded JSON). Verify the signature against the raw 32-byte public key you were configured with; you never hold a private key and can never mint a token.

field value
iss always punchout-bridge
kid which public key signed this (e.g. k1) — a rotation ships a new kid while the old key stays configured
aud the tenant id (pob_live_…) this shop is onboarded as
iat, exp unix seconds; exp - iat is the TTL (120 s by default)
jti unique token id — the single-use marker
op always punchout_login
protocol oci, cxml or ids — which launch minted it (verifiers accept any non-empty value, so a future protocol needs no plugin change)
customer {"kind": "id"|"number"|"email", "value": "…"} — which shop customer to log in as, kind per the tenant’s configured customer_ref_kind
next relative path to redirect to after the login, e.g. /checkout/cart?__pob_sid=…

Canonical JSON. The payload is serialised with sorted keys, no whitespace and no ASCII escaping — Python’s json.dumps(obj, separators=(",",":"), sort_keys=True, ensure_ascii=False). Do not re-serialise the payload to check the signature: verify over the received b64url(payload) string exactly as it arrived, and read claims from the parsed object.

  1. format — pbl1. prefix, exactly three dot-separated parts, strict unpadded b64url, payload parses as a JSON object, every field above present and of the right type, customer.kind one of the three values.
  2. kid — pick the configured public key; unknown kid → reject. (kid selects a key; nothing else in the payload is trusted yet.)
  3. signature — Ed25519 verify; a failure ends it.
  4. iss == punchout-bridge, then op == punchout_login, then aud == your configured tenant id (a token minted for another shop must not work here).
  5. validity window — reject if now > exp + skew (expired) or now < iat - skew, with skew ≤ 60 s. Never widen this: the TTL is what bounds a stolen URL.
  6. single use — store jti (with exp for cleanup) and reject a jti you have already seen, atomically. A token is spent by the first request that presents it, successful or not.
  7. next — must start with / and not with // or /\ (both are off-site redirects), and must contain no backslash, whitespace or control character. Redirect only to a path that passes this; never to a value derived from the request.

Answer the login request with Cache-Control: no-store and Referrer-Policy: no-referrer, so the token is neither cached nor handed to the next page in Referer. Never log the token, the signature, the jti or customer.value; log a fixed reason word and nothing else. On any failure, fall back to a normal guest landing on next’s path — the punchout itself works without the login, and a hard error would break a working cart.

seed.txt (base64url of the 32-byte Ed25519 seed — test key, never a real one; it is sha256("punchout-bridge login-token test vector")), public_key.txt (what a plugin is configured with), token.txt (one token), claims.json (its decoded payload). The vector verifies for tenant pob_live_test0000000000000000 with kid test at any now between iat and exp; re-minting with those inputs must reproduce token.txt byte for byte.