package auth import ( "bytes" "context" "log/slog" "net/http" "net/http/httptest" "testing" "github.com/lestrrat-go/jwx/v2/jwk" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // unavailableValidator returns a validator whose JWKS cache cannot be fetched, // so Validate fails with ErrUnavailable — simulating a Dex/JWKS outage. func unavailableValidator(t *testing.T) *JWTValidator { t.Helper() return &JWTValidator{ entries: []issuerEntry{{ issuer: "https://dex.example", jwksURI: "http://127.0.0.1:0/jwks", // unregistered in cache → Get fails }}, cache: jwk.NewCache(context.Background()), } } // #6: a JWKS/Dex outage at validation time is ErrUnavailable, distinct from a // present-but-invalid token. func TestValidate_JWKSUnreachable_IsUnavailable(t *testing.T) { t.Parallel() _, err := unavailableValidator(t).Validate(context.Background(), "some.jwt.token") require.Error(t, err) require.ErrorIs(t, err, ErrUnavailable) } // #6: when JWT validation can't complete because Dex is down, answer 503 // temporarily_unavailable rather than a generic 401. func TestBearerMiddleware_DexDown_503(t *testing.T) { t.Parallel() h := BearerMiddleware("static-tok", unavailableValidator(t), "gitea", "", http.HandlerFunc(func(http.ResponseWriter, *http.Request) { t.Fatal("next must NOT be called when Dex is down") })) rec := httptest.NewRecorder() req := httptest.NewRequest(http.MethodGet, "/", nil) req.Header.Set("Authorization", "Bearer aaa.bbb.ccc") // not the static token → JWT path h.ServeHTTP(rec, req) require.Equal(t, http.StatusServiceUnavailable, rec.Code) require.Contains(t, rec.Header().Get("WWW-Authenticate"), `error="temporarily_unavailable"`) } // #9: every rejection is audit-logged with reason, remote, token type, and a // non-reversible token fingerprint — never the raw token. func TestBearerMiddleware_AuditLogsRejection(t *testing.T) { var buf bytes.Buffer prev := slog.Default() slog.SetDefault(slog.New(slog.NewJSONHandler(&buf, &slog.HandlerOptions{Level: slog.LevelInfo}))) defer slog.SetDefault(prev) h := BearerMiddleware("expected-secret", nil, "gitea", "", http.HandlerFunc(func(http.ResponseWriter, *http.Request) {})) rec := httptest.NewRecorder() req := httptest.NewRequest(http.MethodGet, "/", nil) req.Header.Set("Authorization", "Bearer wrong-secret-value") h.ServeHTTP(rec, req) require.Equal(t, http.StatusUnauthorized, rec.Code) logged := buf.String() assert.Contains(t, logged, "static_token_mismatch") assert.Contains(t, logged, "token_fp") assert.NotContains(t, logged, "wrong-secret-value", "raw token must never be logged") } func TestBearerMiddleware_StaticTokenWins(t *testing.T) { t.Parallel() called := false next := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { called = true w.WriteHeader(http.StatusNoContent) }) h := BearerMiddleware("supersecret", nil, "brain", "", next) rec := httptest.NewRecorder() req := httptest.NewRequest(http.MethodGet, "/", nil) req.Header.Set("Authorization", "Bearer supersecret") h.ServeHTTP(rec, req) require.True(t, called, "next must be called on valid static token") require.Equal(t, http.StatusNoContent, rec.Code) } func TestBearerMiddleware_NoHeader_401NoChallengeWhenMetadataEmpty(t *testing.T) { t.Parallel() h := BearerMiddleware("any", nil, "brain", "", http.HandlerFunc(func(http.ResponseWriter, *http.Request) {})) rec := httptest.NewRecorder() h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/", nil)) require.Equal(t, http.StatusUnauthorized, rec.Code) require.Empty(t, rec.Header().Get("WWW-Authenticate")) } func TestBearerMiddleware_NoHeader_EmitsChallengeWhenMetadataSet(t *testing.T) { t.Parallel() h := BearerMiddleware("any", nil, "brain", "https://brain-mcp.d-ma.be/.well-known/oauth-protected-resource", http.HandlerFunc(func(http.ResponseWriter, *http.Request) {})) rec := httptest.NewRecorder() h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/", nil)) require.Equal(t, http.StatusUnauthorized, rec.Code) require.Equal(t, `Bearer realm="brain", resource_metadata="https://brain-mcp.d-ma.be/.well-known/oauth-protected-resource"`, rec.Header().Get("WWW-Authenticate"), ) } func TestBearerMiddleware_WrongStaticToken_401(t *testing.T) { t.Parallel() h := BearerMiddleware("expected", nil, "brain", "", http.HandlerFunc(func(http.ResponseWriter, *http.Request) { t.Fatal("next must NOT be called on wrong token") })) rec := httptest.NewRecorder() req := httptest.NewRequest(http.MethodGet, "/", nil) req.Header.Set("Authorization", "Bearer wrong") h.ServeHTTP(rec, req) require.Equal(t, http.StatusUnauthorized, rec.Code) } func TestBearerMiddleware_EmptyBearer_401(t *testing.T) { t.Parallel() h := BearerMiddleware("expected", nil, "brain", "", http.HandlerFunc(func(http.ResponseWriter, *http.Request) { t.Fatal("next must NOT be called on empty bearer") })) rec := httptest.NewRecorder() req := httptest.NewRequest(http.MethodGet, "/", nil) req.Header.Set("Authorization", "Bearer ") h.ServeHTTP(rec, req) require.Equal(t, http.StatusUnauthorized, rec.Code) } func TestBearerMiddleware_StaticOnly_NilValidator_OK(t *testing.T) { t.Parallel() // Verifies that JWT-disabled deployments (validator == nil) work end-to-end. called := false h := BearerMiddleware("tok", nil, "brain", "", http.HandlerFunc(func(http.ResponseWriter, *http.Request) { called = true })) rec := httptest.NewRecorder() req := httptest.NewRequest(http.MethodGet, "/", nil) req.Header.Set("Authorization", "Bearer tok") h.ServeHTTP(rec, req) require.True(t, called) } func TestJWTValidator_NilReturnsError(t *testing.T) { t.Parallel() var v *JWTValidator subj, err := v.Validate(t.Context(), "anything") require.Empty(t, subj) require.Error(t, err) }