Files
tailscale/feature/identityfederation/identityfederation_test.go
T
Mario MinardiandMario Minardi c8ae72b537 various: change OAuth and WIF auth key resolvers to take struct args
Change signature of OAuth and identityfederation auth key resolution
hooks to take in structs instead of lists of args as they were getting
unwieldily.

Updates https://github.com/tailscale/tailscale/issues/20339

Signed-off-by: Mario Minardi <mario@tailscale.com>
2026-07-21 15:44:45 -06:00

190 lines
5.1 KiB
Go

// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package identityfederation
import (
"context"
"net/http"
"net/http/httptest"
"strings"
"testing"
"tailscale.com/internal/client/tailscale"
)
func TestResolveAuthKey(t *testing.T) {
tests := []struct {
name string
clientID string
idToken string
audience string
tags []string
wantAuthKey string
wantErr string
}{
{
name: "success",
clientID: "client-123",
idToken: "token",
audience: "api://tailscale-wif",
tags: []string{"tag:test"},
wantAuthKey: "tskey-auth-xyz",
wantErr: "",
},
{
name: "missing-client-id-noop",
clientID: "",
idToken: "token",
audience: "api://tailscale-wif",
tags: []string{"tag:test"},
wantAuthKey: "",
wantErr: "",
},
{
name: "missing-id-token-and-audience",
clientID: "client-123",
idToken: "",
audience: "",
tags: []string{"tag:test"},
wantErr: "federated identity requires either an ID token or an audience",
},
{
name: "missing-tags",
clientID: "client-123",
idToken: "token",
audience: "api://tailscale-wif",
tags: []string{},
wantErr: "federated identity authkeys require --advertise-tags",
},
{
name: "invalid-client-id-attrs",
clientID: "client-123?invalid=value",
idToken: "token",
audience: "api://tailscale-wif",
tags: []string{"tag:test"},
wantErr: `failed to parse optional config attributes: unknown optional config attribute "invalid"`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
srv := mockedControlServer(t)
defer srv.Close()
authKey, err := resolveAuthKey(context.Background(), tailscale.ResolveAuthKeyWIFArgs{
BaseURL: srv.URL,
ClientID: tt.clientID,
IDToken: tt.idToken,
Audience: tt.audience,
Tags: tt.tags,
})
if tt.wantErr != "" {
if err == nil {
t.Errorf("resolveAuthKey() error = nil, want %q", tt.wantErr)
return
}
if err.Error() != tt.wantErr {
t.Errorf("resolveAuthKey() error = %q, want %q", err.Error(), tt.wantErr)
}
} else if err != nil {
t.Fatalf("resolveAuthKey() unexpected error = %v", err)
}
if authKey != tt.wantAuthKey {
t.Errorf("resolveAuthKey() = %q, want %q", authKey, tt.wantAuthKey)
}
})
}
}
func TestParseOptionalAttributes(t *testing.T) {
tests := []struct {
name string
clientID string
wantClientID string
wantEphemeral bool
wantPreauth bool
wantErr string
}{
{
name: "default-values",
clientID: "client-123",
wantClientID: "client-123",
wantEphemeral: true,
wantPreauth: false,
wantErr: "",
},
{
name: "custom-values",
clientID: "client-123?ephemeral=false&preauthorized=true",
wantClientID: "client-123",
wantEphemeral: false,
wantPreauth: true,
wantErr: "",
},
{
name: "unknown-attribute",
clientID: "client-123?unknown=value",
wantClientID: "",
wantEphemeral: false,
wantPreauth: false,
wantErr: `unknown optional config attribute "unknown"`,
},
{
name: "invalid-value",
clientID: "client-123?ephemeral=invalid",
wantClientID: "",
wantEphemeral: false,
wantPreauth: false,
wantErr: `strconv.ParseBool: parsing "invalid": invalid syntax`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
strippedID, ephemeral, preauth, err := parseOptionalAttributes(tt.clientID)
if tt.wantErr != "" {
if err == nil {
t.Errorf("parseOptionalAttributes() error = nil, want %q", tt.wantErr)
return
}
if err.Error() != tt.wantErr {
t.Errorf("parseOptionalAttributes() error = %q, want %q", err.Error(), tt.wantErr)
}
} else {
if err != nil {
t.Errorf("parseOptionalAttributes() error = %v, want nil", err)
return
}
}
if strippedID != tt.wantClientID {
t.Errorf("parseOptionalAttributes() strippedID = %v, want %v", strippedID, tt.wantClientID)
}
if ephemeral != tt.wantEphemeral {
t.Errorf("parseOptionalAttributes() ephemeral = %v, want %v", ephemeral, tt.wantEphemeral)
}
if preauth != tt.wantPreauth {
t.Errorf("parseOptionalAttributes() preauth = %v, want %v", preauth, tt.wantPreauth)
}
})
}
}
func mockedControlServer(t *testing.T) *httptest.Server {
t.Helper()
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case strings.Contains(r.URL.Path, "/oauth/token-exchange"):
// OAuth2 library sends the token exchange request
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"access_token":"access-123","token_type":"Bearer","expires_in":3600}`))
case strings.Contains(r.URL.Path, "/api/v2/tailnet") && strings.Contains(r.URL.Path, "/keys"):
// Tailscale client creates the authkey
w.Write([]byte(`{"key":"tskey-auth-xyz","created":"2024-01-01T00:00:00Z"}`))
default:
w.WriteHeader(http.StatusNotFound)
}
}))
}