Service tokens (machine / kiosk)

Service tokens are admin-minted JWTs for clients that cannot complete an interactive login: TV displays, kiosks, cron jobs, integration scripts.

They reuse the same ES256 keys and JWKS as human access tokens, carry the existing RBAC of one application (aud + permissions), and can be revoked via RFC 7662 introspection.

Issuerhttps://admin.fixweb.cloud
JWKShttps://admin.fixweb.cloud/.well-known/jwks.json
Introspecthttps://admin.fixweb.cloud/introspect
Admin consolehttps://admin-admin.fixweb.cloud

When to use (and when not)

Use a service token when there is no human at the keyboard and the client must call an API as that application.

Do not use one for a browser SPA that can run authorization code + PKCE — human tokens remain the right path (refresh, per-user grants, logout).

Create (tenant admin)

  1. Open Apps → select the application → Service tokensCreate.
  2. Pick a name, TTL (default 365 days; optional « no expiration »), and

permissions from the app's catalogue. app:{id}:access is always included.

  1. Copy the JWT once — it is never shown again and never stored server-side.

API equivalent:

POST /api/admin/service-tokens
Authorization: Bearer <admin access token>
Content-Type: application/json

{
  "name": "lobby-tv",
  "application_id": "jarvis",
  "permissions": ["app:jarvis:access"],
  "expires_in_days": 30
}

expires_in_days: null mints a token without exp. Prefer a finite TTL.

Token shape

{
  "iss": "https://admin.fixweb.cloud",
  "aud": "<application_id>",
  "sub": "svc:<token-id>",
  "jti": "<token-id>",
  "mode": "access",
  "type": "service",
  "iat": 1710000000,
  "exp": 1741536000,
  "properties": {
    "tokenID": "<token-id>",
    "name": "lobby-tv",
    "application_id": "<application_id>",
    "permissions": ["app:jarvis:access"],
    "apps": ["<application_id>"]
  }
}

iss is always the IdP's public issuer (ISSUER), never the admin console host.

Verify in your backend

  1. Fetch JWKS from https://admin.fixweb.cloud/.well-known/jwks.json.
  2. Verify signature (ES256), iss, aud (= your app id), type === "service".
  3. Read properties.permissions.

Checking aud is mandatory: a service token minted for another app on the same IdP must not authorize against your permission names.

See Integration for library samples (adapt type / aud).

Introspection (revocation)

POST /introspect (application/x-www-form-urlencoded, field token).

Authenticate with either:

OAuth clients), or

Response when active:

{
  "active": true,
  "sub": "svc:…",
  "aud": "jarvis",
  "exp": 1741536000,
  "jti": "…",
  "application_id": "jarvis",
  "name": "lobby-tv",
  "permissions": ["app:jarvis:access"]
}

Otherwise { "active": false } with no reason (RFC 7662). Cache for ~60 s so revocation is effective within a minute without hammering the IdP.

Revoke from the console or:

POST /api/admin/service-tokens/:id/revoke
Authorization: Bearer <admin access token>

Kiosk / URL pattern

Example: https://leaderboard.example.com/?token=<JWT> → front reads ?token= and calls the API with Authorization: Bearer <JWT>.

Precautions:

  1. One token per device — so you can revoke a single kiosk.
  2. Short TTL when the token lives in a URL.
  3. Strip the query after read (history.replaceState) — query strings land

in browser history, CDN logs, and the Referer header.