Lab · 07 / 16
When there is no user.
A nightly job needs data from another service. Nobody types a password: the service proves its own identity and gets a narrowly scoped token.
- Service
- a backend job, no user involved
- IdP
- issues tokens to clients
- API
- the protected resource
- POST /token
Step 1 of 5
Token request with a secretToken request with a signed assertion
In plain words The service shows a password of its own, like a staff code at the door.The service signs a short note with its private key: nothing reusable travels on the wire.
The service authenticates with client_id and client_secret over TLS. Simple, but the secret must be stored, rotated and never logged.The service signs a short-lived JWT with its private key. The IdP verifies it with the registered public key. No shared secret to leak.
POST https://idp.example.com/token Content-Type: application/x-www-form-urlencoded grant_type=client_credentials &scope=invoices:read &client_id=billing-job &client_secret=••••••••••••&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer &client_assertion=eyJhbGciOiJQUzI1NiJ9.eyJpc3MiOiJiaWxsaW5nLWpvYiIsImF1ZCI6Imh0dHBzOi8vaWRwLmV4YW1wbGUuY29tL3Rva2VuIn0.… - access_token
Step 2 of 5
A token for the service
In plain words The service gets a temporary pass that opens one door only.
Only an access token comes back: no ID token, because there is no user, and usually no refresh token, because the service can just ask again.
eyJhbGciOiJSUzI1NiIsInR5cCI6ImF0K2p3dCIsImtpZCI6IjIwMjYtMTAifQ.eyJpc3MiOiJodHRwczovL2lkcC5leGFtcGxlLmNvbSIsInN1YiI6ImJpbGxpbmctam9iIiwiY2xpZW50X2lkIjoiYmlsbGluZy1qb2IiLCJhdWQiOiJodHRwczovL2FwaS5leGFtcGxlLmNvbSIsInNjb3BlIjoiaW52b2ljZXM6cmVhZCIsImV4cCI6MTc2MDAwMDkwMCwiaWF0IjoxNzYwMDAwMDAwfQ.c2lnbmF0dXJlLW9mLWV4YW1wbGUAccess token claims
iss"https://idp.example.com"The IdP that issued it.
sub"billing-job"For client credentials, the subject is the service itself, not a person.
client_id"billing-job"Which registered client asked for the token.
aud"https://api.example.com"The API this token is valid for. Other APIs must reject it.
scope"invoices:read"Only what the job needs: read invoices, nothing else.
exp1760000900Short lifetime: 15 minutes. The job simply asks again.
iat1760000000When it was issued.
- GET /invoices · Bearer
Step 3 of 5
Calling the API
In plain words The service shows the pass at the door of the API.
The token travels in the Authorization header. It is a bearer token: whoever holds it can use it, so it must never end up in logs or URLs.
GET https://api.example.com/invoices?since=2026-10-01 Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6ImF0K2p3dCJ9.… - GET /jwks (cache)
Step 4 of 5
The API fetches the public keys
In plain words The door keeps a copy of the official stamp to compare passes against.
To check the signature, the API downloads the IdP’s public keys once and caches them, picking the key by kid. After that, validation happens locally without calling the IdP on every request.
{ "keys": [ { "kty": "RSA", "kid": "2026-10", "use": "sig", "alg": "RS256", "n": "0vx7agoebGcQSuu…", "e": "AQAB" } ] } - 200 OK
Step 5 of 5
Access granted, within scope
In plain words The pass is genuine and valid for this door, so it opens.
Signature, iss, aud, exp and scope all check out. A request to write invoices with the same token would get 403: the scope only allows reading.
Flow complete.
Example domains and values. Nothing leaves your browser.