|  | 
|  | 1 | +package http | 
|  | 2 | + | 
|  | 3 | +import ( | 
|  | 4 | +	"encoding/base64" | 
|  | 5 | +	"fmt" | 
|  | 6 | +	"net/http" | 
|  | 7 | +	"strings" | 
|  | 8 | +	"testing" | 
|  | 9 | + | 
|  | 10 | +	"github.com/containers/kubernetes-mcp-server/internal/test" | 
|  | 11 | +	"github.com/coreos/go-oidc/v3/oidc" | 
|  | 12 | +	"golang.org/x/oauth2" | 
|  | 13 | +) | 
|  | 14 | + | 
|  | 15 | +func TestExternalAccountTokenExchange(t *testing.T) { | 
|  | 16 | +	mockServer := test.NewMockServer() | 
|  | 17 | +	authServer := mockServer.Config().Host | 
|  | 18 | +	var tokenExchangeRequest *http.Request | 
|  | 19 | +	mockServer.Handle(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { | 
|  | 20 | +		if req.URL.Path == "/.well-known/openid-configuration" { | 
|  | 21 | +			w.Header().Set("Content-Type", "application/json") | 
|  | 22 | +			_, _ = fmt.Fprintf(w, `{ | 
|  | 23 | +				"issuer": "%s", | 
|  | 24 | +				"authorization_endpoint": "https://mock-oidc-provider/authorize", | 
|  | 25 | +				"token_endpoint": "%s/token" | 
|  | 26 | +			}`, authServer, authServer) | 
|  | 27 | +			return | 
|  | 28 | +		} | 
|  | 29 | +		if req.URL.Path == "/token" { | 
|  | 30 | +			tokenExchangeRequest = req | 
|  | 31 | +			_ = tokenExchangeRequest.ParseForm() | 
|  | 32 | +			if tokenExchangeRequest.PostForm.Get("subject_token") != "the-original-access-token" { | 
|  | 33 | +				http.Error(w, "Invalid subject_token", http.StatusUnauthorized) | 
|  | 34 | +				return | 
|  | 35 | +			} | 
|  | 36 | +			w.Header().Set("Content-Type", "application/json") | 
|  | 37 | +			_, _ = w.Write([]byte(`{"access_token":"exchanged-access-token","token_type":"Bearer","expires_in":253402297199}`)) | 
|  | 38 | +			return | 
|  | 39 | +		} | 
|  | 40 | +	})) | 
|  | 41 | +	t.Cleanup(mockServer.Close) | 
|  | 42 | +	provider, err := oidc.NewProvider(t.Context(), authServer) | 
|  | 43 | +	if err != nil { | 
|  | 44 | +		t.Fatalf("oidc.NewProvider() error = %v; want nil", err) | 
|  | 45 | +	} | 
|  | 46 | +	// With missing Token Source information | 
|  | 47 | +	_, err = (&SecurityTokenService{Provider: provider}).ExternalAccountTokenExchange(t.Context(), &oauth2.Token{}) | 
|  | 48 | +	t.Run("ExternalAccountTokenExchange with missing token source returns error", func(t *testing.T) { | 
|  | 49 | +		if err == nil { | 
|  | 50 | +			t.Fatalf("ExternalAccountTokenExchange() error = nil; want error") | 
|  | 51 | +		} | 
|  | 52 | +		if !strings.Contains(err.Error(), "must be set") { | 
|  | 53 | +			t.Errorf("ExternalAccountTokenExchange() error = %v; want missing required field", err) | 
|  | 54 | +		} | 
|  | 55 | +	}) | 
|  | 56 | +	// With valid Token Source information | 
|  | 57 | +	sts := SecurityTokenService{ | 
|  | 58 | +		Provider:                provider, | 
|  | 59 | +		ClientId:                "test-client-id", | 
|  | 60 | +		ClientSecret:            "test-client-secret", | 
|  | 61 | +		ExternalAccountAudience: "test-audience", | 
|  | 62 | +		ExternalAccountScopes:   []string{"test-scope"}, | 
|  | 63 | +	} | 
|  | 64 | +	// With Invalid token | 
|  | 65 | +	_, err = sts.ExternalAccountTokenExchange(t.Context(), &oauth2.Token{ | 
|  | 66 | +		AccessToken: "invalid-access-token", | 
|  | 67 | +		TokenType:   "Bearer", | 
|  | 68 | +	}) | 
|  | 69 | +	t.Run("ExternalAccountTokenExchange with invalid token returns error", func(t *testing.T) { | 
|  | 70 | +		if err == nil { | 
|  | 71 | +			t.Fatalf("ExternalAccountTokenExchange() error = nil; want error") | 
|  | 72 | +		} | 
|  | 73 | +		if !strings.Contains(err.Error(), "status code 401: Invalid subject_token") { | 
|  | 74 | +			t.Errorf("ExternalAccountTokenExchange() error = %v; want invalid_grant: Invalid subject_token", err) | 
|  | 75 | +		} | 
|  | 76 | +	}) | 
|  | 77 | +	// With Valid token | 
|  | 78 | +	exchangeToken, err := sts.ExternalAccountTokenExchange(t.Context(), &oauth2.Token{ | 
|  | 79 | +		AccessToken: "the-original-access-token", | 
|  | 80 | +		TokenType:   "Bearer", | 
|  | 81 | +	}) | 
|  | 82 | +	t.Run("ExternalAccountTokenExchange with valid token returns new token", func(t *testing.T) { | 
|  | 83 | +		if err != nil { | 
|  | 84 | +			t.Errorf("ExternalAccountTokenExchange() error = %v; want nil", err) | 
|  | 85 | +		} | 
|  | 86 | +		if exchangeToken == nil { | 
|  | 87 | +			t.Fatal("ExternalAccountTokenExchange() = nil; want token") | 
|  | 88 | +		} | 
|  | 89 | +		if exchangeToken.AccessToken != "exchanged-access-token" { | 
|  | 90 | +			t.Errorf("exchangeToken.AccessToken = %s; want exchanged-access-token", exchangeToken.AccessToken) | 
|  | 91 | +		} | 
|  | 92 | +	}) | 
|  | 93 | +	t.Run("ExternalAccountTokenExchange with valid token sends POST request", func(t *testing.T) { | 
|  | 94 | +		if tokenExchangeRequest == nil { | 
|  | 95 | +			t.Fatal("tokenExchangeRequest is nil; want request") | 
|  | 96 | +		} | 
|  | 97 | +		if tokenExchangeRequest.Method != "POST" { | 
|  | 98 | +			t.Errorf("tokenExchangeRequest.Method = %s; want POST", tokenExchangeRequest.Method) | 
|  | 99 | +		} | 
|  | 100 | +	}) | 
|  | 101 | +	t.Run("ExternalAccountTokenExchange with valid token has correct form data", func(t *testing.T) { | 
|  | 102 | +		if tokenExchangeRequest.Header.Get("Content-Type") != "application/x-www-form-urlencoded" { | 
|  | 103 | +			t.Errorf("tokenExchangeRequest.Content-Type = %s; want application/x-www-form-urlencoded", tokenExchangeRequest.Header.Get("Content-Type")) | 
|  | 104 | +		} | 
|  | 105 | +		if tokenExchangeRequest.PostForm.Get("audience") != "test-audience" { | 
|  | 106 | +			t.Errorf("tokenExchangeRequest.PostForm[audience] = %s; want test-audience", tokenExchangeRequest.PostForm.Get("audience")) | 
|  | 107 | +		} | 
|  | 108 | +		if tokenExchangeRequest.PostForm.Get("subject_token_type") != "urn:ietf:params:oauth:token-type:access_token" { | 
|  | 109 | +			t.Errorf("tokenExchangeRequest.PostForm[subject_token_type] = %s; want urn:ietf:params:oauth:token-type:access_token", tokenExchangeRequest.PostForm.Get("subject_token_type")) | 
|  | 110 | +		} | 
|  | 111 | +		if tokenExchangeRequest.PostForm.Get("subject_token") != "the-original-access-token" { | 
|  | 112 | +			t.Errorf("tokenExchangeRequest.PostForm[subject_token] = %s; want the-original-access-token", tokenExchangeRequest.PostForm.Get("subject_token")) | 
|  | 113 | +		} | 
|  | 114 | +		if len(tokenExchangeRequest.PostForm["scope"]) == 0 || tokenExchangeRequest.PostForm["scope"][0] != "test-scope" { | 
|  | 115 | +			t.Errorf("tokenExchangeRequest.PostForm[scope] = %v; want [test-scope]", tokenExchangeRequest.PostForm["scope"]) | 
|  | 116 | +		} | 
|  | 117 | +	}) | 
|  | 118 | +	t.Run("ExternalAccountTokenExchange with valid token sends correct client credentials header", func(t *testing.T) { | 
|  | 119 | +		if tokenExchangeRequest.Header.Get("Authorization") != "Basic "+base64.StdEncoding.EncodeToString([]byte("test-client-id:test-client-secret")) { | 
|  | 120 | +			t.Errorf("tokenExchangeRequest.Header[Authorization] = %s; want Basic base64(test-client-id:test-client-secret)", tokenExchangeRequest.Header.Get("Authorization")) | 
|  | 121 | +		} | 
|  | 122 | +	}) | 
|  | 123 | +} | 
0 commit comments