M2M call from a backend worker
Server-to-server OAuth2 with client credentials grant
A backend worker needs to call your API. No user is involved. Use OAuth2 client_credentials grant.
Step 1: Register an M2M client in Athena
Athena IAM → M2M Clients → New Client:
- Name:
nightly-data-sync - Allowed scopes:
data:read,data:write - Grant type:
client_credentials
Athena returns a one-time-displayed client_secret. Capture it now; it's never shown again. (To rotate later: see Operate, M2M client secret rotation.)
Step 2: Worker requests a token
Each time the worker needs to call your API:
async function getMachineToken() {
const response = await fetch(`${ISSUER}/oauth2/token`, {
method: 'POST',
headers: {
'content-type': 'application/x-www-form-urlencoded',
'authorization': 'Basic ' + Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString('base64'),
},
body: new URLSearchParams({
grant_type: 'client_credentials',
scope: 'data:read data:write',
}),
});
const { access_token, expires_in } = await response.json();
return { access_token, expires_at: Date.now() + expires_in * 1000 };
}Step 3: Cache the token
Don't get a new token for every request, cache it until expiry:
let cached: { access_token: string; expires_at: number } | null = null;
async function getCachedMachineToken() {
if (!cached || cached.expires_at < Date.now() + 60_000 /* refresh 1min before expiry */) {
cached = await getMachineToken();
}
return cached.access_token;
}Step 4: Use the token
const token = await getCachedMachineToken();
const result = await fetch('https://api.your-domain/data', {
headers: { authorization: `Bearer ${token}` },
});What the token contains
The M2M access token has:
sub= client_id (not a user!)scope= granted scopes- No
email, noname, there's no user. client_id= same assub.
Your API validation should accept tokens where sub is a client ID for M2M flows. Distinguish from user tokens via the presence/absence of email or by introspection's token_type: "access_token" and client_credentials indication.
Rate limiting
M2M tokens can be issued aggressively. Set metadata.rate_limit_per_minute on the client and check at your API:
hydra update client <id> --endpoint http://localhost:3103 \
--metadata '{"rate_limit_per_minute": 600}'