auth: New auth flow (#273)

Fixes #264. Adds endpoints:

```
/v0/auth/github    # exchange a github token for a registry token that gives you access to namespaces io.github.username and io.github.org for your orgs
/v0/auth/none     # get a registry token for namespace io.modelcontextprotocol.anonymous (only for testing/local dev purposes)
```

Updates `/v0/publish` to accept one of these registry tokens.

## Motivation and Context

See #264

## How Has This Been Tested?

- Running locally and getting a token from `/v0/auth/none` and then
publishing to `/v0/publish`
- Updating the integration tests that get a token and then publish, and
them passing

## Breaking Changes

This is a breaking change to the auth API. This is fine for registry
given it's not deployed yet.

## Types of changes
<!-- What types of changes does your code introduce? Put an `x` in all
the boxes that apply: -->
- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [x] Breaking change (fix or feature that would cause existing
functionality to change)
- [x] Documentation update

## Checklist
<!-- Go over all the following points, and put an `x` in all the boxes
that apply. -->
- [x] I have read the [MCP
Documentation](https://modelcontextprotocol.io)
- [x] My code follows the repository's style guidelines
- [x] New and existing tests pass locally
- [x] I have added appropriate error handling
- [x] I have added or updated documentation as needed

## Additional context
<!-- Add any other context, implementation notes, or design decisions
-->

---------

Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Co-authored-by: adam jones <domdomegg@users.noreply.github.com>
This commit is contained in:
adam jones 2025-08-15 13:03:35 +01:00 • committed by GitHub
commit c733cedaf8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 1759 additions and 1537 deletions

View file

@ -1,28 +0,0 @@
// Package auth provides authentication mechanisms for the MCP registry
package auth
import (
"context"
"errors"
"github.com/modelcontextprotocol/registry/internal/model"
)
var (
// ErrAuthRequired is returned when authentication is required but not provided
ErrAuthRequired = errors.New("authentication required")
// ErrUnsupportedAuthMethod is returned when an unsupported auth method is used
ErrUnsupportedAuthMethod = errors.New("unsupported authentication method")
)
// Service defines the authentication service interface
type Service interface {
// StartAuthFlow initiates an authentication flow and returns the flow information
StartAuthFlow(ctx context.Context, method model.AuthMethod, repoRef string) (map[string]string, string, error)
// CheckAuthStatus checks the status of an authentication flow using a status token
CheckAuthStatus(ctx context.Context, statusToken string) (string, error)
// ValidateAuth validates the authentication credentials
ValidateAuth(ctx context.Context, auth model.Authentication) (bool, error)
}

View file

@ -1,250 +0,0 @@
// Package auth provides authentication mechanisms for the MCP registry
package auth
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"regexp"
)
var (
// ErrAuthFailed is returned when authentication fails
ErrAuthFailed = errors.New("authentication failed")
// ErrInvalidToken is returned when a token is invalid
ErrInvalidToken = errors.New("invalid token")
// ErrMissingScope is returned when a token doesn't have the required scope
ErrMissingScope = errors.New("token missing required scope")
)
// GitHubOAuthConfig holds the configuration for GitHub OAuth
type GitHubOAuthConfig struct {
ClientID string
ClientSecret string
}
// DeviceCodeResponse represents the response from GitHub's device code endpoint
type DeviceCodeResponse struct {
DeviceCode string `json:"device_code"`
UserCode string `json:"user_code"`
VerificationURI string `json:"verification_uri"`
ExpiresIn int `json:"expires_in"`
Interval int `json:"interval"`
}
// AccessTokenResponse represents the response from GitHub's access token endpoint
type AccessTokenResponse struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
Scope string `json:"scope"`
Error string `json:"error,omitempty"`
}
// TokenValidationResponse represents the response from GitHub's token validation endpoint
type TokenValidationResponse struct {
ID int `json:"id"`
URL string `json:"url"`
Scopes []string `json:"scopes"`
SingleFile string `json:"single_file,omitempty"`
Repository string `json:"repository,omitempty"`
Fingerprint string `json:"fingerprint,omitempty"`
Error string `json:"error,omitempty"`
}
// GitHubDeviceAuth provides methods for GitHub device OAuth authentication
type GitHubDeviceAuth struct {
config GitHubOAuthConfig
}
// NewGitHubDeviceAuth creates a new GitHub device auth instance
func NewGitHubDeviceAuth(config GitHubOAuthConfig) *GitHubDeviceAuth {
return &GitHubDeviceAuth{
config: config,
}
}
// ValidateToken validates if a GitHub token has the necessary permissions to access the required repository.
// It verifies the token owner matches the repository owner or is a member of the owning organization.
// It also verifies that the token was created for the same ClientID used to set up the authentication.
// Returns true if valid, false otherwise along with an error explaining the validation failure.
func (g *GitHubDeviceAuth) ValidateToken(ctx context.Context, token string, requiredRepo string) (bool, error) {
// If no repo is required, we can't validate properly
if requiredRepo == "" {
return false, fmt.Errorf("repository reference is required for token validation")
}
// First, validate that the token is associated with our ClientID
tokenReq, err := http.NewRequestWithContext(
ctx,
http.MethodGet,
"https://api.github.com/applications/"+g.config.ClientID+"/token",
nil,
)
if err != nil {
return false, err
}
// The applications endpoint requires basic auth with client ID and secret
tokenReq.SetBasicAuth(g.config.ClientID, g.config.ClientSecret)
tokenReq.Header.Set("Accept", "application/vnd.github+json")
// Create request body with the token
type tokenCheck struct {
AccessToken string `json:"access_token"`
}
checkBody, err := json.Marshal(tokenCheck{AccessToken: token})
if err != nil {
return false, err
}
// POST instead of GET for security reasons per GitHub API
tokenURL := "https://api.github.com/applications/" + g.config.ClientID + "/token"
tokenReq, err = http.NewRequestWithContext(ctx, http.MethodPost, tokenURL, io.NopCloser(bytes.NewReader(checkBody)))
if err != nil {
return false, err
}
tokenReq.SetBasicAuth(g.config.ClientID, g.config.ClientSecret)
tokenReq.Header.Set("Accept", "application/vnd.github+json")
tokenReq.Header.Set("Content-Type", "application/json")
client := &http.Client{}
tokenResp, err := client.Do(tokenReq)
if err != nil {
return false, err
}
defer tokenResp.Body.Close()
// Check response - 200 means token is valid and associated with our app
// 404 means token is not associated with our app
if tokenResp.StatusCode != http.StatusOK {
return false, fmt.Errorf("token is not associated with this application (status: %d)", tokenResp.StatusCode)
}
var tokenInfo TokenValidationResponse
tokenRespBody, err := io.ReadAll(tokenResp.Body)
if err != nil {
return false, err
}
if err := json.Unmarshal(tokenRespBody, &tokenInfo); err != nil {
return false, err
}
// Check if there's an error in the response
if tokenInfo.Error != "" {
return false, fmt.Errorf("token validation error: %s", tokenInfo.Error)
}
// Get the authenticated user
userReq, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.github.com/user", nil)
if err != nil {
return false, err
}
userReq.Header.Set("Accept", "application/vnd.github+json")
userReq.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
client = &http.Client{}
userResp, err := client.Do(userReq)
if err != nil {
return false, err
}
defer userResp.Body.Close()
if userResp.StatusCode != http.StatusOK {
return false, fmt.Errorf("failed to get user info: status %d", userResp.StatusCode)
}
var userInfo struct {
Login string `json:"login"`
}
userBody, err := io.ReadAll(userResp.Body)
if err != nil {
return false, err
}
if err := json.Unmarshal(userBody, &userInfo); err != nil {
return false, err
}
// Extract owner from the required repo
owner, _, err := g.ExtractGitHubRepoFromName(requiredRepo)
if err != nil {
return false, err
}
// Verify that the authenticated user matches the owner
if userInfo.Login != owner {
// Check if the user is a member of the organization
isMember, err := g.checkOrgMembership(ctx, token, userInfo.Login, owner)
if err != nil {
return false, fmt.Errorf("failed to check org membership: %s", owner)
}
if !isMember {
return false, fmt.Errorf(
"token belongs to user %s, but repository is owned by %s and user is not a member of the organization",
userInfo.Login, owner)
}
}
// If we've reached this point, the token has access the repo and the user matches
// the owner or is a member of the owner org
return true, nil
}
func (g *GitHubDeviceAuth) ExtractGitHubRepoFromName(n string) (owner, repo string, err error) {
// match io.github.<owner>/<repo>
regexp := regexp.MustCompile(`io\.github\.([^/]+)/([^/]+)`)
matches := regexp.FindStringSubmatch(n)
if len(matches) != 3 {
return "", "", fmt.Errorf("invalid GitHub repository name: %s", n)
}
return matches[1], matches[2], nil
}
// extractGitHubRepo extracts the owner and repository name from a GitHub repository URL
func (g *GitHubDeviceAuth) ExtractGitHubRepo(repoURL string) (owner, repo string, err error) {
regexp := regexp.MustCompile(`github\.com/([^/]+)/([^/]+)`)
matches := regexp.FindStringSubmatch(repoURL)
if len(matches) != 3 {
return "", "", fmt.Errorf("invalid GitHub repository URL: %s", repoURL)
}
return matches[1], matches[2], nil
}
// checkOrgMembership checks if a user is a member of an organization
func (g *GitHubDeviceAuth) checkOrgMembership(ctx context.Context, token, username, org string) (bool, error) {
// Create request to check if user is a member of the organization
// GitHub API endpoint: GET /orgs/{org}/members/{username}
// true if status code is 204 No Content
// false if status code is 404 Not Found
url := fmt.Sprint("https://api.github.com/orgs/", org, "/members/", username)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return false, err
}
req.Header.Set("Accept", "application/vnd.github+json")
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return false, err
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusNoContent {
return true, nil
}
return false, fmt.Errorf("failed to check org membership: status %d", resp.StatusCode)
}

149
internal/auth/jwt.go Normal file
View file

@ -0,0 +1,149 @@
package auth
import (
"context"
"crypto/ed25519"
"encoding/hex"
"fmt"
"strings"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/modelcontextprotocol/registry/internal/config"
"github.com/modelcontextprotocol/registry/internal/model"
)
// PermissionAction represents the type of action that can be performed
type PermissionAction string
const (
PermissionActionPublish PermissionAction = "publish"
// Intended for admins taking moderation actions only, at least for now
PermissionActionEdit PermissionAction = "edit"
)
type Permission struct {
Action PermissionAction `json:"action"` // The action type (publish or edit)
ResourcePattern string `json:"resource"` // e.g., "io.github.username/*"
}
// JWTClaims represents the claims for the Registry JWT token
type JWTClaims struct {
jwt.RegisteredClaims
// Authentication method used to obtain this token
AuthMethod model.AuthMethod `json:"auth_method"`
AuthMethodSubject string `json:"auth_method_sub"`
Permissions []Permission `json:"permissions"`
}
type TokenResponse struct {
RegistryToken string `json:"registry_token"`
ExpiresAt int `json:"expires_at"`
}
// JWTManager handles JWT token operations
type JWTManager struct {
privateKey ed25519.PrivateKey
publicKey ed25519.PublicKey
tokenDuration time.Duration
}
func NewJWTManager(cfg *config.Config) *JWTManager {
seed, err := hex.DecodeString(cfg.JWTPrivateKey)
if err != nil {
panic(fmt.Sprintf("JWTPrivateKey must be a valid hex-encoded string: %v", err))
}
// Require a valid Ed25519 seed (32 bytes)
if len(seed) != ed25519.SeedSize {
panic(fmt.Sprintf("JWTPrivateKey seed must be exactly %d bytes for Ed25519, got %d bytes", ed25519.SeedSize, len(seed)))
}
// Generate the full Ed25519 key pair from the seed
privateKey := ed25519.NewKeyFromSeed(seed)
publicKey := privateKey.Public().(ed25519.PublicKey)
return &JWTManager{
privateKey: privateKey,
publicKey: publicKey,
tokenDuration: 5 * time.Minute, // 5-minute tokens as per requirements
}
}
// GenerateToken generates a new Registry JWT token
func (j *JWTManager) GenerateTokenResponse(_ context.Context, claims JWTClaims) (*TokenResponse, error) {
if claims.IssuedAt == nil {
claims.IssuedAt = jwt.NewNumericDate(time.Now())
}
if claims.ExpiresAt == nil {
claims.ExpiresAt = jwt.NewNumericDate(time.Now().Add(j.tokenDuration))
}
if claims.NotBefore == nil {
claims.NotBefore = jwt.NewNumericDate(time.Now())
}
if claims.Issuer == "" {
claims.Issuer = "mcp-registry"
}
// Create token with claims
token := jwt.NewWithClaims(&jwt.SigningMethodEd25519{}, claims)
// Sign token with Ed25519 private key
tokenString, err := token.SignedString(j.privateKey)
if err != nil {
return nil, fmt.Errorf("failed to sign token: %w", err)
}
return &TokenResponse{
RegistryToken: tokenString,
ExpiresAt: int(claims.ExpiresAt.Unix()),
}, nil
}
// ValidateToken validates a Registry JWT token and returns the claims
func (j *JWTManager) ValidateToken(_ context.Context, tokenString string) (*JWTClaims, error) {
// Parse token
// This also validates expiry
token, err := jwt.ParseWithClaims(
tokenString,
&JWTClaims{},
func(_ *jwt.Token) (interface{}, error) { return j.publicKey, nil },
jwt.WithValidMethods([]string{"EdDSA"}),
jwt.WithExpirationRequired(),
)
// Validate token
if err != nil {
return nil, fmt.Errorf("failed to parse token: %w", err)
}
if !token.Valid {
return nil, fmt.Errorf("invalid token")
}
// Extract claims
claims, ok := token.Claims.(*JWTClaims)
if !ok {
return nil, fmt.Errorf("invalid token claims")
}
return claims, nil
}
func (j *JWTManager) HasPermission(resource string, action PermissionAction, permissions []Permission) bool {
for _, perm := range permissions {
if perm.Action == action && isResourceMatch(resource, perm.ResourcePattern) {
return true
}
}
return false
}
func isResourceMatch(resource, pattern string) bool {
if pattern == "*" {
return true
}
if strings.HasSuffix(pattern, "*") {
return strings.HasPrefix(resource, strings.TrimSuffix(pattern, "*"))
}
return resource == pattern
}

285
internal/auth/jwt_test.go Normal file
View file

@ -0,0 +1,285 @@
package auth_test
import (
"context"
"crypto/ed25519"
"crypto/rand"
"encoding/hex"
"testing"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/modelcontextprotocol/registry/internal/auth"
"github.com/modelcontextprotocol/registry/internal/config"
"github.com/modelcontextprotocol/registry/internal/model"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestJWTManager_GenerateAndVerifyToken(t *testing.T) {
// Generate a proper Ed25519 seed for testing
testSeed := make([]byte, ed25519.SeedSize)
_, err := rand.Read(testSeed)
require.NoError(t, err)
cfg := &config.Config{
JWTPrivateKey: hex.EncodeToString(testSeed),
}
jwtManager := auth.NewJWTManager(cfg)
ctx := context.Background()
t.Run("generate and verify valid token", func(t *testing.T) {
claims := auth.JWTClaims{
AuthMethod: model.AuthMethodGitHub,
AuthMethodSubject: "testuser",
Permissions: []auth.Permission{
{
Action: auth.PermissionActionPublish,
ResourcePattern: "io.github.testuser/*",
},
},
}
// Generate token
tokenResponse, err := jwtManager.GenerateTokenResponse(ctx, claims)
require.NoError(t, err)
assert.NotEmpty(t, tokenResponse.RegistryToken)
assert.Greater(t, tokenResponse.ExpiresAt, 0)
// Verify token
verifiedClaims, err := jwtManager.ValidateToken(ctx, tokenResponse.RegistryToken)
require.NoError(t, err)
assert.Equal(t, model.AuthMethodGitHub, verifiedClaims.AuthMethod)
assert.Equal(t, "testuser", verifiedClaims.AuthMethodSubject)
assert.Equal(t, "mcp-registry", verifiedClaims.Issuer)
assert.Len(t, verifiedClaims.Permissions, 1)
assert.Equal(t, auth.PermissionActionPublish, verifiedClaims.Permissions[0].Action)
assert.Equal(t, "io.github.testuser/*", verifiedClaims.Permissions[0].ResourcePattern)
})
t.Run("token with custom claims", func(t *testing.T) {
issuedAt := jwt.NewNumericDate(time.Now().Add(-1 * time.Minute))
expiresAt := jwt.NewNumericDate(time.Now().Add(10 * time.Minute))
notBefore := jwt.NewNumericDate(time.Now().Add(-30 * time.Second))
claims := auth.JWTClaims{
RegisteredClaims: jwt.RegisteredClaims{
IssuedAt: issuedAt,
ExpiresAt: expiresAt,
NotBefore: notBefore,
Issuer: "custom-issuer",
},
AuthMethod: model.AuthMethodNone,
AuthMethodSubject: "anonymous",
Permissions: []auth.Permission{
{
Action: auth.PermissionActionEdit,
ResourcePattern: "*",
},
},
}
// Generate token
tokenResponse, err := jwtManager.GenerateTokenResponse(ctx, claims)
require.NoError(t, err)
// Verify token
verifiedClaims, err := jwtManager.ValidateToken(ctx, tokenResponse.RegistryToken)
require.NoError(t, err)
assert.Equal(t, model.AuthMethodNone, verifiedClaims.AuthMethod)
assert.Equal(t, "anonymous", verifiedClaims.AuthMethodSubject)
assert.Equal(t, "custom-issuer", verifiedClaims.Issuer)
assert.Equal(t, issuedAt.Unix(), verifiedClaims.IssuedAt.Unix())
assert.Equal(t, expiresAt.Unix(), verifiedClaims.ExpiresAt.Unix())
assert.Equal(t, notBefore.Unix(), verifiedClaims.NotBefore.Unix())
})
t.Run("expired token should fail validation", func(t *testing.T) {
claims := auth.JWTClaims{
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(-1 * time.Hour)), // Already expired
},
AuthMethod: model.AuthMethodGitHub,
AuthMethodSubject: "testuser",
}
// Generate token
tokenResponse, err := jwtManager.GenerateTokenResponse(ctx, claims)
require.NoError(t, err)
// Verify token should fail
_, err = jwtManager.ValidateToken(ctx, tokenResponse.RegistryToken)
assert.Error(t, err)
assert.Contains(t, err.Error(), "failed to parse token")
})
t.Run("invalid token signature should fail", func(t *testing.T) {
// Create a different seed
differentSeed := make([]byte, ed25519.SeedSize)
_, err := rand.Read(differentSeed)
require.NoError(t, err)
differentCfg := &config.Config{
JWTPrivateKey: hex.EncodeToString(differentSeed),
}
differentJWTManager := auth.NewJWTManager(differentCfg)
claims := auth.JWTClaims{
AuthMethod: model.AuthMethodGitHub,
AuthMethodSubject: "testuser",
}
// Generate token with different key
tokenResponse, err := differentJWTManager.GenerateTokenResponse(ctx, claims)
require.NoError(t, err)
// Try to verify with original key - should fail
_, err = jwtManager.ValidateToken(ctx, tokenResponse.RegistryToken)
assert.Error(t, err)
assert.Contains(t, err.Error(), "failed to parse token")
})
t.Run("malformed token should fail", func(t *testing.T) {
// Try to validate a malformed token
_, err := jwtManager.ValidateToken(ctx, "not.a.valid.token")
assert.Error(t, err)
assert.Contains(t, err.Error(), "failed to parse token")
})
t.Run("multiple permissions", func(t *testing.T) {
claims := auth.JWTClaims{
AuthMethod: model.AuthMethodGitHub,
AuthMethodSubject: "admin",
Permissions: []auth.Permission{
{
Action: auth.PermissionActionPublish,
ResourcePattern: "io.github.admin/*",
},
{
Action: auth.PermissionActionEdit,
ResourcePattern: "*",
},
{
Action: auth.PermissionActionPublish,
ResourcePattern: "io.github.org/*",
},
},
}
// Generate token
tokenResponse, err := jwtManager.GenerateTokenResponse(ctx, claims)
require.NoError(t, err)
// Verify token
verifiedClaims, err := jwtManager.ValidateToken(ctx, tokenResponse.RegistryToken)
require.NoError(t, err)
assert.Len(t, verifiedClaims.Permissions, 3)
assert.Equal(t, auth.PermissionActionPublish, verifiedClaims.Permissions[0].Action)
assert.Equal(t, auth.PermissionActionEdit, verifiedClaims.Permissions[1].Action)
assert.Equal(t, auth.PermissionActionPublish, verifiedClaims.Permissions[2].Action)
})
}
func TestJWTManager_HasPermission(t *testing.T) {
// Generate a proper Ed25519 seed for testing
testSeed := make([]byte, ed25519.SeedSize)
_, err := rand.Read(testSeed)
require.NoError(t, err)
cfg := &config.Config{
JWTPrivateKey: hex.EncodeToString(testSeed),
}
jwtManager := auth.NewJWTManager(cfg)
tests := []struct {
name string
resource string
action auth.PermissionAction
permissions []auth.Permission
expected bool
}{
{
name: "exact match",
resource: "io.github.testuser/server1",
action: auth.PermissionActionPublish,
permissions: []auth.Permission{
{Action: auth.PermissionActionPublish, ResourcePattern: "io.github.testuser/server1"},
},
expected: true,
},
{
name: "wildcard match",
resource: "io.github.testuser/server2",
action: auth.PermissionActionPublish,
permissions: []auth.Permission{
{Action: auth.PermissionActionPublish, ResourcePattern: "io.github.testuser/*"},
},
expected: true,
},
{
name: "global wildcard",
resource: "any.resource.here",
action: auth.PermissionActionEdit,
permissions: []auth.Permission{
{Action: auth.PermissionActionEdit, ResourcePattern: "*"},
},
expected: true,
},
{
name: "wrong action",
resource: "io.github.testuser/server1",
action: auth.PermissionActionEdit,
permissions: []auth.Permission{
{Action: auth.PermissionActionPublish, ResourcePattern: "io.github.testuser/*"},
},
expected: false,
},
{
name: "no match",
resource: "io.github.otheruser/server1",
action: auth.PermissionActionPublish,
permissions: []auth.Permission{
{Action: auth.PermissionActionPublish, ResourcePattern: "io.github.testuser/*"},
},
expected: false,
},
{
name: "multiple permissions with match",
resource: "io.github.org/server1",
action: auth.PermissionActionPublish,
permissions: []auth.Permission{
{Action: auth.PermissionActionPublish, ResourcePattern: "io.github.testuser/*"},
{Action: auth.PermissionActionEdit, ResourcePattern: "*"},
{Action: auth.PermissionActionPublish, ResourcePattern: "io.github.org/*"},
},
expected: true,
},
{
name: "empty permissions",
resource: "io.github.testuser/server1",
action: auth.PermissionActionPublish,
permissions: []auth.Permission{},
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := jwtManager.HasPermission(tt.resource, tt.action, tt.permissions)
assert.Equal(t, tt.expected, result)
})
}
}
func TestNewJWTManager_InvalidKeySize(t *testing.T) {
// Test with invalid key size (should panic)
cfg := &config.Config{
JWTPrivateKey: "too-short-key",
}
assert.Panics(t, func() {
auth.NewJWTManager(cfg)
})
}

View file

@ -1,38 +0,0 @@
//go:build noauth
package auth
import (
"context"
"fmt"
"os"
"github.com/modelcontextprotocol/registry/internal/config"
"github.com/modelcontextprotocol/registry/internal/model"
)
func init() {
fmt.Fprintln(os.Stderr, `WARNING: "noauth" build tag has disabled authentication`)
}
// NewAuthService creates a new authentication service that does nothing. All its methods succeed unconditionally.
func NewAuthService(*config.Config) Service {
return &noAuth{}
}
type noAuth struct{}
// StartAuthFlow always returns fake flow info and a fake status token
func (*noAuth) StartAuthFlow(context.Context, model.AuthMethod, string) (map[string]string, string, error) {
return map[string]string{"fake": "info"}, "fake-status-token", nil
}
// CheckAuthStatus always returns a fake token
func (*noAuth) CheckAuthStatus(context.Context, string) (string, error) {
return "fake-token", nil
}
// ValidateAuth always returns true
func (*noAuth) ValidateAuth(context.Context, model.Authentication) (bool, error) {
return true, nil
}

View file

@ -1,61 +0,0 @@
//go:build !noauth
package auth
import (
"context"
"fmt"
"github.com/modelcontextprotocol/registry/internal/config"
"github.com/modelcontextprotocol/registry/internal/model"
)
// ServiceImpl implements the Service interface
type ServiceImpl struct {
config *config.Config
githubAuth *GitHubDeviceAuth
}
// NewAuthService creates a new authentication service
//
//nolint:ireturn // Factory function intentionally returns interface for dependency injection
func NewAuthService(cfg *config.Config) Service {
githubConfig := GitHubOAuthConfig{
ClientID: cfg.GithubClientID,
ClientSecret: cfg.GithubClientSecret,
}
return &ServiceImpl{
config: cfg,
githubAuth: NewGitHubDeviceAuth(githubConfig),
}
}
func (s *ServiceImpl) StartAuthFlow(_ context.Context, _ model.AuthMethod,
_ string) (map[string]string, string, error) {
// return not implemented error
return nil, "", fmt.Errorf("not implemented")
}
func (s *ServiceImpl) CheckAuthStatus(_ context.Context, _ string) (string, error) {
// return not implemented error
return "", fmt.Errorf("not implemented")
}
// ValidateAuth validates authentication credentials
func (s *ServiceImpl) ValidateAuth(ctx context.Context, auth model.Authentication) (bool, error) {
// If authentication is required but not provided
if auth.Method == "" || auth.Method == model.AuthMethodNone {
return false, ErrAuthRequired
}
switch auth.Method {
case model.AuthMethodGitHub:
// Extract repo reference from the repository URL if it's not provided
return s.githubAuth.ValidateToken(ctx, auth.Token, auth.RepoRef)
case model.AuthMethodNone:
return false, ErrAuthRequired
default:
return false, ErrUnsupportedAuthMethod
}
}