
Let services act under their own identity
Automated work can belong to a service rather than impersonating a person. Wildo gives registered clients their own scoped roles and tokens, so the application can authorize and attribute machine actions explicitly.
The client requests a token for the resource it will call, keeping the credential’s intended destination part of the contract.
Example — A scheduled service updates account records
An organization-owned client receives the roles needed by the scheduled service. Its token identifies that client and account; it does not pretend a human performed the update.
For engineers
Create the organization or application OAuth client through its resource operation. Set its allowed grants and roles, retain the one-time plainSecret, then use the token endpoint. A client using client_credentials acts as itself.
The token issuer carries the resolved scope and principal into the signed claim:
This implementation excerpt from machine-token-issuer.backend.service.ts shows the decision in context; explanatory source comments are omitted.
public async issueClientAccessToken(request: MachineAccessTokenRequest): Promise<MachineAccessTokenResponse> {
const maxMinutes = this.appConfigService.config.jwt.accessTokenExpirationMinutes;
const requestedMinutes = request.expiresInMinutes ?? maxMinutes;
const minutes = Math.max(1, Math.min(requestedMinutes, maxMinutes));
const claim: Jwt_MachineToken_CreationParameter = {
type: request.scopeType === ResourcePrimaryScope.ORGANIZATIONS
? ExecutionContext_ExecutionType.ORGANIZATION_MACHINE
: ExecutionContext_ExecutionType.APPLICATION_MACHINE,
clientId: request.clientId,
scopeId: request.scopeId,
roles: request.roles,
};
const accessToken = await this.jwtService.createJwtForMachineToken(claim, {
audience: request.audience,
expiresInMinutes: minutes,
});
this.logger.debug('Issued machine client-credentials access token', {
clientId: request.clientId,
scopeType: request.scopeType,
scopeId: request.scopeId,
audience: request.audience,
expiresInMinutes: minutes,
});
return { accessToken, tokenType: 'Bearer', expiresIn: minutes * 60 };
}
Request the intended resource
The token request names grant_type=client_credentials, the client credential and the resource audience. Present the returned access_token as a Bearer credential to that resource. The token endpoint checks that this client allows the grant and that the requested resource is known.
The public token endpoint uses the application’s configured access-token lifetime. It does not accept a requested lifetime. The expiresInMinutes option shown above belongs to the internal issuer API; its callers may shorten the policy maximum.
The following request follows external-access-auth.e2e.ts. Set BACKEND_URL to the application backend, CLIENT_SECRET to the one-time secret and RESOURCE_AUDIENCE to a resource identifier advertised by that deployment.
curl "$BACKEND_URL/oauth/token" \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=client_credentials' \
--data-urlencode "client_secret=$CLIENT_SECRET" \
--data-urlencode "resource=$RESOURCE_AUDIENCE"
The response supplies access_token, token_type and expires_in. Save access_token as ACCESS_TOKEN; set RESOURCE_URL to an endpoint in that audience for which the client has permission:
curl "$RESOURCE_URL" -H "Authorization: Bearer $ACCESS_TOKEN"
An audience match is necessary but does not grant an operation role. A valid organization client still cannot use its token to operate on another organization’s records.
Separate the client secret from tokens already issued
The client secret is used to obtain tokens. A Bearer token is a signed snapshot of the client’s roles and scope with its own expiry. Changing the first does not rewrite the second.
| Change | New token requests | Machine Bearer tokens already issued |
|---|---|---|
| Rotate the secret | New secret works; previous secret works until oldSecretInvalidationDate, defaulting to immediate cutover | Retain their signed claims and expiry |
| Regenerate the secret | New secret is returned; the previous secret is retained with open-ended overlap | Retain their signed claims and expiry |
| Client is no longer active or has expired | Refused when the client is read during exchange | Client status is not re-read by the machine Bearer authentication paths |
| Change the client’s roles | Subsequent tokens receive the current roles | Existing tokens retain the roles signed into them |
There is no dedicated OAuth-client deactivate/reactivate operation pair in this resource contract. The status row above describes an admission condition, not an extra management endpoint.
Rotation and regeneration update the same client record; the returned plainSecret is the one-time value to install in the integration. Regeneration is therefore not an immediate retirement of the previous secret. Choose rotation with a deliberate cutoff when the outgoing secret must stop obtaining tokens.
The ordinary API and supported external machine-token paths verify the signature, issuer, audience and token expiry, then construct machine authority from the claims. They do not look up the client again in those Bearer branches. Operation authorization and scope checks still apply; this is not a promise that every request succeeds until expiry.
Direct authentication with the OAuth client secret is different: that path reads the client and checks its status and expiry for the request. User-delegated agent tokens also follow a different validation path. Do not extend this machine-Bearer behavior to every kind of credential.
Make the cutover observable
Use the application’s configured access-token lifetime and the token response’s expires_in when planning the overlap. The public token endpoint does not accept a shorter lifetime requested by the integrating service.
For a controlled rotation, keep a pre-rotation token and test these separate outcomes in a disposable integration:
- Obtain a new token with the replacement secret and call an operation the client is allowed to use.
- After the chosen cutoff, confirm the old secret can no longer obtain a token.
- Check the pre-rotation Bearer separately: the secret cutoff does not itself revoke that token. Token expiry and the resource’s other authorization checks remain its boundaries.
- Confirm the same token is refused after expiry. Keep an authorized fresh-token call as a control so a missing route or stopped service is not mistaken for revocation.
These checks distinguish a successful secret replacement from withdrawal of already issued access. If the product requires immediate client-wide invalidation of machine Bearers, the current authentication paths do not supply that guarantee.
Design business code for a machine caller
Machine identity is recorded independently from user identity. A machine-created record may have no creator user ID, so custom business logic should use the execution context’s principal rather than assuming every authorized action has a human userId.
User delegation is a separate flow: agent tokens represent a consenting person, while this client represents the service itself.