// Copyright (c) Tailscale Inc & AUTHORS // 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, v string, tags []string) (string, error) { if !strings.HasPrefix(v, "tskey-client-") { return v, nil } if len(tags) == 0 { return "", errors.New("oauth authkeys require --advertise-tags") } clientSecret, named, _ := strings.Cut(v, "?") attrs, err := url.ParseQuery(named) if err != nil { return "", err } for k := range attrs { switch k { case "ephemeral", "preauthorized", "baseURL": 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 } baseURL := "https://api.tailscale.com" if v := attrs.Get("baseURL"); v != "" { baseURL = v } credentials := clientcredentials.Config{ ClientID: "some-client-id", // ignored ClientSecret: clientSecret, 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: tags, }, }, } authkey, _, err := tsClient.CreateKey(ctx, caps) if err != nil { return "", err } return authkey, nil }