cmd/tailscale/cli: [up] add experimental oauth2 authkey support

Updates #7982

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
Brad Fitzpatrick 2023-04-26 11:34:28 -07:00 committed by Maisem Ali
parent 095d3edd33
commit 13de36303d
3 changed files with 120 additions and 8 deletions

View File

@ -29,9 +29,9 @@ func main() {
tags := flag.String("tags", "", "comma-separated list of tags to apply to the authkey")
flag.Parse()
clientId := os.Getenv("TS_API_CLIENT_ID")
clientID := os.Getenv("TS_API_CLIENT_ID")
clientSecret := os.Getenv("TS_API_CLIENT_SECRET")
if clientId == "" || clientSecret == "" {
if clientID == "" || clientSecret == "" {
log.Fatal("TS_API_CLIENT_ID and TS_API_CLIENT_SECRET must be set")
}
@ -39,22 +39,22 @@ func main() {
log.Fatal("at least one tag must be specified")
}
baseUrl := os.Getenv("TS_BASE_URL")
if baseUrl == "" {
baseUrl = "https://api.tailscale.com"
baseURL := os.Getenv("TS_BASE_URL")
if baseURL == "" {
baseURL = "https://api.tailscale.com"
}
credentials := clientcredentials.Config{
ClientID: clientId,
ClientID: clientID,
ClientSecret: clientSecret,
TokenURL: baseUrl + "/api/v2/oauth/token",
TokenURL: baseURL + "/api/v2/oauth/token",
Scopes: []string{"device"},
}
ctx := context.Background()
tsClient := tailscale.NewClient("-", nil)
tsClient.HTTPClient = credentials.Client(ctx)
tsClient.BaseURL = baseUrl
tsClient.BaseURL = baseURL
caps := tailscale.KeyCapabilities{
Devices: tailscale.KeyDeviceCapabilities{

View File

@ -13,11 +13,13 @@
"fmt"
"log"
"net/netip"
"net/url"
"os"
"os/signal"
"reflect"
"runtime"
"sort"
"strconv"
"strings"
"sync"
"syscall"
@ -26,6 +28,9 @@
shellquote "github.com/kballard/go-shellquote"
"github.com/peterbourgon/ff/v3/ffcli"
qrcode "github.com/skip2/go-qrcode"
"golang.org/x/oauth2/clientcredentials"
"tailscale.com/client/tailscale"
"tailscale.com/envknob"
"tailscale.com/health/healthmsg"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnstate"
@ -663,6 +668,10 @@ func runUp(ctx context.Context, cmd string, args []string, upArgs upArgsT) (retE
if err != nil {
return err
}
authKey, err = resolveAuthKey(ctx, authKey)
if err != nil {
return err
}
if err := localClient.Start(ctx, ipn.Options{
AuthKey: authKey,
UpdatePrefs: prefs,
@ -1102,3 +1111,103 @@ func anyPeerAdvertisingRoutes(st *ipnstate.Status) bool {
}
return false
}
func init() {
// Required to use our client API. We're fine with the instability since the
// client lives in the same repo as this code.
tailscale.I_Acknowledge_This_API_Is_Unstable = true
}
// resolveAuthKey either returns v unchanged (in the common case)
// or, if it starts with "oauth:" parses it as one of:
//
// oauth2:CLIENT_ID:CLIENT_SECRET?ephemeral=false&tags=foo,bar&preauthorized=BOOL
// oauth2:CLIENT_ID:CLIENT_SECRET:BASE_URL?...
//
// and does the OAuth2 dance to get and return an authkey. The "ephemeral" property
// defaults to true if unspecified. The "preauthorized" defaults to false.
//
// If the BASE_URL argument is not provided, it defaults to https://api.tailscale.com.
func resolveAuthKey(ctx context.Context, v string) (string, error) {
suff, ok := strings.CutPrefix(v, "oauth:")
if !ok {
return v, nil
}
if !envknob.Bool("TS_EXPERIMENT_OAUTH_AUTHKEY") {
return "", errors.New("oauth authkeys are in experimental status")
}
pos, named, _ := strings.Cut(suff, "?")
attrs, err := url.ParseQuery(named)
if err != nil {
return "", err
}
for k := range attrs {
switch k {
case "ephemeral", "preauthorized", "tags":
default:
return "", 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 "", err
}
preauth, err := getBool("preauthorized", false)
if err != nil {
return "", err
}
var tags []string
if v := attrs.Get("tags"); v != "" {
tags = strings.Split(v, ",")
}
f := strings.SplitN(pos, ":", 3)
if len(f) < 2 || len(f) > 3 {
return "", errors.New("invalid auth key format; want oauth2:CLIENT_ID:CLIENT_SECRET[:BASE_URL]")
}
clientID, clientSecret := f[0], f[1]
baseURL := "https://api.tailscale.com"
if len(f) == 3 {
baseURL = f[2]
}
credentials := clientcredentials.Config{
ClientID: clientID,
ClientSecret: clientSecret,
TokenURL: baseURL + "/api/v2/oauth/token",
Scopes: []string{"device"},
}
tsClient := tailscale.NewClient("-", nil)
tsClient.HTTPClient = credentials.Client(ctx)
tsClient.BaseURL = baseURL
caps := tailscale.KeyCapabilities{
Devices: tailscale.KeyDeviceCapabilities{
Create: tailscale.KeyDeviceCreateCapabilities{
Reusable: false,
Ephemeral: ephemeral,
Preauthorized: preauth,
Tags: tags,
},
},
}
authkey, _, err := tsClient.CreateKey(ctx, caps)
if err != nil {
return "", err
}
return authkey, nil
}

View File

@ -155,6 +155,9 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
golang.org/x/net/ipv6 from golang.org/x/net/icmp
golang.org/x/net/proxy from tailscale.com/net/netns
D golang.org/x/net/route from net+
golang.org/x/oauth2 from golang.org/x/oauth2/clientcredentials
golang.org/x/oauth2/clientcredentials from tailscale.com/cmd/tailscale/cli
golang.org/x/oauth2/internal from golang.org/x/oauth2+
golang.org/x/sync/errgroup from tailscale.com/derp+
golang.org/x/sys/cpu from golang.org/x/crypto/blake2b+
LD golang.org/x/sys/unix from tailscale.com/net/netns+