Building Supabase-like OAuth Authentication For MCP Servers

Learn how to implement OAuth2 authentication with dynamic client registration and authorization server metadata for MCP servers using a reverse proxy gateway approach.

Building Supabase-like OAuth Authentication For MCP Servers

MCP servers need OAuth2 authentication to work securely in production environments. The Model Context Protocol specification requires OAuth2 with Dynamic Client Registration (DCR) and Authorization Server Metadata (ASM) extensions - features most identity providers don’t support.

This guide shows you how to build a reverse proxy gateway that adds OAuth2 authentication to existing MCP servers without modifying their code.

The Authentication Challenge

The MCP authorization framework builds on OAuth2 2.1 draft specification with three required extensions:

  • Authorization Server Metadata (ASM) - Discovers OAuth2 endpoints
  • Dynamic Client Registration (DCR) - Creates OAuth2 clients automatically
  • Protected Resource Server (PRS) - Validates access tokens

Most identity providers support OAuth2 but lack these extensions. Here’s what we found:

ProviderDCR SupportASM SupportCORS Support
OAuth2-Proxy
Dex❌ (gRPC only)⚠️ (OIDC only)
Keycloak⚠️ (not for DCR)

Gateway Architecture

The solution uses a reverse proxy that sits between MCP clients and servers. The gateway handles authentication while forwarding requests to upstream MCP servers unchanged.

MCP Client → Gateway (Auth) → MCP Server

We’ll use Dex as the identity provider and implement missing OAuth2 extensions through its gRPC API.

Implementation Steps

1. Basic Reverse Proxy

Start with Go’s built-in reverse proxy:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
upstream, _ := url.Parse("http://localhost:8000/mcp/")

proxy := &httputil.ReverseProxy{
  Rewrite: func(r *httputil.ProxyRequest) {
    r.Out.URL = upstream
    r.Out.Host = upstream.Host
  },
}

mux := http.NewServeMux()
mux.Handle("/myserver/mcp", proxy)

2. Add CORS Support

Web-based MCP clients require CORS headers:

1
http.ListenAndServe(":9000", cors.AllowAll().Handler(mux))

3. OAuth2 Middleware

Protect the proxy endpoint by validating JWT access tokens:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
func OAuthProtected(next http.Handler) http.Handler {
  var jwkSet jwk.Set // Load from your identity provider
  
  return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    rawToken := strings.TrimSpace(strings.TrimPrefix(
      strings.TrimSpace(r.Header.Get("Authorization")), "Bearer"))
    
    if _, err := jwt.ParseString(rawToken, jwt.WithKeySet(jwkSet)); err != nil {
      w.Header().Set("WWW-Authenticate", 
        `Bearer resource_metadata="http://localhost:9000/.well-known/oauth-protected-resource"`)
      w.WriteHeader(http.StatusUnauthorized)
      return
    }
    
    next.ServeHTTP(w, r)
  })
}

4. Protected Resource Server Endpoint

Implement the PRS discovery endpoint:

1
2
3
4
5
6
7
mux.HandleFunc("/.well-known/oauth-protected-resource", func(w http.ResponseWriter, r *http.Request) {
  w.Header().Set("Content-Type", "application/json")
  json.NewEncoder(w).Encode(map[string]any{
    "resource":              "http://localhost:9000/",
    "authorization_servers": []string{"http://localhost:9000/"},
  })
})

5. Authorization Server Metadata Proxy

Most OIDC providers expose metadata at /.well-known/openid-configuration. Proxy this to the OAuth2 ASM endpoint:

1
2
3
4
5
mux.HandleFunc("/.well-known/oauth-authorization-server", func(w http.ResponseWriter, r *http.Request) {
  metadata := GetMetadata() // Fetch from OIDC provider
  w.Header().Set("Content-Type", "application/json")
  w.Write(metadata)
})

6. Dynamic Client Registration

Use Dex’s gRPC API to create OAuth2 clients on demand:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
grpcClient, _ := grpc.NewClient("localhost:5557", grpc.WithTransportCredentials(insecure.NewCredentials()))
dexClient := api.NewDexClient(grpcClient)

mux.HandleFunc("/oauth/register", func(w http.ResponseWriter, r *http.Request) {
  var body ClientInformation
  json.NewDecoder(r.Body).Decode(&body)
  
  client := api.Client{
    Id:           rand.Text(),
    Name:         body.ClientName,
    RedirectUris: body.RedirectURIs,
    Public:       true,
  }
  
  clientResponse, _ := dexClient.CreateClient(r.Context(), &api.CreateClientReq{Client: &client})
  
  w.WriteHeader(http.StatusCreated)
  json.NewEncoder(w).Encode(ClientInformation{
    ClientID:     clientResponse.Client.Id,
    ClientSecret: clientResponse.Client.Secret,
    RedirectURIs: clientResponse.Client.RedirectUris,
  })
})

7. Authorization Endpoint Proxy

Some clients don’t send required scopes. Inject them automatically:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
mux.HandleFunc("/oauth/authorize", func(w http.ResponseWriter, r *http.Request) {
  metadata := GetMetadata()
  redirectURI, _ := url.Parse(metadata["authorization_endpoint"].(string))
  
  q := r.URL.Query()
  if s := q.Get("scope"); !strings.Contains(s, "openid") {
    q.Set("scope", s+" openid")
  }
  
  redirectURI.RawQuery = q.Encode()
  http.Redirect(w, r, redirectURI.String(), http.StatusFound)
})

Configuration Example

Configure Dex with static users for testing:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
issuer: http://localhost:5556
web:
  http: 0.0.0.0:5556
  allowedOrigins: ['*']
grpc:
  addr: 0.0.0.0:5557
storage:
  type: memory
enablePasswordDB: true
staticPasswords:
  - email: '[email protected]'
    hash: '$2a$10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W'
    username: 'admin'
    userID: '08a8684b-db88-4b73-90a9-3cd1661f5466'

Common Issues

Client Type Confusion: Visual Studio Code drops client secrets, requiring public clients. The MCP Inspector needs private clients. Test with both types.

Client Persistence: DCR clients stored in memory don’t survive restarts. Use persistent storage in production.

CORS Headers: Web clients require CORS support for all OAuth2 endpoints, not just the main API.

Next Steps

You now have a working OAuth2 gateway for MCP servers. The complete implementation is available at hyprmcp/mcp-gateway with additional features like configuration management and production-ready error handling.

Test your implementation with the MCP Who Am I server to verify JWT parsing and user information extraction.