Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
address comments
  • Loading branch information
gossion committed Sep 9, 2025
commit a4a241b18867fb4df5cab6a9830553c0e5b77df9
39 changes: 37 additions & 2 deletions docs/oauth-authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,7 @@ curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:8000/mcp
- `--oauth-tenant-id`: Azure AD tenant ID (or use AZURE_TENANT_ID env var)
- `--oauth-client-id`: Azure AD client ID (or use AZURE_CLIENT_ID env var)
- `--oauth-redirects`: Comma-separated list of allowed redirect URIs (required when OAuth enabled)
- `--oauth-cors-origins`: Comma-separated list of allowed CORS origins for OAuth endpoints (e.g. http://localhost:6274 for MCP Inspector). If empty, no cross-origin requests are allowed for security

**Note**: OAuth scopes are automatically configured to use `https://management.azure.com/.default` for optimal Azure AD compatibility. Custom scopes are not currently configurable via command line.

Expand All @@ -227,8 +228,7 @@ curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:8000/mcp
```bash
./aks-mcp --transport=sse --oauth-enabled=true \
--oauth-tenant-id="12345678-1234-1234-1234-123456789012" \
--oauth-client-id="87654321-4321-4321-4321-210987654321" \
--oauth-redirects="http://localhost:8000/oauth/callback"
--oauth-client-id="87654321-4321-4321-4321-210987654321"
```

**Note**: Scopes are automatically set to `https://management.azure.com/.default` and cannot be customized via command line.
Expand Down Expand Up @@ -436,4 +436,39 @@ To migrate from a non-OAuth AKS-MCP deployment:

The MCP Inspector tool can be used to test OAuth-enabled AKS-MCP servers. Configure the Inspector's OAuth settings to match your AKS-MCP OAuth configuration for testing.

### Important: Redirect URI Configuration for MCP Inspector

When using MCP Inspector with OAuth authentication, you need to add the Inspector's proxy redirect URI to your OAuth configuration:

```bash
# Add Inspector's redirect URI (typically http://localhost:6274/oauth/callback)
./aks-mcp \
--transport=streamable-http \
--port=8000 \
--oauth-enabled \
--oauth-redirects="http://localhost:8000/oauth/callback,http://localhost:6274/oauth/callback" \
--access-level=readonly
```

**Key Points:**
- MCP Inspector typically runs on port 6274 by default
- The Inspector creates a proxy redirect URI at `/oauth/callback`
- You must include both your server's redirect URI AND the Inspector's redirect URI
- You must also configure CORS origins to allow the Inspector's web interface to make requests
- Comma-separate multiple redirect URIs in the `--oauth-redirects` parameter
- Comma-separate multiple CORS origins in the `--oauth-cors-origins` parameter
- Without the Inspector's redirect URI, OAuth authentication will fail with "redirect_uri not registered" error
- Without the Inspector's CORS origin, the web interface will be blocked by browser CORS policy

**Example with MCP Inspector configuration:**
```bash
./aks-mcp \
--transport=streamable-http \
--port=8000 \
--oauth-enabled \
--oauth-redirects="http://localhost:8000/oauth/callback,http://localhost:6274/oauth/callback" \
--oauth-cors-origins="http://localhost:6274" \
--access-level=readonly
```

For more information, see the MCP OAuth specification and Azure AD documentation.
105 changes: 80 additions & 25 deletions internal/auth/oauth/endpoints.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,15 +55,29 @@ func NewEndpointManager(provider *AzureOAuthProvider, cfg *config.ConfigData) *E
}
}

// setCORSHeaders sets CORS headers for OAuth endpoints to allow MCP Inspector access
func (em *EndpointManager) setCORSHeaders(w http.ResponseWriter) {
origin := "*" // TODO: Restrict to specific origins

w.Header().Set("Access-Control-Allow-Origin", origin)
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, mcp-protocol-version")
w.Header().Set("Access-Control-Max-Age", "86400") // 24 hours
w.Header().Set("Access-Control-Allow-Credentials", "false") // Explicit false for wildcard origin
// setCORSHeaders sets CORS headers for OAuth endpoints with origin whitelisting
func (em *EndpointManager) setCORSHeaders(w http.ResponseWriter, r *http.Request) {
requestOrigin := r.Header.Get("Origin")

// Check if the request origin is in the allowed list
var allowedOrigin string
for _, allowed := range em.provider.config.AllowedOrigins {
if requestOrigin == allowed {
allowedOrigin = requestOrigin
break
}
}

// Only set CORS headers if origin is allowed
if allowedOrigin != "" {
w.Header().Set("Access-Control-Allow-Origin", allowedOrigin)
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, mcp-protocol-version")
w.Header().Set("Access-Control-Max-Age", "86400") // 24 hours
w.Header().Set("Access-Control-Allow-Credentials", "false")
} else if requestOrigin != "" {
log.Printf("CORS ERROR: Origin %s is not in the allowed list - cross-origin requests will be blocked for security", requestOrigin)
}
}

// setCacheHeaders sets cache control headers based on EnableCache configuration
Expand Down Expand Up @@ -116,7 +130,7 @@ func (em *EndpointManager) authServerMetadataProxyHandler() http.HandlerFunc {
log.Printf("OAuth DEBUG: Received request for authorization server metadata: %s %s", r.Method, r.URL.Path)

// Set CORS headers for all requests
em.setCORSHeaders(w)
em.setCORSHeaders(w, r)

// Handle preflight OPTIONS request
if r.Method == http.MethodOptions {
Expand Down Expand Up @@ -171,7 +185,7 @@ func (em *EndpointManager) clientRegistrationHandler() http.HandlerFunc {
log.Printf("OAuth DEBUG: Received client registration request: %s %s", r.Method, r.URL.Path)

// Set CORS headers for all requests
em.setCORSHeaders(w)
em.setCORSHeaders(w, r)

// Handle preflight OPTIONS request
if r.Method == http.MethodOptions {
Expand Down Expand Up @@ -220,8 +234,6 @@ func (em *EndpointManager) clientRegistrationHandler() http.HandlerFunc {
// But since Azure AD requires pre-registered client IDs, we return the configured one
clientID := em.cfg.OAuthConfig.ClientID

log.Printf("OAuth DEBUG: Client registration successful - returning client_id: %s", clientID)

clientInfo := map[string]interface{}{
"client_id": clientID, // Use configured Azure AD client ID
"client_id_issued_at": time.Now().Unix(), // RFC 7591: timestamp of issuance
Expand Down Expand Up @@ -284,11 +296,28 @@ func (em *EndpointManager) validateClientRegistration(req *ClientRegistrationReq
return nil
}

// validateRedirectURI validates that a redirect URI is registered and allowed
func (em *EndpointManager) validateRedirectURI(redirectURI string) error {
if len(em.cfg.OAuthConfig.RedirectURIs) == 0 {
return fmt.Errorf("no redirect URIs configured")
}

for _, allowed := range em.cfg.OAuthConfig.RedirectURIs {
if redirectURI == allowed {
return nil
}
}

log.Printf("OAuth SECURITY WARNING: Invalid redirect URI attempted: %s, allowed: %v",
redirectURI, em.cfg.OAuthConfig.RedirectURIs)
return fmt.Errorf("redirect_uri not registered: %s", redirectURI)
}

// tokenIntrospectionHandler implements RFC 7662 OAuth 2.0 Token Introspection
func (em *EndpointManager) tokenIntrospectionHandler() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Set CORS headers for all requests
em.setCORSHeaders(w)
em.setCORSHeaders(w, r)

// Handle preflight OPTIONS request
if r.Method == http.MethodOptions {
Expand Down Expand Up @@ -350,7 +379,7 @@ func (em *EndpointManager) tokenIntrospectionHandler() http.HandlerFunc {
func (em *EndpointManager) healthHandler() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Set CORS headers for all requests
em.setCORSHeaders(w)
em.setCORSHeaders(w, r)

// Handle preflight OPTIONS request
if r.Method == http.MethodOptions {
Expand Down Expand Up @@ -384,7 +413,7 @@ func (em *EndpointManager) protectedResourceMetadataHandler() http.HandlerFunc {
log.Printf("OAuth DEBUG: Received request for protected resource metadata: %s %s", r.Method, r.URL.Path)

// Set CORS headers for all requests
em.setCORSHeaders(w)
em.setCORSHeaders(w, r)

// Handle preflight OPTIONS request
if r.Method == http.MethodOptions {
Expand Down Expand Up @@ -457,7 +486,7 @@ func (em *EndpointManager) authorizationProxyHandler() http.HandlerFunc {
log.Printf("OAuth DEBUG: Received authorization proxy request: %s %s", r.Method, r.URL.Path)

// Set CORS headers for all requests
em.setCORSHeaders(w)
em.setCORSHeaders(w, r)

// Handle preflight OPTIONS request
if r.Method == http.MethodOptions {
Expand All @@ -474,6 +503,23 @@ func (em *EndpointManager) authorizationProxyHandler() http.HandlerFunc {
// Parse query parameters
query := r.URL.Query()

// Validate redirect_uri parameter for security and better user experience
redirectURI := query.Get("redirect_uri")
if redirectURI == "" {
log.Printf("OAuth ERROR: Missing redirect_uri parameter in authorization request")
log.Printf("OAuth HELP: To fix this error, configure redirect URIs using --oauth-redirects flag")
log.Printf("OAuth HELP: For MCP Inspector, use: --oauth-redirects=\"http://localhost:8000/oauth/callback,http://localhost:6274/oauth/callback\"")
em.writeErrorResponse(w, "invalid_request", "redirect_uri parameter is required", http.StatusBadRequest)
return
}

// Validate that the redirect_uri is registered and allowed
if err := em.validateRedirectURI(redirectURI); err != nil {
log.Printf("OAuth ERROR: redirect_uri %s not registered - requests will be blocked for security", redirectURI)
em.writeErrorResponse(w, "invalid_request", fmt.Sprintf("redirect_uri not registered: %s", redirectURI), http.StatusBadRequest)
return
}

// Enforce PKCE for OAuth 2.1 compliance (MCP requirement)
codeChallenge := query.Get("code_challenge")
codeChallengeMethod := query.Get("code_challenge_method")
Expand Down Expand Up @@ -532,7 +578,7 @@ func (em *EndpointManager) callbackHandler() http.HandlerFunc {
log.Printf("OAuth DEBUG: Received callback request: %s %s", r.Method, r.URL.Path)

// Set CORS headers for all requests
em.setCORSHeaders(w)
em.setCORSHeaders(w, r)

// Handle preflight OPTIONS request
if r.Method == http.MethodOptions {
Expand Down Expand Up @@ -575,6 +621,14 @@ func (em *EndpointManager) callbackHandler() http.HandlerFunc {

log.Printf("OAuth DEBUG: Callback parameters validated - has_code: true, state: %s", state)

// Validate redirect URI for security - construct expected URI and validate it
expectedRedirectURI := fmt.Sprintf("http://%s:%d/oauth/callback", em.cfg.Host, em.cfg.Port)
if err := em.validateRedirectURI(expectedRedirectURI); err != nil {
log.Printf("OAuth ERROR: Redirect URI validation failed: %v", err)
em.writeCallbackErrorResponse(w, "Invalid redirect URI")
return
}

// Exchange authorization code for access token
tokenResponse, err := em.exchangeCodeForToken(code, state)
if err != nil {
Expand All @@ -583,8 +637,6 @@ func (em *EndpointManager) callbackHandler() http.HandlerFunc {
return
}

log.Printf("OAuth DEBUG: Token exchange successful, preparing callback success response")

// Skip token validation in callback - validation happens during MCP requests
// Create minimal token info for callback success page
tokenInfo := &auth.TokenInfo{
Expand Down Expand Up @@ -806,7 +858,7 @@ func (em *EndpointManager) tokenHandler() http.HandlerFunc {
log.Printf("OAuth DEBUG: Received token endpoint request: %s %s", r.Method, r.URL.Path)

// Set CORS headers for all requests
em.setCORSHeaders(w)
em.setCORSHeaders(w, r)

// Handle preflight OPTIONS request
if r.Method == http.MethodOptions {
Expand Down Expand Up @@ -866,15 +918,20 @@ func (em *EndpointManager) tokenHandler() http.HandlerFunc {
return
}

log.Printf("OAuth DEBUG: Token request parameters validated - client_id: %s, redirect_uri: %s, has_code_verifier: %t", clientID, redirectURI, codeVerifier != "")

// Validate client ID (accept both configured and dynamically registered clients)
if !em.isValidClientID(clientID) {
log.Printf("OAuth ERROR: Invalid client_id: %s", clientID)
em.writeErrorResponse(w, "invalid_client", "Invalid client_id", http.StatusBadRequest)
return
}

// Validate redirect URI for security
if err := em.validateRedirectURI(redirectURI); err != nil {
log.Printf("OAuth ERROR: Redirect URI validation failed in token endpoint: %v", err)
em.writeErrorResponse(w, "invalid_request", "Invalid redirect_uri", http.StatusBadRequest)
return
}

// Extract scope from the token request (MCP client should send the same scope)
requestedScope := r.FormValue("scope")
if requestedScope == "" {
Expand All @@ -892,8 +949,6 @@ func (em *EndpointManager) tokenHandler() http.HandlerFunc {
return
}

log.Printf("OAuth DEBUG: Token exchange successful, returning token response to client")

// Return token response
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-store")
Expand Down
Loading
Loading