Authaz logoAuthaz
DocumentationAPI Reference
  • Get Started

    • Authaz
    • Core Concepts
    • Set up your app
    • Quickstart — cURL
  • Authentication

    • Authentication Settings
    • Signup
    • Invitations
    • Password Authentication
    • Multi-Factor Auth
    • Magic Link
    • OAuth / Social Login
    • Passkey (WebAuthn)
    • SAML SSO
    • Machine-to-Machine (M2M)
    • API Keys
  • Authorization

    • Authorization
    • Resources
    • Policies
    • Roles
    • Access Explorer
  • Tenancy

    • Multi-tenancy
    • Tenancy Customization
  • Brand & Host

    • Branding
    • Custom Domains
    • Communications & Email Templates
  • Operate

    • Users
    • Analytics
    • Audit Logs
    • Application Settings
  • SDK Quickstarts

    • Quickstart — Next.js
    • Quickstart — React SPA
    • Quickstart — Hono
    • Quickstart — .NET (Authaz.Sdk)
  • Recipes

    • Recipes & Cookbook
    • Next.js — first integration
    • Next.js — B2B SaaS (multi-tenant)
    • Hono — first integration
    • Hono — B2B SaaS (multi-tenant)
    • React SPA — first integration
    • React SPA — B2B SaaS (multi-tenant)
    • .NET — first integration
    • .NET — B2B SaaS (multi-tenant)
  • Reference

    • Tokens
    • API Reference
    • Errors & Troubleshooting
  • Documentation

    • How Authaz is Built
  1. Authaz
  2. Docs
  3. Authentication
  4. Machine-to-Machine (M2M)

Authentication

Machine-to-Machine (M2M)

2 min read·Updated Jun 19, 2026

M2M authentication is for servers, jobs, and services that need to call your API without a human user. Authaz issues access tokens through the standard OAuth 2.0 client credentials grant — no Authaz Sign-In, no redirects, just a client_id and client_secret exchange.

# Get an access token for a service
curl -X POST "https://your-app.authaz.io/oauth2/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials" \
  -d "client_id=YOUR_CLIENT_ID" \
  -d "client_secret=YOUR_CLIENT_SECRET" \
  -d "scope=invoices:read"
{
  "access_token": "eyJhbGciOiJSUzI1NiIs...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "invoices:read"
Previous
SAML SSO
Next
API Keys
}

Setup#

1. Enable M2M#

Dashboard → Application → Authentication → Identity Broker → Enable, or via the API:

curl -X PUT https://your-app.authaz.io/api/v1/applications/{appId}/auth/m2m \
  -H "X-API-Key: $AUTHAZ_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "enabled": true,
    "defaultTokenLifetimeSeconds": 3600
  }'

2. Create a credential#

Each service that needs to authenticate gets its own credential:

curl -X POST https://your-app.authaz.io/api/v1/applications/{appId}/m2m/credentials \
  -H "X-API-Key: $AUTHAZ_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "billing-job",
    "scopes": ["invoices:read", "invoices:create"],
    "expiresAt": "2027-01-01T00:00:00Z"
  }'
{
  "id": "cred_01h...",
  "clientId": "01h8...",
  "clientSecret": "secret_xxxxxxxxxxxx...",
  "name": "billing-job",
  "scopes": ["invoices:read", "invoices:create"],
  "expiresAt": "2027-01-01T00:00:00Z"
}

The clientSecret is shown once. Store it in your secrets manager immediately — Authaz cannot retrieve it later, only rotate it.

3. Use it from the service#

Either request a fresh token on every batch of API calls, or cache the token and refresh it before expires_in runs out (e.g. with a 5-minute buffer):

const cached = { token: null, expiresAt: 0 };
 
const getToken = async () => {
  if (cached.token && Date.now() < cached.expiresAt - 5 * 60_000) {
    return cached.token;
  }
 
  const res = await fetch("https://your-app.authaz.io/oauth2/token", {
    method: "POST",
    headers: { "Content-Type": "application/x-www-form-urlencoded" },
    body: new URLSearchParams({
      grant_type: "client_credentials",
      client_id: process.env.AUTHAZ_CLIENT_ID!,
      client_secret: process.env.AUTHAZ_CLIENT_SECRET!,
      scope: "invoices:read invoices:create",
    }),
  });
 
  const json = await res.json();
  cached.token = json.access_token;
  cached.expiresAt = Date.now() + json.expires_in * 1000;
  return cached.token;
};

Permissions and scopes#

The credential is its own principal as far as Authaz is concerned. Assign it roles, scope it to a tenant, do anything you'd do with a user:

# Give the credential a role
curl -X POST https://your-app.authaz.io/api/v1/role-assignments \
  -H "X-API-Key: $AUTHAZ_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "subjectType": "credential",
    "subjectId": "cred_01h...",
    "roleId": "role_billing_writer"
  }'

The access token's permissions claim includes everything the credential can do. Your backend can read it and short-circuit unauthorized calls without round-tripping to Authaz.

High-security: private_key_jwt#

For credentials that move through high-risk environments (CI runners, third-party servers), swap the static client_secret for a signed JWT (private_key_jwt). The secret never travels — only a one-shot, short-lived JWT does.

1. Generate an RSA keypair#

openssl genrsa -out private.pem 2048
openssl rsa -in private.pem -pubout -out public.pem

2. Upload the public key as JWKS#

curl -X POST https://your-app.authaz.io/api/v1/applications/{appId}/m2m/credentials/{credId}/jwks \
  -H "X-API-Key: $AUTHAZ_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "keys": [{
      "kty": "RSA",
      "kid": "key-1",
      "use": "sig",
      "alg": "RS256",
      "n": "...",
      "e": "AQAB"
    }]
  }'

(Convert public.pem to a JWK using any standard tool, e.g. pem-jwk on npm.)

3. Mint and exchange a JWT#

import { SignJWT } from "jose";
 
const assertion = await new SignJWT({})
  .setProtectedHeader({ alg: "RS256", kid: "key-1" })
  .setIssuer(clientId)
  .setSubject(clientId)
  .setAudience("https://your-app.authaz.io/oauth2/token")
  .setExpirationTime("2m")
  .setJti(crypto.randomUUID())
  .sign(privateKey);
 
const res = await fetch("https://your-app.authaz.io/oauth2/token", {
  method: "POST",
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
  body: new URLSearchParams({
    grant_type: "client_credentials",
    client_id: clientId,
    client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
    client_assertion: assertion,
    scope: "invoices:read",
  }),
});

The clientSecret you noted earlier becomes irrelevant — Authaz validates the JWT signature against your uploaded JWKS instead. (JWKS = JSON Web Key Set, the standard format for publishing public keys.)

Rotating secrets#

Static secrets need rotation. Authaz lets you rotate without downtime:

curl -X POST https://your-app.authaz.io/api/v1/applications/{appId}/m2m/credentials/{credId}/rotate \
  -H "X-API-Key: $AUTHAZ_API_KEY"

The response contains the new clientSecret. The old one stays valid for a 24-hour grace period — deploy the new secret to your services within that window, then nothing further to do.

Revoking immediately#

For incidents, skip rotation and revoke:

curl -X POST https://your-app.authaz.io/api/v1/applications/{appId}/m2m/credentials/{credId}/revoke \
  -H "X-API-Key: $AUTHAZ_API_KEY"

Existing access tokens issued by the revoked credential remain valid until their expires_in runs out. Pair with defaultTokenLifetimeSeconds: 300 if you need fast revocation.

Next steps#

  • API Keys — long-lived keys for end-customers, not your internal services.
  • API Reference — the Management API uses M2M tokens or API keys.
  • Multi-tenancy — credentials can be tenant-scoped.