Machine Clients
Give one of your own systems a credential, roles at your nodes, and a token it obtains for itself, so your API can authorize a machine the way it authorizes a person.
When to use one
A machine client is an OAuth client that is a principal of its own. A web client signs a person in and the token it receives is that person's; a machine client holds roles at your nodes, acts inside at most one organization, and obtains a token for itself with the client_credentials grant. Use one wherever a program calls your API with no person present and you want it held to the same permissions as everyone else: a backend service, a scheduled job, or a customer's own integration pinned to that customer's organization. It is not an API key, which manages Canopy; a machine client authenticates a machine to your API, and your API verifies its token exactly as it verifies a user's.
The pieces
Four things make a machine client work. The first two are set when it is registered; the roles and audiences can change at any time.
Register from the Console
The Console registers a machine client in the same dialog as a web client and manages its roles from its drawer.
- Application
- Developer Console
- Path
- Access ControlIntegrationsOAuth Clients
- Opens with
oauth_clients.viewRead the Environment's OAuth clients.
- Context
- Select an Application and an Environment in the switcher first; the page belongs to that Environment.
client_secret before Done; it is shown once.Register and give roles via the API
The same operations are on the public API, keyed to one Environment by the API key: oauth_client.manage to register and to give, change or remove roles, oauth_client.view to read. The caller must also be allowed to grant the role at that node, as for a person.
Register a machine client
curl -X POST https://auth.canopy-io.com/api/v1/oauth-clients \ -H "X-API-Key: $CANOPY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Billing worker", "client_kind": "machine", "allowed_audiences": ["https://api.example.com"] }' # 201 Created # { "data": { "id": "…", "client_id": "…", "client_secret": "…", "client_kind": "machine", "organization_node_id": null, "allowed_audiences": ["https://api.example.com"], "grant_types": ["client_credentials"] } }
Give it a role at a node
curl -X POST https://auth.canopy-io.com/api/v1/oauth-clients/$CLIENT_ID/assignments \ -H "X-API-Key: $CANOPY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "role_id": "3f1c9b2e-7a4d-4e8f-9b6a-2d5c8e1f0a3b", "node_id": "7d2a4c6e-1b3f-4a5d-8e9c-0f1a2b3c4d5e" }' # 201 Created # { "data": { "id": "…", "oauth_client_id": "…", "identity_id": null, "role_id": "3f1c9b2e-7a4d-4e8f-9b6a-2d5c8e1f0a3b", "application_node_id": "7d2a4c6e-1b3f-4a5d-8e9c-0f1a2b3c4d5e" } }
Add organization_node_id at registration to pin the client to an organization; a role outside it is refused with 409 oauth.client_outside_organization. GET /api/v1/oauth-clients/:id/assignments lists its roles, PATCH and DELETE on …/assignments/:assignmentId change and remove them, with If-Match carrying the assignment's version.
Obtain a token
The client posts its credentials to the token endpoint with grant_type=client_credentials. audience is optional when the Environment has a default; otherwise it must name one the client lists.
curl -X POST https://auth.canopy-io.com/oauth/token \ -u "$CLIENT_ID:$CLIENT_SECRET" \ -d grant_type=client_credentials \ -d audience=https://api.example.com # 200 OK # { "access_token": "…", "token_type": "Bearer", "expires_in": 300, "scope": "permissions" }
No refresh token and no ID token: nobody signed in, and the client simply asks again when its token expires. A client may obtain at most 60 counts tokens a minute; reuse a token until it expires.
What the token carries
The access token is signed with the Environment's keys and carries:
Verify and authorize it
Verify the token as you verify a user's (Hosted Login, Verifying tokens): the Environment's issuer, its JWKS, RS256, exp, and aud equal to the audience your API expects. Then branch on type: a route meant for people accepts identity only; a route meant for machines accepts service. Authorize from the token's permissions, or read the client's grants from GET /api/v1/oauth-clients/:id/grants with the sub as the id: one page of (permission, node_id) pairs with model_version, the same shape an identity's grants take, so a local evaluator treats both alike. A deactivated client answers an empty page.
With @canopy-io/nestjs, admit the token on the routes built for machines:
// The module: hold service tokens to the audience your API answers to.
CanopyModule.forRoot({
apiKey: process.env.CANOPY_API_KEY,
verify: {
environmentId: process.env.CANOPY_ENVIRONMENT_ID,
serviceAudience: 'https://api.example.com',
},
resolveIdentity: (request) => request.canopyToken?.sub,
});
// The route: a machine client's token is admitted here, and only here.
@Controller('orders')
@UseGuards(CanopyTokenGuard, CanopyGuard)
export class OrdersController {
@Post('import')
@AllowServiceTokens()
@RequirePermission('orders.import', { scope: 'org' })
importOrders() {}
}CanopyTokenGuard refuses a service token with 401 on every route without @AllowServiceTokens(), so a route built for people never admits a machine by accident. On a route that carries it, CanopyGuard reads the token's type off the verified claims and evaluates the client's own grants, read from GET /api/v1/oauth-clients/:id/grants and cached as a person's are; scope: "org" evaluates at the organization the client is pinned to. Without NestJS, @canopy-io/node does the same: TokenVerifier takes serviceAudience and verify(token, { alsoAllow: ["service"] }), and LocalAuthorizer takes principal_type: "service" (SDKs and Libraries).
Revocation
Rotating the secret, deactivating the client, removing a role or deleting the client takes effect at the next token request: the client gets nothing new, or a token without the role. A token already issued keeps working until it expires, at most the Environment's Machine token lifetime; set it short where that matters. Deleting a client deletes its roles with it.
Billing
A machine client that obtained a token this month counts as one active machine credential, beside your API keys, under the plan's monthly active machine credentials allowance (Billing). It never counts as a monthly active user.
Errors
The token endpoint answers in the OAuth error shape, { error, error_description }:
| Error | Meaning |
|---|---|
invalid_client | The client id or secret is wrong, or the client is deactivated. 401. |
unauthorized_client | A web client presented client_credentials; only a machine client may. |
invalid_target | The audience is neither the Environment's default nor one the client lists. |
invalid_request | No audience was sent and the Environment has no default; or, with status 429 and a Retry-After header, the client asked for too many tokens in a minute. |
invalid_scope | The scope names one the client does not carry. |
Tell us how we can improve this guide.