Files
mcp-chassis/auth/bearer_test.go
Mathias 67decddc8a feat: initial mcp-chassis with auth primitives
Shared Go library for Mathias-owned MCP servers, born from spike S3 of
the 2026-05 homelab architecture review (see
gitea.d-ma.be/mathias/infra/docs/superpowers/handoffs/2026-05-22-mcp-chassis-spike.md
for the viability assessment and abort-criterion check).

Provides three primitives every MCP server today re-implements:

- auth.JWTValidator — Dex OIDC JWT validation. nil-safe (nil = "JWT
  disabled"), audience-optional. Lifted from the identical
  ~80-LOC implementations in gitea-mcp and hyperguild/ingestion.
- auth.BearerMiddleware — dual-mode static-Bearer-or-Dex-JWT gate.
  Static wins first to avoid emitting a WWW-Authenticate challenge
  that would flip claude.ai's MCP client into OAuth discovery for
  static-only deployments. The fall-through 401 emits the RFC 9728
  resource_metadata header only when explicitly configured.
- auth.ProtectedResourceHandler — RFC 9728
  /.well-known/oauth-protected-resource metadata document handler.

Test coverage exercises every branch (static OK, JWT-disabled,
empty bearer, wrong static, with-challenge vs without-challenge,
nil-validator-Validate). go test -race clean.

Deps: github.com/lestrrat-go/jwx/v2 (already a dep of every consumer)
and testify (test-only). No new transitive deps.

First migration target: gitea-mcp. If that port lands in one PR
(abort criterion from spec), brain-mcp (ingestion) follows. Otherwise
chassis reverts per the spec.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 09:07:53 +02:00

115 lines
3.2 KiB
Go

package auth
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/require"
)
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)
}