Authentication

Service Client Authentication using OAuth 2.0 Client Credentials (private_key_jwt, RFC 7523). No shared secrets — authenticate by signing a JWT with your private key.

How it works

  1. Register a service client in the admin dashboard and receive a client_id
  2. Get an access token — sign a JWT with your private key, exchange it at /api/oauth/token, and call any API endpoint using the token as a Bearer token

Register a service client

A tenant admin registers service clients through the admin dashboard. Navigate to Integrations → API Clients and click Create Client.

During registration you provide:

  • Name — a human-readable label for this integration
  • Description — optional description of the integration
  • Permissions — the scopes this client may request

The system automatically generates an ES256 key pair. After creation, the dashboard displays your client_id, key_id, and private key in JWK format (a single-line JSON string). Save the private key immediately — it cannot be retrieved again. Only the public key is stored on the server.

Step 1 — Get an access token

Build a short-lived JWT assertion signed with your private key and exchange it for an access token. The assertion replaces a client_secret — only you can produce this signature. POST the assertion to the token endpoint using application/x-www-form-urlencoded encoding (not JSON).

Required JWT claims:

  • iss — your client_id (e.g. svc_a1b2c3d4e5f6)
  • sub — same as iss
  • aud https://abyrgverslun.is/api/oauth/token
  • jti — a unique UUID for each request (replay prevention)
  • exp — short expiry, max 5 minutes from iat (60s recommended)

Store your private key as an environment variable. The key is in JWK format — a single-line JSON string that is natively .env-friendly:

PRIVATE_KEY='{"kty":"EC","crv":"P-256","x":"...","y":"...","d":"...","alg":"ES256","kid":"your-key-id"}'

No newline escaping needed. Avoid committing key files to source control.

# 1. Create a client assertion (requires step-cli)
# Convert JWK to PEM first (one-time):
#   step crypto key format --jwk --pem < private-key.jwk > private.pem
#
# macOS:
EXP=$(date -v+60S +%s)
# Linux:
# EXP=$(date -d '+60 seconds' +%s)

ASSERTION=$(step crypto jwt sign \
  --iss svc_a1b2c3d4e5f6 \
  --sub svc_a1b2c3d4e5f6 \
  --aud https://abyrgverslun.is/api/oauth/token \
  --kid your-key-id \
  --key private.pem \
  --jti $(uuidgen) \
  --exp $EXP)

# 2. Exchange for access token
curl -s -X POST https://abyrgverslun.is/api/oauth/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials\
&client_id=svc_a1b2c3d4e5f6\
&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer\
&client_assertion=$ASSERTION\
&scope=devices:read transactions:read" | jq .

Response

POST/api/oauth/token

Exchange a signed client assertion for an access token.

Headers

Content-TypestringRequired

application/x-www-form-urlencoded

Request Body

grant_typestringRequired

Must be client_credentials

client_idstringRequired

Your service client ID (svc_xxx)

client_assertion_typestringRequired

urn:ietf:params:oauth:client-assertion-type:jwt-bearer

client_assertionstringRequired

The signed JWT assertion

scopestringOptional

Space-separated list of requested scopes — must be a subset of registered scopes

Response

{
  "access_token": "eyJhbGciOiJFUzI1NiIsInR5cCI6ImF0K2p3dCJ9...",
  "token_type": "Bearer",
  "expires_in": 300,
  "scope": "devices:read transactions:read"
}

Step 2 — Use the access token

Include the token in the Authorization header of every API request. Tokens expire in 5 minutes. No refresh tokens are issued — when it expires, create a new assertion and request a fresh token.

POST/api/verification/pass

Example authenticated request for age verification. Verification outcomes return HTTP 200 with result pass/fail.

Headers

AuthorizationstringRequired

Bearer <access_token>

Content-TypestringRequired

application/json

Request Body

passDatastringRequired

Base64-encoded 64-byte pass (legacy 64-char Latin-1 also accepted)

requiredAgenumberRequired

Minimum age threshold to check (for example, 18)

Examples

cURL

ACCESS_TOKEN="eyJhbGciOiJFUzI1NiIsInR5cCI6ImF0K2p3dCJ9..."

curl -s -X POST https://abyrgverslun.is/api/verification/pass \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "passData": "<base64-or-latin1-pass>",
    "requiredAge": 18
  }' | jq .

Response

{
  "result": "pass",
  "transactionId": "j57b2mNkR4e9..."
}

Available permissions

Request only the scopes your integration needs. All resources support both read and write operations.

PermissionDescription
devices:readList and read device details
devices:writeCreate, update, and delete devices
locations:readRead location information
locations:writeCreate, update, and delete locations
employees:readRead employee records
employees:writeCreate, update, and delete employee records
transactions:readRead verification transaction records (read-only)
transactions:writeCreate verification transactions (age verification)

Key rotation

Rotate keys without downtime using the Rotate Keys action on the client detail page in the dashboard. A new key pair is generated automatically.

The old key transitions to retiring status for a 24-hour grace window before moving to retired. Update your signing key during the grace window to avoid interruption.

Instant revocation

An admin can instantly invalidate all active tokens for a client via the Revoke All Tokens action in the dashboard. This sets tokensInvalidBefore to the current timestamp. Any token issued before that timestamp is immediately rejected on next use — no token blacklist required.

Security best practices

  • No shared secrets — the server stores only public keys; a database breach does not expose client credentials
  • Short-lived assertions — assertions expire in 60s; combined with JTI replay prevention, intercepted assertions cannot be reused
  • Scope restriction — only request the permissions your integration actually needs
  • No refresh tokens — eliminates refresh token theft as an attack vector
  • Rotate keys without downtime — use the key rotation action and the 24-hour retiring grace window
  • Instant revocation — use tokensInvalidBefore to immediately revoke all tokens if a client is compromised