feat(auth): audit-log rejections + 503 on Dex outage (gitea-mcp#9, #6)
BearerMiddleware now emits a structured slog line on every rejection with the reason (no_token / static_token_mismatch / jwt_invalid / jwt_dex_unavailable), client IP (X-Forwarded-For aware), presented token type (jwt/opaque), and a truncated SHA-256 fingerprint — never the raw token, so no secret material reaches stdout/log aggregation. Validate now tags a JWKS/Dex fetch failure with the exported ErrUnavailable sentinel (distinct from a present-but-invalid token). BearerMiddleware maps it to HTTP 503 with `WWW-Authenticate: Bearer error="temporarily_unavailable"`, so a transient Dex outage is distinguishable from a bad token instead of a silent generic 401. No signature change — logging goes through slog.Default(); existing consumers are unaffected until they set a default logger. Behavior reaches a consumer only when it bumps the chassis version. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+11
-1
@@ -12,6 +12,7 @@ package auth
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
@@ -20,6 +21,13 @@ import (
|
||||
"github.com/lestrrat-go/jwx/v2/jwt"
|
||||
)
|
||||
|
||||
// ErrUnavailable indicates JWT validation could not be COMPLETED because the
|
||||
// JWKS / Dex endpoint was unreachable — a transient condition — as opposed to
|
||||
// the token being present and invalid. Callers (e.g. BearerMiddleware) map it
|
||||
// to HTTP 503 temporarily_unavailable rather than a generic 401, so a Dex
|
||||
// outage is distinguishable from a bad token (gitea-mcp#6).
|
||||
var ErrUnavailable = errors.New("jwt validation temporarily unavailable")
|
||||
|
||||
// JWTValidator validates Bearer JWTs issued by a Dex (OIDC) authorization server.
|
||||
// Audience is optional; leave empty to skip audience validation.
|
||||
//
|
||||
@@ -94,7 +102,9 @@ func (v *JWTValidator) Validate(ctx context.Context, rawToken string) (string, e
|
||||
|
||||
keySet, err := v.cache.Get(ctx, v.jwksURI)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("get jwks: %w", err)
|
||||
// The JWKS could not be fetched — Dex/JWKS is unreachable, not a bad
|
||||
// token. Tag it ErrUnavailable so the caller can answer 503, not 401.
|
||||
return "", fmt.Errorf("%w: get jwks: %v", ErrUnavailable, err)
|
||||
}
|
||||
|
||||
opts := []jwt.ParseOption{
|
||||
|
||||
Reference in New Issue
Block a user