githubEdit

S2S JWT Auth Contract (Hyper Lite -> Hyper Core)

Status: Updated for CORE-2005 (March 13, 2026)

Purpose

This document defines the machine-to-machine JWT contract for Hyper Lite server calls into Hyper Core integration endpoints.

Token header

Required protected header fields:

  • alg: RS256

  • typ: JWT

  • kid: active signing key id

Claims

Required claims:

  • iss

  • sub

  • aud

  • exp

  • iat

  • nbf

  • scope

  • jti

Claim conventions:

  • scope uses a space-delimited string (OAuth-style), for example "spaces:create join_tokens:issue".

  • Supported scopes for CORE-2003:

    • spaces:create

    • join_tokens:issue

  • Maximum TTL: 300 seconds.

  • Clock skew tolerance: 60 seconds.

Auth response mapping

  • 401 Unauthorized

    • missing/invalid/expired token

    • malformed token/header/claims

    • unknown kid

    • invalid iss or aud

    • replayed jti (see Replay protection)

  • 403 Forbidden

    • signature/claims are valid, but required scope is missing

Shared executable test vectors

Canonical vectors live at:

  • docs/contracts/s2s-jwt-auth-test-vectors.json

The vectors include:

  • valid tokens for each required scope

  • unknown kid

  • invalid aud

  • missing scope

  • expired token

  • not-yet-valid token

  • malformed token

  • insufficient scope (403)

Both repos should treat this file as the compatibility source of truth for auth behavior.

Replay protection

Each S2S JWT must carry a unique jti (JWT ID) claim. Hyper Core enforces single-use semantics:

  • On first presentation the jti is recorded in session.s2s_jwt_replay.

  • A second request bearing the same jti is rejected as 401 Unauthorized (reason: replayed_token).

  • Records are retained for 24 hours after the token's exp timestamp.

  • Expired records are purged every 5 minutes.

Hyper Lite generates a unique jti (nanoid) per S2S request, so replay rejection should never occur under normal operation. A spike in replayed_token rejections indicates either a network-level replay attack or a bug causing token reuse.

Key rotation procedure

kid naming convention: lite-YYYY-MM (e.g., lite-2026-03).

  1. Generate a new RSA keypair and assign a new kid.

  2. Add the new public key to Core's HYPER_S2S_JWT_PUBLIC_KEYS_JSON keyset (both old and new kid present).

  3. Deploy Core — it now accepts tokens signed with either key.

  4. Update Hyper Lite's HYPER_S2S_JWT_PRIVATE_KEY_PEM and HYPER_S2S_JWT_KID to the new key.

  5. Deploy Hyper Lite — during rolling deploy some workers sign with the old key, some with the new. Core accepts both.

  6. After all Hyper Lite workers are updated (verify via metrics showing only the new kid), remove the old public key from Core's keyset.

  7. Deploy Core again.

Maximum dual-key window: Hyper Lite deploy rollout time + 300 s max token TTL.

To revoke a key immediately: remove it from Core's keyset and deploy. In-flight tokens signed with the revoked key will fail with 401 unknown_kid until they expire (at most 300 s).

Auth observability

Metrics

Hyper Core emits three counters via metrics::counter!(), forwarded to ClickHouse and admin dashboard:

Counter
Labels
Meaning

s2s_auth_success

reason, path, scope

Successful S2S JWT authorization

s2s_auth_401

reason, path, scope

Rejected — missing, invalid, expired, replayed, or unknown-kid token

s2s_auth_403

reason, path, scope

Rejected — valid token but missing required scope

Notable reason values for s2s_auth_401:

  • replayed_tokenjti already seen

  • unknown_kidkid not in keyset

  • expired_signature — token past exp + clock_skew

  • invalid_issuer, invalid_audience — claim mismatch

  • missing_claim(X) — required claim absent

Where metrics surface

  • Prometheus: /metrics endpoint (all counters available for scraping).

  • ClickHouse: forwarded via ServerMetricsForwarder for historical queries.

  • Admin dashboard: live feed via broadcast channel.

Alert conditions to configure

Condition
Likely cause
Investigation

s2s_auth_401 spike, reason=replayed_token

Replay attack or token-reuse bug

Check Hyper Lite jti generation, network replay indicators

s2s_auth_401 spike, reason=unknown_kid

Key rotation misconfiguration or stale deploy

Verify Core keyset contains expected kid values

s2s_auth_403 spike

Scope misconfiguration on Hyper Lite

Check HYPER_S2S_JWT_SCOPE_* env vars

s2s_auth_success drops to zero

Integration broken

Check Hyper Lite logs at meetings.hypervideo.auth, verify connectivity

Hyper Lite side

  • Structured logs at logger category meetings.hypervideo.auth capture S2S auth rejections with actor context, endpoint, scope, and Hypervideo request ID.

  • ClientErrorObservabilityReporter captures client-side join token refresh failures in the browser.

  • Both flow through the log-tail worker to Analytics Engine for querying.

Operational notes

  • Signing key material remains private to Hyper Lite.

  • Hyper Core should only receive/consume public keys keyed by kid.

  • Raw JWTs must never be logged.

Last updated