Machine to machine
Let a service call your APIs as itself with the client credentials grant: clients and consumers, scopes and audience, token caching and credential rotation.
When no person is involved — a nightly job, a backend calling another backend, a partner’s integration — the caller authenticates as itself with the client_credentials grant (RFC 6749). It presents a client ID and a secret at the token endpoint and receives an access token that describes the service, not a user.
Set up the client
Create a confidential client
In Clients, create a client with client authentication on and enable the
client_credentialsgrant. A public client cannot use it: it has no secret to present. Copy the secret when it is shown — it is shown once.Give it the scopes the service needs
Allow the scopes on the client, or on a consumer if each integration should have its own credential and permissions.
Let it reach the API
Register the API as a resource server, such as
https://api.example.com, and add the client under its allowed clients. Tokens asked for withresourcethen carry that identifier asaud.
Consumers: one credential per integration
A consumer is an extra credential attached to a client, with its own scopes and roles. Give each system that calls you its own consumer and you can see, limit and revoke each one without touching the others. Create them in Consumers: pick the client, the type Client Secret, a value of 8 to 512 characters (or generate one), and the scopes and roles. The value is shown once, when the consumer is created or its value replaced; afterwards only its last four characters are, and an edit that leaves the value blank keeps it.
| Aspect | Client secret | Consumer |
|---|---|---|
| Sent as | client_id and the client’s secret. | client_id of the client and the consumer’s value as the secret. |
| Scopes | The client’s scopes. | Only the consumer’s scopes; nothing is inherited from the client. |
| Roles | None. | The consumer’s roles, in the consumer_resource_access claim. |
| Identified in the token by | azp. | azp, plus consumer_name, credential_id and credential_type. |
| Turned off by | Deactivating the client, which stops every integration. | Deactivating or suspending that consumer alone. |
A consumer’s credential works only for the client it belongs to, is presented with the client’s registered authentication method, and must be active. How many consumers you can keep active depends on your plan.
Consumer types
| Type | Presented as | Validated by |
|---|---|---|
| Client Secret | The client’s secret, with the client’s client_id, at the token endpoint. | Authio, which issues a client_credentials token for the consumer. |
| Personal Access Token | client_secret of the personal_access_token grant, with the client’s client_id. | Authio, which issues a token with sub = the consumer and azp = the client’s client_id. |
| API Key | As is, to your own API (for example in an x-api-key header you define). | Your API, at the realm’s introspection endpoint — see API keys. |
The type is fixed when the consumer is created: an update that leaves type out keeps it, and one that names another type is refused with ValidationFailed on type. For another type, create another consumer. Every consumer sign-in — a token issued or refused, a key accepted or refused — is one entry in the access log naming the consumer and the client, and one login_succeeded or login_failed webhook carrying consumer_id and consumer_name.
API keys
An API Key consumer is a key a partner sends to your API on every call, with no token exchange. Your API checks it at the realm’s introspection endpoint (RFC 7662), authenticated as the client the consumer belongs to — or as the client your resource server introspects with, when that is the same client — with token_type_hint=api_key. A key is never validated through the Admin API, and the Admin API does not accept it.
curl https://authio.api.hyperio.tech/realms/acme/protocol/openid-connect/introspect \
-u "acme-orders-api:$CLIENT_SECRET" \
-d token="$PARTNER_API_KEY" \
-d token_type_hint=api_keysubandcredential_idare the consumer;client_idis the client;scopeis every scope the consumer is allowed (optional ones included, since a key cannot ask for them), without the scopes meant for people.- A wrong key, another client’s key, or the key of an inactive or suspended consumer answers
{"active": false}. There is noexp: a key stands until the consumer is switched off or its value replaced, so introspect on every call or cache the answer for a few seconds. - Without
token_type_hint=api_keya value is only introspected as a token, so an API that expects tokens is never told a key is one. With the hint, a value that is no key is still introspected as a token, as RFC 7662 asks.
Request a token
curl -X POST https://authio.api.hyperio.tech/realms/acme/protocol/openid-connect/token \
-u 'acme-reports:YOUR_CLIENT_SECRET' \
-d grant_type=client_credentials \
-d scope=orders:read \
--data-urlencode resource=https://api.example.com-u sends the credential as HTTP Basic (client_secret_basic, the default). If the client is registered with client_secret_post, send client_id and client_secret as form fields instead: Authio accepts only the registered method. There is no refresh token and no ID token — there is no session to keep alive.
Which scopes and audience you get
- The credential’s default scopes are always granted; its optional scopes only when
scopenames them. - A requested scope the credential does not hold is left out of the token, not refused — check
scopein the response. - Scopes meant for people —
openid,profile,email,phone,address,offline_access— never enter a machine token. - With
resource,audis that resource server and the server’s scopes you ask for are added. A resource the client may not reach is refused withinvalid_target. - Without
resource,audis the client’s audience —Authioby default — which your API should not accept.
The token’s sub is the client’s internal ID and azp its client ID, so an API can admit one machine client by azp — RequireAuthioClient does it in .NET.
Cache tokens
Do not ask for a token per call. Keep it until shortly before expires_in runs out, then ask for a new one: each issuance is a round trip to Authio, is recorded in the access log and counts as client-credentials usage. Cache one token per combination of scopes and resource.
using Authio.Client.Extensions;
builder.Services.AddAuthioClient(builder.Configuration.GetSection("Authio"));
builder.Services
.AddHttpClient<OrdersApi>(client => client.BaseAddress = new Uri("https://api.example.com"))
.AddAuthioClientCredentials(options =>
{
options.Scopes = ["orders:read"];
options.Resource = "https://api.example.com";
});In .NET, AddAuthioClientCredentials does all of it: the token is cached per scopes and resource, renewed TokenRefreshSkew (one minute) before it expires, shared by concurrent callers, and fetched again once if the API answers 401. The full setup is in .NET.
Rotate credentials
Rotate a client’s secret from the client’s view in Clients, and an account credential — an API key, a personal access token or an account client secret — from its view in API secrets. The new value is shown once. The rotation asks why you are rotating, and the answer decides what happens to the value it replaces:
| You choose | The previous value | Tokens issued with it |
|---|---|---|
| Routine rotation, no overlap | Refused from the next call. | Keep working until they expire. |
| Routine rotation, kept working for 15 minutes, an hour or a day | Still accepted until the period ends, then refused. The console offers only the periods the environment allows. | Keep working until they expire. |
| The current value leaked | Refused from the next call. | Access tokens issued before the rotation turn inactive at introspection and are refused by Authio’s own APIs, and their refresh tokens stop working. For a client’s secret, people signed in to the client sign in again. |
- Without downtime. Choose an overlap and deploy the new secret to every instance while the old one still works; the client’s view says until when it does. Or use consumers: create a new consumer with the same scopes and roles, deploy the service with its value, then deactivate the old consumer.
- After a leak. Choose The current value leaked: no overlap is offered, because a leaked value has to stop at once.
- Cut off issued tokens too. Turn on Revoke Issued Tokens Immediately in the client’s security settings. Introspection then answers
active: falsefor a token whose credential was deactivated, revoked, expired or deleted — APIs that only verify signatures locally do not see it, so they need introspection.
Troubleshooting
| What you see | Why | What to do |
|---|---|---|
401 invalid_client | Wrong secret, an inactive consumer, or the secret sent by a method other than the registered one. | Check the value, and use Basic or form fields as the client is registered. |
unauthorized_client | The client_credentials grant is not enabled on the client. | Enable it in the client’s grant types. |
invalid_target | The resource is not a resource server the client may reach. | Check the identifier and add the client to the resource server’s allowed clients. |
| The token lacks a scope you asked for | The credential does not hold it, so it was left out. | Allow the scope on the client or on the consumer. |
| The API refuses the token’s audience | The token was asked for without resource. | Send resource with the API’s identifier. |