diff --git a/auth/extauth/enterprise_handler.go b/auth/extauth/enterprise_handler.go index a62c9c99..0b1b0e64 100644 --- a/auth/extauth/enterprise_handler.go +++ b/auth/extauth/enterprise_handler.go @@ -195,6 +195,13 @@ func (h *EnterpriseHandler) Authorize(ctx context.Context, req *http.Request, re if err != nil { return fmt.Errorf("token exchange failed: %w", err) } + // The token is sent on to the MCP Server's authorization server, a different + // party than the IdP. Only forward it if the IdP says it is an ID-JAG, so + // that any other token the IdP issued, such as an access token, does not + // leave it. + if issued, _ := idJAGToken.Extra("issued_token_type").(string); issued != oauthex.TokenTypeIDJAG { + return fmt.Errorf("token exchange issued a token of type %q, want %q", issued, oauthex.TokenTypeIDJAG) + } // Step 4: Discover MCP Server token endpoint mcpMeta, err := auth.GetAuthServerMetadata(ctx, h.config.MCPAuthServerURL, httpClient) diff --git a/auth/extauth/enterprise_handler_test.go b/auth/extauth/enterprise_handler_test.go index 90bcb02f..840b5cfc 100644 --- a/auth/extauth/enterprise_handler_test.go +++ b/auth/extauth/enterprise_handler_test.go @@ -303,6 +303,62 @@ func TestEnterpriseHandler_ConcurrentAuthorize(t *testing.T) { wg.Wait() } +// TestEnterpriseHandler_Authorize_NotIDJAG checks that a token the IdP did not +// issue as an ID-JAG is not sent on to the MCP Server's authorization server, +// even one that would accept it. +func TestEnterpriseHandler_Authorize_NotIDJAG(t *testing.T) { + const idpAccessToken = "idp-access-token" + + idpServer := oauthtest.NewFakeIdPServer(oauthtest.IdPConfig{ + PreregisteredClients: map[string]oauthtest.ClientInfo{ + "idp_client_id": {Secret: "idp_secret"}, + }, + TokenExchangeConfig: &oauthtest.TokenExchangeConfig{ + IDJAGToken: idpAccessToken, + IssuedTokenType: "urn:ietf:params:oauth:token-type:access_token", + }, + }) + idpServer.Start(t) + + mcpAuthServer := oauthtest.NewFakeAuthorizationServer(oauthtest.Config{ + RegistrationConfig: &oauthtest.RegistrationConfig{ + PreregisteredClients: map[string]oauthtest.ClientInfo{ + "mcp_client_id": {Secret: "mcp_secret"}, + }, + }, + JWTBearerConfig: &oauthtest.JWTBearerConfig{ + ValidAssertions: []string{idpAccessToken}, + }, + }) + mcpAuthServer.Start(t) + + config := validEnterpriseHandlerConfig() + config.IdPIssuerURL = idpServer.URL() + config.IdPCredentials.ClientSecretAuth = &oauthex.ClientSecretAuth{ClientSecret: "idp_secret"} + config.MCPAuthServerURL = mcpAuthServer.URL() + config.MCPCredentials.ClientSecretAuth = &oauthex.ClientSecretAuth{ClientSecret: "mcp_secret"} + + handler, err := NewEnterpriseHandler(config) + if err != nil { + t.Fatalf("NewEnterpriseHandler failed: %v", err) + } + + req := httptest.NewRequest(http.MethodGet, "https://mcp.example.com/api", nil) + resp := &http.Response{ + StatusCode: http.StatusUnauthorized, + Header: make(http.Header), + Body: http.NoBody, + Request: req, + } + err = handler.Authorize(context.Background(), req, resp) + if err == nil || !strings.Contains(err.Error(), "token of type") { + t.Fatalf("Authorize error = %v, want an issued token type error", err) + } + if ts, _ := handler.TokenSource(context.Background()); ts != nil { + t.Error("TokenSource is set after the token exchange issued a token that is not an ID-JAG") + } +} + func TestEnterpriseHandler_Authorize_IDTokenFetcherError(t *testing.T) { config := validEnterpriseHandlerConfig() config.IDTokenFetcher = func(ctx context.Context) (*oauth2.Token, error) { diff --git a/internal/oauthtest/fake_idp_server.go b/internal/oauthtest/fake_idp_server.go index b7057ffd..e5c2c2ef 100644 --- a/internal/oauthtest/fake_idp_server.go +++ b/internal/oauthtest/fake_idp_server.go @@ -25,6 +25,10 @@ type TokenExchangeConfig struct { // IDJAGToken is the ID-JAG value returned from token exchange. // Defaults to "test-id-jag-token" if empty. IDJAGToken string + + // IssuedTokenType is the issued_token_type returned from token exchange. + // Defaults to oauthex.TokenTypeIDJAG if empty. + IssuedTokenType string } // IdPConfig holds configuration for [FakeIdPServer]. @@ -218,11 +222,15 @@ func (s *FakeIdPServer) handleTokenExchangeGrant(w http.ResponseWriter, r *http. if idJAG == "" { idJAG = "test-id-jag-token" } + issuedTokenType := s.config.TokenExchangeConfig.IssuedTokenType + if issuedTokenType == "" { + issuedTokenType = oauthex.TokenTypeIDJAG + } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]any{ "access_token": idJAG, - "issued_token_type": oauthex.TokenTypeIDJAG, + "issued_token_type": issuedTokenType, "token_type": "N_A", "expires_in": 300, })