feat(auth): JWT-or-static middleware + /.well-known/oauth-protected-resource (issue #5)
Some checks failed
CD / Lint / Test / Vet (push) Failing after 2s
CD / Build & Import (push) Has been skipped
CD / Deploy via GitOps (push) Has been skipped

- internal/auth/jwt.go: JWTValidator via lestrrat-go/jwx/v2, JWKS auto-refresh
- internal/auth/bearer.go: replace Gitea PAT validation with JWT->static->default chain
- internal/gitea/client.go: always use service PAT; remove TokenFromContext lookup
- internal/config/config.go: add DexIssuerURL, MCPAudience, MCPResourceURL, StaticToken
- cmd/gitea-mcp/main.go: wire validator, fix /.well-known to return real AS list
- bearer_test.go: rewrite for new API
This commit is contained in:
Mathias Bergqvist
2026-05-12 11:30:52 +02:00
parent efbbd37882
commit 91be18c100
20 changed files with 1745 additions and 114 deletions

View File

@@ -10,8 +10,13 @@ import (
"github.com/stretchr/testify/require"
)
func TestBearerMiddleware_NoAuthHeader(t *testing.T) {
srv := httptest.NewServer(auth.BearerMiddleware("https://gitea.example.com", "",
// helper: BearerMiddleware with no JWT validator and no static token
func noJWTMiddleware(defaultToken string, next http.Handler) http.Handler {
return auth.BearerMiddleware(nil, "", defaultToken, next)
}
func TestBearerMiddleware_NoAuthHeader_NoDefault(t *testing.T) {
srv := httptest.NewServer(noJWTMiddleware("",
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
}),
@@ -24,20 +29,11 @@ func TestBearerMiddleware_NoAuthHeader(t *testing.T) {
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
}
func TestBearerMiddleware_NoAuthHeaderWithDefault(t *testing.T) {
const defaultToken = "default-pat"
giteaMock := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "token "+defaultToken, r.Header.Get("Authorization"))
w.WriteHeader(http.StatusOK)
}))
defer giteaMock.Close()
func TestBearerMiddleware_NoAuthHeader_WithDefault(t *testing.T) {
called := false
srv := httptest.NewServer(auth.BearerMiddleware(giteaMock.URL, defaultToken,
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
srv := httptest.NewServer(noJWTMiddleware("default-pat",
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
called = true
assert.Equal(t, defaultToken, auth.TokenFromContext(r.Context()))
w.WriteHeader(http.StatusOK)
}),
))
@@ -50,51 +46,19 @@ func TestBearerMiddleware_NoAuthHeaderWithDefault(t *testing.T) {
assert.True(t, called)
}
func TestBearerMiddleware_InvalidToken(t *testing.T) {
// Mock Gitea that rejects the token
giteaMock := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusUnauthorized)
}))
defer giteaMock.Close()
srv := httptest.NewServer(auth.BearerMiddleware(giteaMock.URL, "",
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
}),
))
defer srv.Close()
req, _ := http.NewRequest(http.MethodPost, srv.URL+"/mcp", nil)
req.Header.Set("Authorization", "Bearer bad-token")
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
defer func() { _ = resp.Body.Close() }()
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
}
func TestBearerMiddleware_ValidToken(t *testing.T) {
const token = "valid-pat"
// Mock Gitea that accepts the token and returns a user
giteaMock := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "token "+token, r.Header.Get("Authorization"))
w.WriteHeader(http.StatusOK)
}))
defer giteaMock.Close()
func TestBearerMiddleware_StaticToken_Valid(t *testing.T) {
const staticToken = "my-static-token"
called := false
srv := httptest.NewServer(auth.BearerMiddleware(giteaMock.URL, "",
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
srv := httptest.NewServer(auth.BearerMiddleware(nil, staticToken, "",
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
called = true
// Token must be available in context for downstream Gitea client
assert.Equal(t, token, auth.TokenFromContext(r.Context()))
w.WriteHeader(http.StatusOK)
}),
))
defer srv.Close()
req, _ := http.NewRequest(http.MethodPost, srv.URL+"/mcp", nil)
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Authorization", "Bearer "+staticToken)
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
defer func() { _ = resp.Body.Close() }()
@@ -102,7 +66,34 @@ func TestBearerMiddleware_ValidToken(t *testing.T) {
assert.True(t, called)
}
func TestTokenFromContext_Empty(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/", nil)
assert.Equal(t, "", auth.TokenFromContext(req.Context()))
func TestBearerMiddleware_StaticToken_Invalid(t *testing.T) {
srv := httptest.NewServer(auth.BearerMiddleware(nil, "correct-token", "",
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
}),
))
defer srv.Close()
req, _ := http.NewRequest(http.MethodPost, srv.URL+"/mcp", nil)
req.Header.Set("Authorization", "Bearer wrong-token")
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
defer func() { _ = resp.Body.Close() }()
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
}
func TestBearerMiddleware_UnknownBearer_NoJWT(t *testing.T) {
srv := httptest.NewServer(noJWTMiddleware("",
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
}),
))
defer srv.Close()
req, _ := http.NewRequest(http.MethodPost, srv.URL+"/mcp", nil)
req.Header.Set("Authorization", "Bearer random-unknown-token")
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
defer func() { _ = resp.Body.Close() }()
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
}