M2M Audit & Monitoring¶
Every M2M authentication event is logged, queryable, and surfaced through Prometheus metrics. This page covers the audit model, the query endpoint, anomaly detection, metrics, and retention.
The ApiClientAuditEvent model¶
Every significant M2M event produces an ApiClientAuditEvent:
| Field | Type | Description |
|---|---|---|
client_id |
str |
The ApiClient that triggered the event. |
event_type |
str |
Event category (see below). |
timestamp |
datetime |
When the event occurred. |
request_ip |
str |
Client IP address. |
success |
bool |
Whether the operation succeeded. |
failure_reason |
str |
Details when success=false. |
actor_user_id |
str |
The human user who performed the action (for management operations). |
Event types¶
| Event type | Trigger |
|---|---|
token_issued |
Access token successfully issued. |
token_failed |
Token request rejected (bad signature, expired, replay, lockout, IP denied). |
key_added |
A new public key was uploaded to the client. |
key_rotated |
A key was replaced (new key added, old key revoked in sequence). |
key_revoked |
A specific key was revoked. |
scope_changed |
The client's capabilities list was updated. |
client_created |
A new ApiClient was registered. |
client_disabled |
The client was disabled (is_enabled set to false). |
client_revoked |
The client was permanently revoked or deleted. |
ip_whitelist_changed |
The client's IP allowlist was updated. |
dpop_toggled |
The dpop_required flag was changed. |
anomaly_detected |
The anomaly detection heuristic flagged unusual activity. |
Query endpoint¶
Retrieve audit events for a specific client:
GET /api-clients/{client_id}/audit?event_type=token_failed&from=2026-04-01&to=2026-04-06&per_page=50
| Parameter | Type | Default | Description |
|---|---|---|---|
event_type |
str |
— | Filter by event type. |
from |
date |
30 days ago | Start of date range. |
to |
date |
Today | End of date range. |
success |
bool |
— | Filter by success/failure. |
per_page |
int |
25 | Page size. |
Example response:
{
"items": [
{
"event_type": "token_failed",
"timestamp": "2026-04-05T14:32:10Z",
"request_ip": "203.0.113.99",
"success": false,
"failure_reason": "Signature verification failed for kid=key-old123"
},
{
"event_type": "token_issued",
"timestamp": "2026-04-05T14:30:00Z",
"request_ip": "10.0.1.42",
"success": true,
"failure_reason": null
}
],
"total": 2,
"page": 1,
"per_page": 50
}
cURL¶
# All failed token requests in the last week
curl "http://localhost:5001/api-clients/664abc.../audit?event_type=token_failed&from=2026-03-30" \
-H "Authorization: Bearer $ADMIN_TOKEN"
# All events for a client
curl "http://localhost:5001/api-clients/664abc.../audit" \
-H "Authorization: Bearer $ADMIN_TOKEN"
Anomaly detection¶
Craft Easy includes first-generation heuristics that flag unusual M2M activity:
| Heuristic | What it detects |
|---|---|
| Failure spike | Sudden increase in token_failed events compared to the client's baseline. |
| New IP | Token request from an IP not previously seen for this client. |
| Unusual time | Token request outside the client's historical activity window. |
| Rapid key rotation | Multiple key additions or revocations in a short period. |
When a heuristic fires, an anomaly_detected audit event is created with details in the failure_reason field. Anomaly events are informational — they do not block the request.
Anomaly thresholds are configured per-deployment and documented in the Craft Easy settings reference.
Prometheus metrics¶
The M2M auth module exposes the following metrics at the configured metrics endpoint (default /metrics):
| Metric | Type | Labels | Description |
|---|---|---|---|
m2m_token_issued_total |
Counter | client_id, tenant_id |
Total tokens issued. |
m2m_token_denied_total |
Counter | client_id, reason |
Total token denials. reason is one of: invalid_signature, expired_assertion, jti_replay, ip_denied, rate_limited, locked_out, client_disabled, no_matching_key, invalid_scope. |
m2m_jti_replay_total |
Counter | client_id |
Replay attempts detected (subset of m2m_token_denied_total{reason="jti_replay"}). |
m2m_dpop_verification_total |
Counter | client_id, result |
DPoP proof verifications. result is success or failure. |
m2m_token_request_duration_seconds |
Histogram | client_id |
Time taken to process a token request. |
m2m_active_clients |
Gauge | tenant_id |
Number of enabled ApiClient records per tenant. |
Example Prometheus queries¶
# Token denial rate over 5 minutes
rate(m2m_token_denied_total[5m])
# Top denial reasons
topk(5, sum by (reason) (rate(m2m_token_denied_total[1h])))
# Replay attempts per client
sum by (client_id) (rate(m2m_jti_replay_total[1h]))
# P95 token request latency
histogram_quantile(0.95, rate(m2m_token_request_duration_seconds_bucket[5m]))
Alerting examples¶
# Alert on high token denial rate
- alert: M2MHighDenialRate
expr: rate(m2m_token_denied_total[5m]) > 10
for: 5m
labels:
severity: warning
annotations:
summary: "High M2M token denial rate for {{ $labels.client_id }}"
# Alert on replay attempts
- alert: M2MReplayDetected
expr: increase(m2m_jti_replay_total[10m]) > 0
labels:
severity: critical
annotations:
summary: "JTI replay detected for client {{ $labels.client_id }}"
Retention¶
Audit events are retained according to the M2M_AUDIT_RETENTION_DAYS setting (default: 90 days). A built-in retention job runs periodically and deletes events older than the configured threshold.
Events related to security incidents (anomaly_detected, client_revoked, key_revoked) are retained for twice the standard retention period.
To query or export historical audit data beyond the retention window, use the BI Export pipeline to stream api_client_audit events to your data warehouse before they are purged.
Integration with tenant alerting¶
Audit events can trigger webhooks when configured. Set up a webhook subscription for the m2m.anomaly_detected or m2m.token_denied event types to push alerts to Slack, PagerDuty, or any HTTP endpoint.
For event bus integration, M2M audit events are also published as DomainEvent instances with event types like m2m.token_issued, m2m.token_denied, and m2m.anomaly_detected. Subscribe to these via the event bus for in-process reactions.