From 2d6a7cce30fb6bb74efbf146c5afefa65b7772ab Mon Sep 17 00:00:00 2001 From: Admin Bot Date: Thu, 10 Sep 2026 04:33:56 +0900 Subject: [PATCH] feat(audit): log successful JWT auth with subject, acting_party, roles - Add 'auth ok' info log after JWT validation passes - Export ClaimString/ClaimStringSlice from identity package - Log fields: subject, acting_party, roles/permissions, path, method - Complements existing 'auth failed' error logs for full audit trail --- internal/identity/headers.go | 18 +++++++++--------- internal/proxy/proxy.go | 18 ++++++++++++++++++ 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/internal/identity/headers.go b/internal/identity/headers.go index 3930832..17dc78b 100644 --- a/internal/identity/headers.go +++ b/internal/identity/headers.go @@ -52,18 +52,18 @@ func StripIncoming(r *http.Request) { func Inject(r *http.Request, claims jwt.MapClaims) { r.Header.Set(HeaderAuthVerified, "true") - if sub := claimString(claims, "sub"); sub != "" { + if sub := ClaimString(claims, "sub"); sub != "" { r.Header.Set(HeaderUser, sub) } - if roles := claimStringSlice(claims, "roles"); len(roles) > 0 { + if roles := ClaimStringSlice(claims, "roles"); len(roles) > 0 { r.Header.Set(HeaderRoles, strings.Join(roles, ",")) - } else if perms := claimStringSlice(claims, "permissions"); len(perms) > 0 { + } else if perms := ClaimStringSlice(claims, "permissions"); len(perms) > 0 { r.Header.Set(HeaderRoles, strings.Join(perms, ",")) } - if azp := claimString(claims, "azp"); azp != "" { - sub := claimString(claims, "sub") + if azp := ClaimString(claims, "azp"); azp != "" { + sub := ClaimString(claims, "sub") // Only set acting-service when azp differs from sub // (i.e., a service account acting, not the user themselves) if azp != sub { @@ -72,9 +72,9 @@ func Inject(r *http.Request, claims jwt.MapClaims) { } } -// claimString extracts a string value from claims, returning "" if +// ClaimString extracts a string value from claims, returning "" if // the key is missing or not a string. -func claimString(claims jwt.MapClaims, key string) string { +func ClaimString(claims jwt.MapClaims, key string) string { val, ok := claims[key] if !ok || val == nil { return "" @@ -86,10 +86,10 @@ func claimString(claims jwt.MapClaims, key string) string { return s } -// claimStringSlice extracts a []string from claims. JWT libraries +// ClaimStringSlice extracts a []string from claims. JWT libraries // deserialize JSON arrays as []interface{}, so each element is // type-asserted individually. Non-string elements are skipped. -func claimStringSlice(claims jwt.MapClaims, key string) []string { +func ClaimStringSlice(claims jwt.MapClaims, key string) []string { val, ok := claims[key] if !ok || val == nil { return nil diff --git a/internal/proxy/proxy.go b/internal/proxy/proxy.go index aaaeeab..d804c58 100644 --- a/internal/proxy/proxy.go +++ b/internal/proxy/proxy.go @@ -359,6 +359,24 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { // Inject identity headers for downstream services identity.Inject(r, claims) + // Audit trail: log successful JWT authentication + auditFields := map[string]string{ + "path": r.URL.Path, + "method": r.Method, + } + if sub := identity.ClaimString(claims, "sub"); sub != "" { + auditFields["subject"] = sub + } + if azp := identity.ClaimString(claims, "azp"); azp != "" { + auditFields["acting_party"] = azp + } + if roles := identity.ClaimStringSlice(claims, "roles"); len(roles) > 0 { + auditFields["roles"] = strings.Join(roles, ",") + } else if perms := identity.ClaimStringSlice(claims, "permissions"); len(perms) > 0 { + auditFields["permissions"] = strings.Join(perms, ",") + } + logging.Infof("auth ok", auditFields) + // Check required capability if configured if h.config.Auth.RequiredCapability != "" { if !h.jwtValidator.CheckPermissions(claims, h.config.Auth.RequiredCapability, "*") {