Skip to content
anchr.ing
Guide

Machine-to-machine with client credentials

A confidential service app gets tokens for itself with the client_credentials grant; your API verifies them like any other token, within their ceilings.

A backend job or another service calls your API as itself, with no user. Register an app of kind service (machine to machine, client credentials) with your API's audience. It is confidential: copy its client secret into the service's environment, never into code.

worker/token.ts
// A machine token (client_credentials): the app calls your API as itself.
const res = await fetch('https://acme.test.anchr.ing/token', {
  method: 'POST',
  headers: { 'content-type': 'application/x-www-form-urlencoded' },
  body: new URLSearchParams({ grant_type: 'client_credentials', client_id: 'test_…', client_secret: process.env.CLIENT_SECRET! }),
});
const { access_token, expires_in } = await res.json();

await fetch('https://api.acme.example/api/reports', { headers: { authorization: `Bearer ${access_token}` } });

Tokens from the client_credentials grant have the app's client id as sub and no session. Request a new one when expires_in runs out.

In your API

// In the API: the same middleware verifies it (typed 'apiKey', sub = the client id).
app.use('/api/*', auth.middleware());
app.get('/api/reports', (c) => {
  const machine = c.var.user.claims.client_id === c.var.user.sub;
  return c.json(listReports(machine ? undefined : c.var.user.sub));
});

The middleware types machine tokens apiKey, like a personal access token, so tokens.apiKey permission ceilings and the token's scopes limit them. maxSessionAge and requireMfaWithin do not apply (there is no sign-in); limit machine callers with scopes, ceilings and allowIps. /userinfo refuses these tokens: key your data by sub.

Every option, error and token type: the SDK README.