Files
tailscale/feature/oauthkey/oauthkey.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

116 lines
3.2 KiB
Go

// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package oauthkey registers support for using OAuth client secrets to
// automatically request authkeys for logging in.
package oauthkey
import (
"context"
"errors"
"fmt"
"net/url"
"strconv"
"strings"
"golang.org/x/oauth2/clientcredentials"
"tailscale.com/feature"
"tailscale.com/internal/client/tailscale"
)
func init() {
feature.Register("oauthkey")
tailscale.HookResolveAuthKey.Set(resolveAuthKey)
}
// resolveAuthKey either returns v unchanged (in the common case) or, if it
// starts with "tskey-client-" (as Tailscale OAuth secrets do) parses it like
//
// tskey-client-xxxx[?ephemeral=false&bar&preauthorized=BOOL&baseURL=...]
//
// and does the OAuth2 dance to get and return an authkey. The "ephemeral"
// property defaults to true if unspecified. The "preauthorized" defaults to
// 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, args tailscale.ResolveAuthKeyArgs) (string, error) {
if !strings.HasPrefix(args.AuthKey, "tskey-client-") {
return args.AuthKey, nil
}
if len(args.Tags) == 0 {
return "", errors.New("oauth authkeys require --advertise-tags")
}
strippedSecret, ephemeral, preauth, baseURL, err := parseOptionalAttributes(args.AuthKey)
if err != nil {
return "", err
}
credentials := clientcredentials.Config{
ClientID: "some-client-id", // ignored
ClientSecret: strippedSecret,
TokenURL: baseURL + "/api/v2/oauth/token",
}
tsClient := tailscale.NewClient("-", nil)
tsClient.UserAgent = "tailscale-cli"
tsClient.HTTPClient = credentials.Client(ctx)
tsClient.BaseURL = baseURL
caps := tailscale.KeyCapabilities{
Devices: tailscale.KeyDeviceCapabilities{
Create: tailscale.KeyDeviceCreateCapabilities{
Reusable: false,
Ephemeral: ephemeral,
Preauthorized: preauth,
Tags: args.Tags,
},
},
}
authkey, _, err := tsClient.CreateKey(ctx, caps)
if err != nil {
return "", err
}
return authkey, nil
}
func parseOptionalAttributes(clientSecret string) (strippedSecret string, ephemeral bool, preauth bool, baseURL string, err error) {
strippedSecret, named, _ := strings.Cut(clientSecret, "?")
attrs, err := url.ParseQuery(named)
if err != nil {
return "", false, false, "", err
}
for k := range attrs {
switch k {
case "ephemeral", "preauthorized", "baseURL":
default:
return "", false, false, "", fmt.Errorf("unknown attribute %q", k)
}
}
getBool := func(name string, def bool) (bool, error) {
v := attrs.Get(name)
if v == "" {
return def, nil
}
ret, err := strconv.ParseBool(v)
if err != nil {
return false, fmt.Errorf("invalid attribute boolean attribute %s value %q", name, v)
}
return ret, nil
}
ephemeral, err = getBool("ephemeral", true)
if err != nil {
return "", false, false, "", err
}
preauth, err = getBool("preauthorized", false)
if err != nil {
return "", false, false, "", err
}
baseURL = "https://api.tailscale.com"
if v := attrs.Get("baseURL"); v != "" {
baseURL = v
}
return strippedSecret, ephemeral, preauth, baseURL, nil
}