# OAuth MCP proxy

OAuth 2.1 authentication library for Go MCP servers.

**Supports both MCP SDKs:**

* ✅ `mark3labs/mcp-go`
* ✅ `modelcontextprotocol/go-sdk` (official)

**One-time setup:** Configure provider + add `WithOAuth()` to your server. **Result:** All tools automatically protected with token validation and caching.

### mark3labs/mcp-go

```go
import "github.com/tuannvm/oauth-mcp-proxy/mark3labs"

oauthServer, oauthOption, _ := mark3labs.WithOAuth(mux, &oauth.Config{
    Provider: "okta",
    Issuer:   "https://your-company.okta.com",
    Audience: "api://your-mcp-server",
})

mcpServer := server.NewMCPServer("Server", "1.0.0", oauthOption)
streamable := server.NewStreamableHTTPServer(mcpServer, /*options*/)
mux.HandleFunc("/mcp", oauthServer.WrapMCPEndpoint(streamable))
```

### Official SDK

```go
import mcpoauth "github.com/tuannvm/oauth-mcp-proxy/mcp"

mcpServer := mcp.NewServer(&mcp.Implementation{...}, nil)
_, handler, _ := mcpoauth.WithOAuth(mux, cfg, mcpServer)
http.ListenAndServe(":8080", handler)
```

[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/tuannvm/oauth-mcp-proxy/test.yml?branch=main\&label=Tests\&logo=github)](https://github.com/tuannvm/oauth-mcp-proxy/actions/workflows/test.yml) [![Go Version](https://img.shields.io/github/go-mod/go-version/tuannvm/oauth-mcp-proxy?logo=go)](https://github.com/tuannvm/oauth-mcp-proxy/blob/main/go.mod) [![Go Report Card](https://goreportcard.com/badge/github.com/tuannvm/oauth-mcp-proxy)](https://goreportcard.com/report/github.com/tuannvm/oauth-mcp-proxy) [![Go Reference](https://pkg.go.dev/badge/github.com/tuannvm/oauth-mcp-proxy.svg)](https://pkg.go.dev/github.com/tuannvm/oauth-mcp-proxy) [![GitHub Release](https://img.shields.io/github/v/release/tuannvm/oauth-mcp-proxy?sort=semver)](https://github.com/tuannvm/oauth-mcp-proxy/releases/latest) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

***

## Why Use This Library?

* **Dual SDK support** - Works with both mark3labs and official SDKs
* **Simple integration** - One `WithOAuth()` call protects all tools
* **Automatic 401 handling** - RFC 6750 compliant error responses with OAuth discovery
* **Zero per-tool config** - All tools automatically protected
* **Fast token caching** - 5-min cache with JWT expiry awareness
* **Security hardened** - State replay protection, DoS prevention, input validation
* **Built-in rate limiting** - Token-based rate limiter included
* **CORS support** - OPTIONS pass-through for browser clients
* **Multiple providers** - HMAC, Okta, Google, Azure AD

***

## How It Works

### Request Flow

{% @mermaid/diagram content="sequenceDiagram
participant Client
participant MCP Server
box lightyellow oauth-mcp-proxy Library
participant Middleware
participant Cache
participant Provider
end
participant Your Tool Handler

```
Client->>MCP Server: Request + Bearer token
MCP Server->>Middleware: WithOAuth() intercepts

alt Token in cache and fresh
    Middleware->>Cache: Check token hash
    Cache-->>Middleware: Return cached user
else Token not cached or expired
    Middleware->>Provider: Validate token (HMAC/OIDC)
    Provider-->>Middleware: User claims
    Middleware->>Cache: Store user for 5 minutes
end

Middleware->>Your Tool Handler: Pass request with user in context
Your Tool Handler->>Your Tool Handler: GetUserFromContext(ctx)
Your Tool Handler-->>Client: Send response" %}
```

### Token Validation Flow

{% @mermaid/diagram content="flowchart TB
Start(\[Your MCP Server receives request]) --> Extract\[oauth-mcp-proxy: Extract Token]
Extract --> Hash\[oauth-mcp-proxy: SHA-256 Hash]
Hash --> CheckCache{oauth-mcp-proxy: Token Cached?}

```
CheckCache -->|Cache Hit| GetUser[oauth-mcp-proxy: Get Cached User]
CheckCache -->|Cache Miss| Validate{oauth-mcp-proxy: Validate}

Validate -->|Valid| Claims[oauth-mcp-proxy: Extract Claims]
Validate -->|Invalid| Reject([Return 401])

Claims --> Store[oauth-mcp-proxy: Cache]
Store --> GetUser

GetUser --> Context[oauth-mcp-proxy: Add User to Context]
Context --> Tool[Your Tool Handler: GetUserFromContext]
Tool --> Response([Your MCP Server: Return Response])

style Start fill:#e8f5e9
style Extract fill:#fff9c4
style Hash fill:#fff9c4
style CheckCache fill:#fff9c4
style Validate fill:#fff9c4
style Claims fill:#fff9c4
style Store fill:#fff9c4
style GetUser fill:#fff9c4
style Context fill:#fff9c4
style Tool fill:#e8f5e9
style Response fill:#e8f5e9
style Reject fill:#ffebee" %}
```

**What oauth-mcp-proxy does:**

1. Extracts Bearer tokens from HTTP requests
2. Validates against your OAuth provider (with caching)
3. Adds authenticated user to request context
4. All your tools automatically protected

***

## 🔒 Security Features

Production-ready security hardening built-in:

### State Replay Protection

* **Timestamp + nonce validation** - States include timestamp and nonce for replay attack prevention
* **Automatic nonce cleanup** - Expired nonces removed before replay check (prevents memory leaks)
* **Rolling deploy compatible** - Accepts legacy states without timestamp/nonce for zero-downtime upgrades

### Token Security

* **JWT expiry-aware caching** - Cache respects token expiration time (uses min(token.expiry, now+5min))
* **Constant-time HMAC comparison** - Timing attack prevention for signature verification
* **Secure nonce generation** - Panics on crypto/rand failure (no weak fallback)

### Input Validation & DoS Prevention

* **Parameter length limits** - code, state, code\_challenge validated to prevent abuse
* **Request body size limits** - MaxBytesReader on token endpoint (1MB), registration (256KB)
* **Issuer URL validation** - Enforced HTTPS for non-localhost OIDC providers

### Session Management (Official SDK)

* **auth.TokenInfo population** - Populates go-sdk auth context for session binding
* **User-based session tracking** - Prevents session hijacking via user ID verification

### HTTP Security

* **Security headers** - CSP, X-Frame-Options, X-Content-Type-Options, Cache-Control
* **CORS support** - OPTIONS pass-through for browser clients
* **RFC 6750 compliant** - Proper WWW-Authenticate headers with resource\_metadata

### Built-in Rate Limiting

```go
// Simple token-based rate limiter included
limiter := oauth.NewRateLimiter(time.Minute, 100)
if !limiter.Allow("client-ip") {
    http.Error(w, "Rate limit exceeded", http.StatusTooManyRequests)
}
```

***

## Breaking Changes (Security Hardening)

### v1.0.0 → v1.1.0

The following security improvements introduce **breaking changes**:

**1. Issuer URL Validation (CRITICAL)**

* **Change**: OIDC providers now enforce HTTPS validation for issuer URLs
* **Impact**: Invalid issuer URLs will cause `NewServer()` to fail
* **Migration**: Ensure your `Issuer` config uses HTTPS (or localhost for testing)

  ```go
  // ✅ Valid
  Issuer: "https://company.okta.com"
  Issuer: "http://localhost:8080"  // Testing only

  // ❌ Invalid - will fail validation
  Issuer: "http://company.okta.com"  // Not localhost
  Issuer: "company.okta.com"         // Missing scheme
  ```

**2. State Signing Key Initialization**

* **Change**: `NewServer()` now panics if state signing key cannot be generated
* **Impact**: Server startup will fail if crypto/rand fails (should never happen on healthy systems)
* **Migration**: Ensure your system has a working CSPRNG. No code changes needed.

**3. Nonce Generation Failure Behavior**

* **Change**: `generateSecureNonce()` now panics instead of falling back to weak timestamp-based nonces
* **Impact**: OAuth authorization requests will fail if crypto/rand fails
* **Migration**: Ensure your system has a working CSPRNG. No code changes needed.

**4. Error Message Simplification**

* **Change**: Security-sensitive error messages are less verbose to prevent information leakage
* **Impact**: Debugging authentication failures may require checking logs
* **Migration**: Use server logs for detailed debugging; client errors are intentionally generic

### No Migration Needed For

* **Token cache expiry fix** - Fully backwards compatible
* **State replay protection** - Legacy states without timestamp/nonce still accepted
* **Input validation** - Only affects malformed requests
* **go-sdk adapter fixes** - Fully backwards compatible

***

## Quick Start

### Using mark3labs/mcp-go

#### 1. Install

```bash
go get github.com/tuannvm/oauth-mcp-proxy
```

#### 2. Add to Your Server

```go
import (
    oauth "github.com/tuannvm/oauth-mcp-proxy"
    "github.com/tuannvm/oauth-mcp-proxy/mark3labs"
)

mux := http.NewServeMux()

// Enable OAuth (one time setup)
oauthServer, oauthOption, _ := mark3labs.WithOAuth(mux, &oauth.Config{
    Provider: "okta",                    // or "hmac", "google", "azure"
    Issuer:   "https://your-company.okta.com",
    Audience: "api://your-mcp-server",
    ServerURL: "https://your-server.com",
})

// Create MCP server with OAuth
mcpServer := mcpserver.NewMCPServer("Server", "1.0.0", oauthOption)

// Add tools - all automatically protected
mcpServer.AddTool(myTool, myHandler)

// Setup endpoint with automatic 401 handling
streamable := mcpserver.NewStreamableHTTPServer(
    mcpServer,
    mcpserver.WithHTTPContextFunc(oauth.CreateHTTPContextFunc()),
)
mux.HandleFunc("/mcp", oauthServer.WrapMCPEndpoint(streamable))
```

#### 3. Access Authenticated User

```go
func myHandler(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
    user, ok := oauth.GetUserFromContext(ctx)
    if !ok {
        return nil, fmt.Errorf("authentication required")
    }
    // Use user.Username, user.Email, user.Subject
}
```

***

### Using Official SDK

#### 1. Install

```bash
go get github.com/modelcontextprotocol/go-sdk
go get github.com/tuannvm/oauth-mcp-proxy
```

#### 2. Add to Your Server

```go
import (
    "github.com/modelcontextprotocol/go-sdk/mcp"
    oauth "github.com/tuannvm/oauth-mcp-proxy"
    mcpoauth "github.com/tuannvm/oauth-mcp-proxy/mcp"
)

mux := http.NewServeMux()

// Create MCP server
mcpServer := mcp.NewServer(&mcp.Implementation{
    Name:    "my-server",
    Version: "1.0.0",
}, nil)

// Add tools
mcp.AddTool(mcpServer, &mcp.Tool{
    Name: "greet",
    Description: "Greet user",
}, func(ctx context.Context, req *mcp.CallToolRequest, params *struct{}) (*mcp.CallToolResult, any, error) {
    user, _ := oauth.GetUserFromContext(ctx)
    return &mcp.CallToolResult{
        Content: []mcp.Content{
            &mcp.TextContent{Text: "Hello, " + user.Username},
        },
    }, nil, nil
})

// Add OAuth protection
_, handler, _ := mcpoauth.WithOAuth(mux, &oauth.Config{
    Provider: "okta",
    Issuer:   "https://your-company.okta.com",
    Audience: "api://your-mcp-server",
}, mcpServer)

http.ListenAndServe(":8080", handler)
```

Your MCP server now requires OAuth authentication.

***

## Examples

See [examples/README.md](https://docs.tuannvm.com/oauth-mcp-proxy/examples) for detailed setup guide including Okta configuration.

| SDK           | Example                                                                                                | Description                            |
| ------------- | ------------------------------------------------------------------------------------------------------ | -------------------------------------- |
| **mark3labs** | [Simple](https://github.com/tuannvm/oauth-mcp-proxy/blob/main/examples/mark3labs/simple/README.md)     | Minimal setup - copy/paste ready       |
| **mark3labs** | [Advanced](https://github.com/tuannvm/oauth-mcp-proxy/blob/main/examples/mark3labs/advanced/README.md) | ConfigBuilder, multiple tools, logging |
| **Official**  | [Simple](https://github.com/tuannvm/oauth-mcp-proxy/blob/main/examples/official/simple/README.md)      | Minimal setup - copy/paste ready       |
| **Official**  | [Advanced](https://github.com/tuannvm/oauth-mcp-proxy/blob/main/examples/official/advanced/README.md)  | ConfigBuilder, multiple tools, logging |

***

## Supported Providers

| Provider     | Best For             | Setup Guide                                                                                |
| ------------ | -------------------- | ------------------------------------------------------------------------------------------ |
| **HMAC**     | Testing, development | [docs/providers/HMAC.md](https://docs.tuannvm.com/oauth-mcp-proxy/docs/providers/hmac)     |
| **Okta**     | Enterprise SSO       | [docs/providers/OKTA.md](https://docs.tuannvm.com/oauth-mcp-proxy/docs/providers/okta)     |
| **Google**   | Google Workspace     | [docs/providers/GOOGLE.md](https://docs.tuannvm.com/oauth-mcp-proxy/docs/providers/google) |
| **Azure AD** | Microsoft 365        | [docs/providers/AZURE.md](https://docs.tuannvm.com/oauth-mcp-proxy/docs/providers/azure)   |

***

## Documentation

**Getting Started:**

* [Setup Guide](https://docs.tuannvm.com/oauth-mcp-proxy/docs/client-setup) - Complete server integration and client configuration
* [Configuration Guide](https://docs.tuannvm.com/oauth-mcp-proxy/docs/configuration) - All config options
* [Provider Setup](https://github.com/tuannvm/oauth-mcp-proxy/blob/main/docs/providers/README.md) - OAuth provider guides

**Advanced:**

* [Security Guide](https://docs.tuannvm.com/oauth-mcp-proxy/docs/security) - Production best practices
* [Troubleshooting](https://docs.tuannvm.com/oauth-mcp-proxy/docs/troubleshooting) - Common issues

***

## License

MIT License - See [LICENSE](https://github.com/tuannvm/oauth-mcp-proxy/blob/main/LICENSE/README.md)


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.tuannvm.com/oauth-mcp-proxy/readme.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
