registry/internal/api/handlers/v0/auth/oidc_test.go
adam jones 5746e7e99b
Remove OIDC authorization flow to fix data race vulnerability (#526)
## Summary

Fixes a concurrent map access vulnerability in the OIDC handler that
could cause remote unauthenticated DoS.

## Changes

- Removed OIDC authorization flow endpoints ( and )
- Removed the unsynchronized  map that caused the data race
- Kept only the OIDC token exchange endpoint ()
- Simplified  by removing OAuth2 flow dependencies

The map was being accessed concurrently by the start (write) and
callback (read/delete) endpoints without synchronization, causing
runtime panics. Since the token exchange flow doesn't require session
state, removing the authorization flow eliminates the vulnerability
entirely.
2025-09-22 13:41:32 +01:00

107 lines
3.1 KiB
Go

package auth_test
import (
"context"
"fmt"
"testing"
"github.com/modelcontextprotocol/registry/internal/api/handlers/v0/auth"
"github.com/modelcontextprotocol/registry/internal/config"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// MockGenericOIDCValidator for testing
type MockGenericOIDCValidator struct {
validateFunc func(ctx context.Context, token string) (*auth.OIDCClaims, error)
}
func (m *MockGenericOIDCValidator) ValidateToken(ctx context.Context, token string) (*auth.OIDCClaims, error) {
if m.validateFunc != nil {
return m.validateFunc(ctx, token)
}
return nil, fmt.Errorf("not implemented")
}
func TestOIDCHandler_ExchangeToken(t *testing.T) {
tests := []struct {
name string
config *config.Config
mockValidator *MockGenericOIDCValidator
token string
expectedError bool
}{
{
name: "successful token exchange with publish permissions",
config: &config.Config{
OIDCEnabled: true,
OIDCIssuer: "https://accounts.google.com",
OIDCClientID: "test-client-id",
OIDCExtraClaims: `[{"hd":"modelcontextprotocol.io"}]`,
OIDCPublishPerms: "*",
JWTPrivateKey: "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", // 32 byte hex
},
mockValidator: &MockGenericOIDCValidator{
validateFunc: func(_ context.Context, _ string) (*auth.OIDCClaims, error) {
return &auth.OIDCClaims{
ExtraClaims: map[string]any{
"email": "admin@modelcontextprotocol.io",
"email_verified": true,
"hd": "modelcontextprotocol.io",
"preferred_username": "admin",
},
}, nil
},
},
token: "valid-oidc-token",
expectedError: false,
},
{
name: "failed validation with invalid hosted domain",
config: &config.Config{
OIDCEnabled: true,
OIDCIssuer: "https://accounts.google.com",
OIDCClientID: "test-client-id",
OIDCExtraClaims: `[{"hd":"modelcontextprotocol.io"}]`,
OIDCPublishPerms: "*",
JWTPrivateKey: "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
},
mockValidator: &MockGenericOIDCValidator{
validateFunc: func(_ context.Context, _ string) (*auth.OIDCClaims, error) {
return &auth.OIDCClaims{
ExtraClaims: map[string]any{
"email": "user@example.com",
"email_verified": true,
"hd": "example.com", // Wrong domain
"preferred_username": "user",
},
}, nil
},
},
token: "invalid-domain-token",
expectedError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
handler := auth.NewOIDCHandler(tt.config)
if tt.mockValidator != nil {
handler.SetValidator(tt.mockValidator)
}
ctx := context.Background()
response, err := handler.ExchangeToken(ctx, tt.token)
if tt.expectedError {
assert.Error(t, err)
assert.Nil(t, response)
} else {
require.NoError(t, err)
require.NotNil(t, response)
assert.NotEmpty(t, response.RegistryToken)
assert.Greater(t, response.ExpiresAt, 0)
}
})
}
}