Skip to content

Machine-to-Machine Authentication

Machine-to-machine (M2M) authentication lets external services, batch jobs, and partner integrations call Craft Easy APIs without a human in the loop. It replaces the legacy API key system with a standards-based approach built on OAuth 2.0 Client Credentials and private_key_jwt client assertion (RFC 7523).

Why private_key_jwt?

Traditional API keys are shared secrets — if one leaks, the attacker has permanent access until the key is revoked. private_key_jwt avoids this by using asymmetric cryptography:

  • The private key never leaves the client.
  • The client signs a short-lived JWT assertion and sends it to the token endpoint.
  • The server verifies the signature against the client's registered public key.
  • Even if an assertion is intercepted, it expires in seconds and cannot be replayed (the jti claim is cached).

The result is a flow where no secret is ever transmitted over the wire.

How it differs from user authentication

Aspect User auth M2M auth
Caller Human (browser, mobile app) Service, script, cron job
First factor OTP, OAuth2, WebAuthn private_key_jwt assertion
Token endpoint /auth/login/* POST /auth/token
Token header Authorization: Bearer <token> Authorization: Bearer <token>
Session tracking UserSession with fingerprint No session — stateless tokens
Refresh Token refresh via /auth/refresh Re-authenticate with a new assertion
Capabilities Inherited from roles Assigned directly to the ApiClient
Optional binding DPoP (RFC 9449) token binding

Both tracks produce a Bearer token that downstream middleware treats identically — access control, tenant isolation, audit logging, and ETag concurrency all work the same way.

The M2M flow

┌──────────────┐                          ┌──────────────────┐
│              │  1. POST /auth/token      │                  │
│   Client     │  grant_type=             │   Craft Easy     │
│   Service    │    client_credentials    │   API            │
│              │  client_assertion=<jwt>  │                  │
│              │ ───────────────────────▶ │                  │
│              │                          │  2. Verify JWT   │
│              │                          │     signature    │
│              │                          │     + jti cache  │
│              │  3. Access token         │     + rate limit │
│              │ ◀─────────────────────── │     + cap check  │
│              │                          │                  │
│              │  4. GET /resource        │                  │
│              │  Authorization: Bearer   │                  │
│              │  (+ DPoP proof if req.)  │                  │
│              │ ───────────────────────▶ │                  │
│              │                          │  5. Validate     │
│              │  6. Response             │     token (+DPoP)│
│              │ ◀─────────────────────── │                  │
└──────────────┘                          └──────────────────┘
  1. The client builds a JWT assertion signed with its private key and POSTs it to /auth/token.
  2. The server validates the signature, checks claims, verifies the jti has not been seen before, and intersects the client's capabilities with the requested scope.
  3. An access token is returned (Bearer, short-lived).
  4. The client calls any API endpoint with Authorization: Bearer <token>. If the ApiClient has dpop_required: true, a DPoP proof header is also required.
  5. The API validates the token (and DPoP binding if applicable).
  6. The resource is returned, subject to the same access control as any user request.

Components

Component What it does Page
ApiClient Represents a registered service with keys, capabilities, and configuration. API Clients
private_key_jwt The assertion mechanism for obtaining access tokens. Private Key JWT
DPoP Optional token binding that prevents stolen tokens from being used on a different machine. DPoP Token Binding
Audit Every token issuance, denial, key rotation, and anomaly is logged and queryable. Audit & Monitoring

Capabilities

An ApiClient carries a capabilities list — the same capability strings used by the Capability Registry for human roles. When a token is issued, the granted capabilities are the intersection of the client's capabilities and the requested scope. This means a client can never exceed the permissions it was registered with, even if it requests a broader scope.

See Capabilities for the full capability model, naming conventions (resource:action), and how capabilities are auto-generated from resources.

Getting started

  1. Create an ApiClient — register a new client via POST /api-clients with a name, capabilities, and optionally an IP allowlist. See API Clients.
  2. Generate a key pair — create an RSA or EC key pair and upload the public key to the client. See the key generation examples in API Clients.
  3. Request a token — build a JWT assertion, sign it, and POST to /auth/token. See Private Key JWT.
  4. Call the API — use the access token as Authorization: Bearer <token>.
  5. (Optional) Enable DPoP — set dpop_required: true on the client and include a DPoP proof header on every request. See DPoP Token Binding.