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>
This commit is contained in:
114
auth/bearer_test.go
Normal file
114
auth/bearer_test.go
Normal file
@@ -0,0 +1,114 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user