OAuth Client Credentials, M2M Tokens, JWKS & Best Practices
Machine-to-machine (M2M) authentication enables two services — servers, daemons, CI/CD pipelines, or IoT devices — to authenticate and authorize with each other without a human user involved. The most common, standards-based pattern uses the OAuth 2.0 Client Credentials Flow to issue short-lived access tokens (often JWTs) that a calling service includes with requests to protected APIs. See the OAuth 2.0 spec: RFC 6749.
This guide explains:
- What M2M authentication is and why it’s better than static API keys
- How the OAuth Client Credentials Flow works (step-by-step)
- How to implement it (curl, Node, Python, Go examples)
- JWKS (JWK format & hosting) and verification strategies
- Key rotation, revocation, and production security best practices
- How Authgear supports M2M tokens (link to product)
Why use M2M tokens (not API keys)?
Static API keys are easy to leak and difficult to rotate at scale. M2M tokens (issued via OAuth) offer:
- Short lifetime — tokens expire quickly so leaked tokens have limited value.
- Scoped access — tokens can carry fine-grained scopes limiting what a machine may do.
- Revocation & rotation — client secrets can be rotated and tokens revoked or expire.
- Signed tokens (JWTs) — recipients can verify signature and claims locally (or use introspection). See the JSON Web Token (JWT) spec: RFC 7519.
- Auditability — token issuance and use can be logged for compliance.
For these reasons, M2M tokens are the recommended pattern for secure service-to-service authentication.
How the OAuth Client Credentials Flow works
- Register the client: In your authorization server (e.g., Authgear), register the calling service and obtain a
client_id
andclient_secret
. - Request a token: The client posts its credentials to the token endpoint (
/oauth/token
) usingapplication/x-www-form-urlencoded
. (See OAuth 2.0: RFC 6749, Section 4.4 — Client Credentials Grant.) - Receive an access token: If credentials are valid, the server returns an access token (often a signed JWT) and optionally a refresh token (rare in Client Credentials).
- Call the resource: The client includes
Authorization: Bearer <access_token>
in API requests. - Verification: The API verifies the token — either by verifying the signature against a published JWK (JWKS) or by calling a token introspection endpoint(RFC 7662). See Token Introspection: RFC 7662.
This flow avoids user logins entirely and is ideal for backends and automated systems.
Example: Get an M2M access token (curl)
# Replace with your Authgear token endpoint and client credentials
TOKEN_ENDPOINT="https://project_id.authgear.cloud/oauth/token"
CLIENT_ID="your_client_id"
CLIENT_SECRET="your_client_secret"
curl -X POST "$TOKEN_ENDPOINT" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials&scope=read:orders write:orders" \
-u "$CLIENT_ID:$CLIENT_SECRET"
Success response sample
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "read:orders write:orders"
}
Example: Request & verify in Node JS
Request token
const axios = require('axios');
async function getToken() {
const tokenEndpoint = 'https://project_id.authgear.cloud/oauth/token';
const clientId = process.env.CLIENT_ID;
const clientSecret = process.env.CLIENT_SECRET;
const params = new URLSearchParams();
params.append('grant_type', 'client_credentials');
params.append('scope', 'read:orders write:orders');
const response = await axios.post(tokenEndpoint, params.toString(), {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
auth: { username: clientId, password: clientSecret },
});
return response.data.access_token;
}
Verify JWT using jose
and a JWKS endpoint
const { jwtVerify, createRemoteJWKSet } = require('jose');
const jwksUrl = 'https://project_id.authgear.cloud/.well-known/jwks.json';
const JWKS = createRemoteJWKSet(new URL(jwksUrl));
async function verify(token) {
const { payload } = await jwtVerify(token, JWKS, {
issuer: 'https://project_id.authgear.cloud',
audience: 'your-api-audience'
});
return payload;
}
Example: Python
Install dependencies
pip install requests pyjwt cryptography
Request token
import requests
from requests.auth import HTTPBasicAuth
TOKEN_ENDPOINT = "https://project_id.authgear.cloud/oauth/token"
client_id = "your_client_id"
client_secret = "your_client_secret"
resp = requests.post(
TOKEN_ENDPOINT,
data={"grant_type": "client_credentials", "scope": "read:orders"},
auth=HTTPBasicAuth(client_id, client_secret),
)
access_token = resp.json()["access_token"]
print(access_token)
Verify
To verify the JWT, see our docs for code examples.
Go
Install dependencies
go get gopkg.in/square/go-jose.v2
go get github.com/dgrijalva/jwt-go
Request token
package main
import (
"net/http"
"net/url"
"strings"
"io/ioutil"
"fmt"
)
func getToken() {
endpoint := "https://project_id.authgear.cloud/oauth/token"
data := url.Values{}
data.Set("grant_type", "client_credentials")
data.Set("scope", "read:orders")
req, _ := http.NewRequest("POST", endpoint, strings.NewReader(data.Encode()))
req.SetBasicAuth("your_client_id", "your_client_secret")
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
client := &http.Client{}
resp, _ := client.Do(req)
body, _ := ioutil.ReadAll(resp.Body)
fmt.Println(string(body))
}
Verify
For JWT verification in Go, use the golang-jwt/jwt
or square/go-jose
packages to fetch JWKS and verify tokens.
Verification strategies: local verification vs introspection
- Local verification (recommended for performance): Fetch JWKS periodically (with caching), and verify JWT signature locally using JWS rules. Use the
kid
header to select the correct JWK. - Token Introspection: Call the authorization server’s introspection endpoint to check token validity. Useful when tokens are opaque or when you need immediate revocation checks.
Combine both: verify locally for performance but fall back to introspection if the signature is unknown or kid
is missing.
Key rotation & revocation
Rotation must be planned to avoid downtime. Common strategies:
1. Dual-signing
- Introduce a new key with a new
kid
and publish it in JWKS before you start issuing tokens signed by it. - Continue to serve old keys in JWKS until all issued tokens using the old key expire.
- Remove the old key from JWKS after the max token lifetime has passed.
2. Secret rotation for client credentials
Rotate client_secret
in an atomic or staged way:
- Add second secret to the client configuration (if platform supports), deploy clients with new secret, then revoke the old one.
- If no second secret support, do a short maintenance window and rotate.
Checklist
- Document rotation plan & SLAs.
- Automate rotation where possible (CI job).
- Ensure JWKS served contains all keys needed during overlap.
- Communicate rotation windows to stakeholders.
Production security best practices
- Never paste production private keys into browser tools; generate production keys in HSM or KMS.
- Use short token lifetimes and refresh tokens only where appropriate (rare for M2M).
- Segment scopes per service — follow the least-privilege principle.
- Protect client secrets in secure stores (HashiCorp Vault, cloud KMS, CI secret storage).
- Monitor & alert on unusual token issuance or use.
- Use TLS/mTLS for service communication if appropriate.
- Log token issuance and usage for auditing and forensics.
Troubleshooting common issues
- “Invalid signature” — ensure the API fetches the right public key (check
kid
) and that the JWKS has the correspondingkid
. - “Expired token” — increase token lifetime for long background jobs (carefully) or implement automatic re-auth for jobs.
- “Invalid scope” — verify that the requested scope matches what the client is allowed to request.
- Clock drift — ensure servers have synchronized clocks (NTP) so
exp
/nbf
checks pass.
Real-world use cases
- Microservices: Each service authenticates to others using its own client credentials and receives scoped tokens.
- CI/CD agents: Build agents obtain tokens to deploy or interact with internal APIs.
- IoT devices: Devices receive per-device tokens for telemetry. Prefer asymmetric keys and short lifetimes for constrained devices.
- Batch jobs: Cron jobs request tokens to run scheduled tasks without a user.
How Authgear helps
Authgear provides an easy path to implement M2M tokens: registration UX for clients, token endpoints, JWKS hosting, short-lived JWTs, audit logs, and options for cloud or self-hosted deployment. See Machine-to-Machine Token feature.
Example sequence diagram

FAQ
Q: What is the recommended token lifetime for M2M tokens?
A: Short as possible while still enabling operations — 5–60 minutes is common. For long-running jobs, use auto re-auth that requests a fresh token when needed.
Q: Should I use symmetric (HS256
) or asymmetric (RS256
) tokens?
A: For high security and ease of key management across services, asymmetric (RS/ES) is preferred — you can publish public keys in JWKS. Use symmetric only when both issuer and verifier share a secure secret and distribution is simple.
Q: How do I safely rotate client secrets?
A: Use staged rotation with dual secrets if supported, or schedule a maintenance window and update clients quickly using automation. Prefer supporting multiple active secrets per client.
Conclusion
M2M tokens are the secure, standards-based way to authenticate machines and services. If you want a managed path that implements these best practices with a developer-first UX, see Authgear’s Machine-to-Machine Token feature — Get started and secure your backend in minutes.