Skip to content

Commit

Permalink
Add failing test case for tampered pkce verifier
Browse files Browse the repository at this point in the history
  • Loading branch information
Rorical committed Dec 17, 2024
1 parent 9faeda2 commit 4c15384
Showing 1 changed file with 200 additions and 0 deletions.
200 changes: 200 additions & 0 deletions integration/auth_oidc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"net/netip"
"sort"
"strconv"
"strings"
"testing"
"time"

Expand Down Expand Up @@ -838,6 +839,205 @@ func (s *AuthOIDCScenario) runTailscaleUp(
return fmt.Errorf("failed to up tailscale node: %w", errNoUserAvailable)
}

func (s *AuthOIDCScenario) runTailscaleUpPKCE(
userStr string,
loginServer string,
httpClientModifier func(*http.Client),
) error {
headscale, err := s.Headscale()
if err != nil {
return err
}

allClients, err := s.ListTailscaleClients()
if err != nil {
return fmt.Errorf("failed to list tailscale clients: %w", err)
}

for _, client := range allClients {
status, err := client.Status()
if err != nil {
log.Printf("%s failed to get status: %s", client.Hostname(), err)

continue
}

if status.BackendState == "Running" {
log.Printf("%s is already running", client.Hostname())

continue
}

log.Printf("%s running tailscale up", client.Hostname())

loginURL, err := client.LoginWithURL(loginServer)
if err != nil {
log.Printf("%s failed to run tailscale up: %s", client.Hostname(), err)
}

loginURL.Host = fmt.Sprintf("%s:8080", headscale.GetIP())
loginURL.Scheme = "http"

if len(headscale.GetCert()) > 0 {
loginURL.Scheme = "https"
}

insecureTransport := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, // nolint
}

log.Printf("%s login url: %s\n", client.Hostname(), loginURL.String())

log.Printf("%s logging in with url", client.Hostname())
httpClient := &http.Client{Transport: insecureTransport}

// Allow the test to modify the HTTP client
if httpClientModifier != nil {
httpClientModifier(httpClient)
}

ctx := context.Background()
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, loginURL.String(), nil)
resp, err := httpClient.Do(req)
if err != nil {
log.Printf(
"%s failed to login using url %s: %s",
client.Hostname(),
loginURL,
err,
)

return err
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
log.Printf("%s response code of oidc login request was %s", client.Hostname(), resp.Status)
body, _ := io.ReadAll(resp.Body)
log.Printf("body: %s", body)

return errStatusCodeNotOK
}

return nil
}

return fmt.Errorf("failed to up tailscale node: %w", errNoUserAvailable)
}

type tamperVerifierTransport struct {
base http.RoundTripper
}

func (t *tamperVerifierTransport) RoundTrip(req *http.Request) (*http.Response, error) {
// If this is a token exchange request, tamper with the verifier
if strings.Contains(req.URL.Path, "/token") && req.Method == http.MethodPost {
err := req.ParseForm()
if err != nil {
return nil, err
}
if verifier := req.Form.Get("code_verifier"); verifier != "" {
// Tamper with the verifier
req.Form.Set("code_verifier", verifier+"_tampered")
// Update request body with modified form
req.Body = io.NopCloser(strings.NewReader(req.Form.Encode()))
req.ContentLength = int64(len(req.Form.Encode()))
}
}
// Forward the request with the tampered verifier
return t.base.RoundTrip(req)
}

func TestOIDCAuthenticationWithPKCEVerifierTampering(t *testing.T) {
IntegrationSkip(t)
t.Parallel()

baseScenario, err := NewScenario(dockertestMaxWait())
assertNoErr(t, err)

scenario := AuthOIDCScenario{
Scenario: baseScenario,
}
defer scenario.ShutdownAssertNoPanics(t)

// Single user with one node for testing PKCE flow
spec := map[string]int{
"user1": 1,
}

mockusers := []mockoidc.MockUser{
oidcMockUser("user1", true),
}

oidcConfig, err := scenario.runMockOIDC(defaultAccessTTL, mockusers)
assertNoErrf(t, "failed to run mock OIDC server: %s", err)
defer scenario.mockOIDC.Close()

oidcMap := map[string]string{
"HEADSCALE_OIDC_ISSUER": oidcConfig.Issuer,
"HEADSCALE_OIDC_CLIENT_ID": oidcConfig.ClientID,
"HEADSCALE_OIDC_CLIENT_SECRET_PATH": "${CREDENTIALS_DIRECTORY_TEST}/hs_client_oidc_secret",
"CREDENTIALS_DIRECTORY_TEST": "/tmp",
"HEADSCALE_OIDC_PKCE_ENABLED": "1", // Enable PKCE
"HEADSCALE_OIDC_PKCE_METHOD": "S256",
"HEADSCALE_OIDC_MAP_LEGACY_USERS": "0",
"HEADSCALE_OIDC_STRIP_EMAIL_DOMAIN": "0",
}

err = scenario.CreateHeadscaleEnv(
spec,
hsic.WithTestName("oidcauthpkcetampered"),
hsic.WithConfigEnv(oidcMap),
hsic.WithTLS(),
hsic.WithHostnameAsServerURL(),
hsic.WithFileInContainer("/tmp/hs_client_oidc_secret", []byte(oidcConfig.ClientSecret)),
)
assertNoErrHeadscaleEnv(t, err)

// Create a transport that modifies the PKCE verifier in transit
baseTransport := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
tamperTransport := &tamperVerifierTransport{
base: baseTransport,
}

// Get all clients
allClients, err := scenario.ListTailscaleClients()
assertNoErrListClients(t, err)

// Try to authenticate each client with the tampered PKCE verifier
for _, client := range allClients {
err := scenario.runTailscaleUpPKCE("user1", "user1", func(client *http.Client) {
client.Transport = tamperTransport
})
if err == nil {
t.Errorf("expected authentication to fail for client %s due to PKCE verifier tampering, but it succeeded", client.Hostname())
}
}

// Verify no users were created since authentication should fail
headscale, err := scenario.Headscale()
assertNoErr(t, err)

var listUsers []v1.User
err = executeAndUnmarshal(headscale,
[]string{
"headscale",
"users",
"list",
"--output",
"json",
},
&listUsers,
)
assertNoErr(t, err)

if len(listUsers) > 0 {
t.Errorf("expected no users to be created, but found %d users", len(listUsers))
}
}

func (s *AuthOIDCScenario) Shutdown() {
err := s.pool.Purge(s.mockOIDC)
if err != nil {
Expand Down

0 comments on commit 4c15384

Please sign in to comment.