chore(auth): log JWT header + payload on verify failure
Temporary diagnostic for triaging token-verification 401s. On verify failure (signature mismatch / expired / kid not in JWKS / wrong aud), log the underlying error plus a whitelisted decode of the JWT header (kid, alg, typ) and payload (iss, aud, azp, sub, exp, iat) so the cause is distinguishable from the log alone. Only fires on failure — successful requests stay unlogged. The decodeJWTPart helper whitelists safe metadata fields and never returns the signature segment. Remove once the current realm-config drift is settled. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -11,8 +11,11 @@ package auth
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -162,6 +165,12 @@ func (vr *Verifier) Middleware() fiber.Handler {
|
|||||||
raw := strings.TrimPrefix(header, "Bearer ")
|
raw := strings.TrimPrefix(header, "Bearer ")
|
||||||
claims, err := vr.Verify(c.UserContext(), raw)
|
claims, err := vr.Verify(c.UserContext(), raw)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
// Temporary diagnostic — log the actual verify error and the
|
||||||
|
// token's header (kid, alg, typ) so we can tell signature vs
|
||||||
|
// expiration vs key-not-found apart. Payload claims (sub, iss,
|
||||||
|
// exp, aud) too so we can correlate with realm config.
|
||||||
|
log.Printf("[auth] verify failed: %v | header=%s payload=%s",
|
||||||
|
err, decodeJWTPart(raw, 0), decodeJWTPart(raw, 1))
|
||||||
return c.Status(fiber.StatusUnauthorized).
|
return c.Status(fiber.StatusUnauthorized).
|
||||||
JSON(fiber.Map{"error": fmt.Sprintf("invalid token: %v", err)})
|
JSON(fiber.Map{"error": fmt.Sprintf("invalid token: %v", err)})
|
||||||
}
|
}
|
||||||
@@ -170,6 +179,39 @@ func (vr *Verifier) Middleware() fiber.Handler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// decodeJWTPart base64-decodes header (idx 0) or payload (idx 1) of a JWT
|
||||||
|
// and pretty-prints selected fields. Best-effort — returns the raw segment
|
||||||
|
// on parse failure. Never returns the signature segment.
|
||||||
|
func decodeJWTPart(token string, idx int) string {
|
||||||
|
parts := strings.Split(token, ".")
|
||||||
|
if idx >= len(parts) || idx > 1 {
|
||||||
|
return "(missing)"
|
||||||
|
}
|
||||||
|
raw, err := base64.RawURLEncoding.DecodeString(parts[idx])
|
||||||
|
if err != nil {
|
||||||
|
return parts[idx][:min(len(parts[idx]), 40)] + "(b64err)"
|
||||||
|
}
|
||||||
|
var m map[string]any
|
||||||
|
if err := json.Unmarshal(raw, &m); err != nil {
|
||||||
|
return string(raw)[:min(len(raw), 80)]
|
||||||
|
}
|
||||||
|
keep := map[string]any{}
|
||||||
|
for _, k := range []string{"kid", "alg", "typ", "iss", "aud", "azp", "sub", "exp", "iat"} {
|
||||||
|
if v, ok := m[k]; ok {
|
||||||
|
keep[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b, _ := json.Marshal(keep)
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func min(a, b int) int {
|
||||||
|
if a < b {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
func User(c *fiber.Ctx) *UserClaims {
|
func User(c *fiber.Ctx) *UserClaims {
|
||||||
if u, ok := c.Locals("user").(*UserClaims); ok {
|
if u, ok := c.Locals("user").(*UserClaims); ok {
|
||||||
return u
|
return u
|
||||||
|
|||||||
Reference in New Issue
Block a user