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
jticlaim 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)│
│ │ ◀─────────────────────── │ │
└──────────────┘ └──────────────────┘
- The client builds a JWT assertion signed with its private key and POSTs it to
/auth/token. - The server validates the signature, checks claims, verifies the
jtihas not been seen before, and intersects the client's capabilities with the requested scope. - An access token is returned (Bearer, short-lived).
- The client calls any API endpoint with
Authorization: Bearer <token>. If theApiClienthasdpop_required: true, a DPoP proof header is also required. - The API validates the token (and DPoP binding if applicable).
- 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¶
- Create an ApiClient — register a new client via
POST /api-clientswith a name, capabilities, and optionally an IP allowlist. See API Clients. - 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.
- Request a token — build a JWT assertion, sign it, and POST to
/auth/token. See Private Key JWT. - Call the API — use the access token as
Authorization: Bearer <token>. - (Optional) Enable DPoP — set
dpop_required: trueon the client and include a DPoP proof header on every request. See DPoP Token Binding.