1. Docs
  2. Machine Clients

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.

The client: a name, a client_id and a client_secret shown once, with client_kind: "machine". It has no redirect URIs and nothing a person signs in with.Its organization (organization_node_id, optional): the one organization it acts in. Its tokens name that organization, and it may hold roles only at or beneath that organization's node. Leave it out and the client acts across the Environment. Set at registration and never changed.Its roles: assignments at nodes, written through the same grant writer as a person's roles and held to the same grant ceiling. A client with no role still obtains tokens; they carry no permissions.Its audiences (allowed_audiences): the values it may ask a token for with audience, beside the Environment's Default audience on the Authentication page. The token's aud names the one asked for, so the API that verifies it knows the token was meant for it. Up to 20 counts values.

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.view Read the Environment's OAuth clients.
Context
Select an Application and an Environment in the switcher first; the page belongs to that Environment.
In the Developer Console, open Access Control → Integrations → OAuth Clients and click Register OAuth Client. Set Kind to Machine. The invite, sign-in and redirect URI fields give way to the machine fields.Enter a Name. Where the Environment has organizations on, choose the Organization the client acts in, or leave Acts across the Environment. Add any Allowed audiences. Click Register and copy the client_secret before Done; it is shown once.The client is listed with a Machine badge. Open it: under Configuration, Roles lists what it holds. Click Add role, pick a role and the node it applies at, and an optional window. Each role can be changed or removed from the same list.On Tenant → Authentication → Tokens, set the Environment's Machine token lifetime and Default audience; see Set token lifetimes.

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:

type: service. A route built for people should refuse it; see below.sub: the client's client_id, and client_id beside it. gty: client_credentials.aud: the audience the client asked for, or the Environment's default. Verify it names your API.permissions: the keys of the roles the client holds, in effect now, when the client carries the permissions scope (it does unless you registered it without).org_id, org_roles and org_role_ids: the organization the client is pinned to and the roles it holds at that organization's node; absent for a client that acts across the Environment.exp: 5 minutes after issue by default, the Environment's Machine token lifetime (60 to 3,600 seconds). No amr: nothing was authenticated beyond the secret.

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_clientThe client id or secret is wrong, or the client is deactivated. 401.
unauthorized_clientA web client presented client_credentials; only a machine client may.
invalid_targetThe audience is neither the Environment's default nor one the client lists.
invalid_requestNo 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_scopeThe scope names one the client does not carry.
Environment
API version
v1.0
On this page Was this page helpful?

Tell us how we can improve this guide.