02 — Authentication
Zitadel machine-to-machine flow
Each producer is a Zitadel service user. It exchanges credentials for a short-lived JWT, then the API validates that token locally against cached JWKS — no round-trip per request.
Flow
1 · Obtain a token
Producer calls Zitadel's token endpoint with grant_type=client_credentials (or a signed JWT-profile assertion for keypair auth).
2 · Receive access JWT
Zitadel returns a signed JWT scoped to the API's audience (project:id:…:aud), typically valid ~1h.
3 · Call the ingest API
Send the event with Authorization: Bearer <jwt>.
4 · Validate locally
Gin middleware checks signature (cached JWKS), iss, aud, exp, and required role. → 401/403 on failure.
Examples
① token request → Zitadel
curl -s https://<tenant>.zitadel.cloud/oauth/v2/token \
-d grant_type=client_credentials \
-d scope="openid urn:zitadel:iam:org:project:id:<pid>:aud" \
-u "$CLIENT_ID:$CLIENT_SECRET"
# → { "access_token": "eyJ…", "expires_in": 3599 }
④ Gin middleware — local verify
func RequireM2M(v *oidc.Verifier) gin.HandlerFunc {
return func(c *gin.Context) {
tok := bearer(c.GetHeader("Authorization"))
claims, err := v.Verify(c, tok) // JWKS-cached
if err != nil { c.AbortWithStatus(401); return }
if !claims.HasRole("audit.writer") {
c.AbortWithStatus(403); return
}
c.Set("client_id", claims.Subject)
c.Next()
}
}