The Complete Guide to Machine-to-Machine (M2M) Authentication

Learn how M2M tokens work, implement OAuth 2.0 Client Credentials, host JWKS, rotate keys, and secure service-to-service authentication with examples in curl, Node, Python, and Go.

10
 min. read
August 23, 2025

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

  1. Register the client: In your authorization server (e.g., Authgear), register the calling service and obtain a client_id and client_secret.
  2. Request a token: The client posts its credentials to the token endpoint (/oauth/token) using application/x-www-form-urlencoded. (See OAuth 2.0: RFC 6749, Section 4.4 — Client Credentials Grant.)
  3. 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).
  4. Call the resource: The client includes Authorization: Bearer <access_token> in API requests.
  5. 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 corresponding kid.
  • “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 featureGet started and secure your backend in minutes.

Preferences

Privacy is important to us, so you have the option of disabling certain types of storage that may not be necessary for the basic functioning of the website. Blocking categories may impact your experience on the website.

Accept all cookies

These items are required to enable basic website functionality.

Always active

These items are used to deliver advertising that is more relevant to you and your interests.

These items allow the website to remember choices you make (such as your user name, language, or the region you are in) and provide enhanced, more personal features.

These items help the website operator understand how its website performs, how visitors interact with the site, and whether there may be technical issues.

Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.