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>
This commit is contained in:
committed by
Mario Minardi
parent
3ccc7725a3
commit
c8ae72b537
@@ -29,34 +29,38 @@ func init() {
|
||||
}
|
||||
|
||||
// resolveAuthKey uses OIDC identity federation to exchange the provided ID token and client ID for an authkey.
|
||||
func resolveAuthKey(ctx context.Context, baseURL, clientID, idToken, audience string, tags []string) (string, error) {
|
||||
if clientID == "" {
|
||||
func resolveAuthKey(ctx context.Context, args tailscale.ResolveAuthKeyWIFArgs) (string, error) {
|
||||
if args.ClientID == "" {
|
||||
return "", nil // Short-circuit, no client ID means not using identity federation
|
||||
}
|
||||
|
||||
if idToken == "" {
|
||||
if audience == "" {
|
||||
if args.IDToken == "" {
|
||||
if args.Audience == "" {
|
||||
return "", errors.New("federated identity requires either an ID token or an audience")
|
||||
}
|
||||
providerIdToken, err := wif.ObtainProviderToken(ctx, audience)
|
||||
providerIdToken, err := wif.ObtainProviderToken(ctx, args.Audience)
|
||||
if err != nil {
|
||||
return "", errors.New("federated identity authkeys require --id-token")
|
||||
}
|
||||
idToken = providerIdToken
|
||||
args.IDToken = providerIdToken
|
||||
}
|
||||
if len(tags) == 0 {
|
||||
if len(args.Tags) == 0 {
|
||||
return "", errors.New("federated identity authkeys require --advertise-tags")
|
||||
}
|
||||
if baseURL == "" {
|
||||
baseURL = ipn.DefaultControlURL
|
||||
if args.BaseURL == "" {
|
||||
args.BaseURL = ipn.DefaultControlURL
|
||||
}
|
||||
|
||||
strippedID, ephemeral, preauth, err := parseOptionalAttributes(clientID)
|
||||
strippedID, ephemeral, preauth, err := parseOptionalAttributes(args.ClientID)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to parse optional config attributes: %w", err)
|
||||
}
|
||||
|
||||
accessToken, err := exchangeJWTForToken(ctx, baseURL, strippedID, idToken)
|
||||
accessToken, err := exchangeJWTForToken(ctx, tailscale.ExchangeJWTForTokenWIFArgs{
|
||||
BaseURL: args.BaseURL,
|
||||
ClientID: strippedID,
|
||||
IDToken: args.IDToken,
|
||||
})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to exchange JWT for access token: %w", err)
|
||||
}
|
||||
@@ -66,7 +70,7 @@ func resolveAuthKey(ctx context.Context, baseURL, clientID, idToken, audience st
|
||||
|
||||
tsClient := tailscale.NewClient("-", tailscale.APIKey(accessToken))
|
||||
tsClient.UserAgent = "tailscale-cli-identity-federation"
|
||||
tsClient.BaseURL = baseURL
|
||||
tsClient.BaseURL = args.BaseURL
|
||||
|
||||
authkey, _, err := tsClient.CreateKey(ctx, tailscale.KeyCapabilities{
|
||||
Devices: tailscale.KeyDeviceCapabilities{
|
||||
@@ -74,7 +78,7 @@ func resolveAuthKey(ctx context.Context, baseURL, clientID, idToken, audience st
|
||||
Reusable: false,
|
||||
Ephemeral: ephemeral,
|
||||
Preauthorized: preauth,
|
||||
Tags: tags,
|
||||
Tags: args.Tags,
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -117,15 +121,15 @@ func parseOptionalAttributes(clientID string) (strippedID string, ephemeral bool
|
||||
}
|
||||
|
||||
// exchangeJWTForToken exchanges a JWT for a Tailscale access token.
|
||||
func exchangeJWTForToken(ctx context.Context, baseURL, clientID, idToken string) (string, error) {
|
||||
func exchangeJWTForToken(ctx context.Context, args tailscale.ExchangeJWTForTokenWIFArgs) (string, error) {
|
||||
httpClient := &http.Client{Timeout: 10 * time.Second}
|
||||
ctx = context.WithValue(ctx, oauth2.HTTPClient, httpClient)
|
||||
|
||||
token, err := (&oauth2.Config{
|
||||
Endpoint: oauth2.Endpoint{
|
||||
TokenURL: fmt.Sprintf("%s/api/v2/oauth/token-exchange", baseURL),
|
||||
TokenURL: fmt.Sprintf("%s/api/v2/oauth/token-exchange", args.BaseURL),
|
||||
},
|
||||
}).Exchange(ctx, "", oauth2.SetAuthURLParam("client_id", clientID), oauth2.SetAuthURLParam("jwt", idToken))
|
||||
}).Exchange(ctx, "", oauth2.SetAuthURLParam("client_id", args.ClientID), oauth2.SetAuthURLParam("jwt", args.IDToken))
|
||||
if err != nil {
|
||||
// Try to extract more detailed error message
|
||||
if retrieveErr, ok := errors.AsType[*oauth2.RetrieveError](err); ok {
|
||||
|
||||
@@ -9,6 +9,8 @@ import (
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"tailscale.com/internal/client/tailscale"
|
||||
)
|
||||
|
||||
func TestResolveAuthKey(t *testing.T) {
|
||||
@@ -70,7 +72,13 @@ func TestResolveAuthKey(t *testing.T) {
|
||||
srv := mockedControlServer(t)
|
||||
defer srv.Close()
|
||||
|
||||
authKey, err := resolveAuthKey(context.Background(), srv.URL, tt.clientID, tt.idToken, tt.audience, tt.tags)
|
||||
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)
|
||||
|
||||
@@ -33,15 +33,15 @@ func init() {
|
||||
// false. The "baseURL" defaults to https://api.tailscale.com.
|
||||
// The passed in tags are required, and must be non-empty. These will be
|
||||
// set on the authkey generated by the OAuth2 dance.
|
||||
func resolveAuthKey(ctx context.Context, clientSecret string, tags []string) (string, error) {
|
||||
if !strings.HasPrefix(clientSecret, "tskey-client-") {
|
||||
return clientSecret, nil
|
||||
func resolveAuthKey(ctx context.Context, args tailscale.ResolveAuthKeyArgs) (string, error) {
|
||||
if !strings.HasPrefix(args.AuthKey, "tskey-client-") {
|
||||
return args.AuthKey, nil
|
||||
}
|
||||
if len(tags) == 0 {
|
||||
if len(args.Tags) == 0 {
|
||||
return "", errors.New("oauth authkeys require --advertise-tags")
|
||||
}
|
||||
|
||||
strippedSecret, ephemeral, preauth, baseURL, err := parseOptionalAttributes(clientSecret)
|
||||
strippedSecret, ephemeral, preauth, baseURL, err := parseOptionalAttributes(args.AuthKey)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -63,7 +63,7 @@ func resolveAuthKey(ctx context.Context, clientSecret string, tags []string) (st
|
||||
Reusable: false,
|
||||
Ephemeral: ephemeral,
|
||||
Preauthorized: preauth,
|
||||
Tags: tags,
|
||||
Tags: args.Tags,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ import (
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"tailscale.com/internal/client/tailscale"
|
||||
)
|
||||
|
||||
func TestResolveAuthKey(t *testing.T) {
|
||||
@@ -80,7 +82,10 @@ func TestResolveAuthKey(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
got, err := resolveAuthKey(context.Background(), tt.clientID, tt.tags)
|
||||
got, err := resolveAuthKey(context.Background(), tailscale.ResolveAuthKeyArgs{
|
||||
AuthKey: tt.clientID,
|
||||
Tags: tt.tags,
|
||||
})
|
||||
|
||||
if tt.wantErr {
|
||||
if err == nil {
|
||||
|
||||
Reference in New Issue
Block a user