Skip to content

Commit

Permalink
v2: move OAuth support into the OAuthConfig type
Browse files Browse the repository at this point in the history
Including it on the core Client object complicated state management.

The example in the README now shows how to use OAuthConfig to obtain an
HTTP client to use with our Client.

Updates tailscale/corp#21867

Signed-off-by: Percy Wegmann <[email protected]>
  • Loading branch information
oxtoacart committed Sep 16, 2024
1 parent cab510f commit 5613d6b
Show file tree
Hide file tree
Showing 3 changed files with 56 additions and 36 deletions.
23 changes: 12 additions & 11 deletions v2/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,9 @@ import (
)

func main() {
apiKey := os.Getenv("TAILSCALE_API_KEY")
tailnet := os.Getenv("TAILSCALE_TAILNET")

&tsclient.Client{
APIKey: apiKey,
Tailnet: tailnet,
Tailnet: os.Getenv("TAILSCALE_TAILNET"),
APIKey: os.Getenv("TAILSCALE_API_KEY"),
}

devices, err := client.Devices().List(context.Background())
Expand All @@ -50,16 +47,20 @@ import (
)

func main() {
oauthClientID := os.Getenv("TAILSCALE_OAUTH_CLIENT_ID")
tailnet := os.Getenv("TAILSCALE_OAUTH_CLIENT_SECRET")
oauthClientID :=
oauthClientID :=
oauthScopes := []string{"all:write"}
tailnet := os.Getenv("TAILSCALE_TAILNET")

&tsclient.Client{
APIKey: apiKey,
Tailnet: tailnet,
Tailnet: os.Getenv("TAILSCALE_TAILNET"),
HTTP: tsclient.OAuthConfig{
ClientID: os.Getenv("TAILSCALE_OAUTH_CLIENT_ID"),
ClientSecret: os.Getenv("TAILSCALE_OAUTH_CLIENT_SECRET"),
Scopes: []string{"all:write"},
}.HTTPClient(),
}
clientV2.UseOAuth(oauthClientID, oauthClientSecret, oauthScopes)


devices, err := client.Devices().List(context.Background())
}
```
26 changes: 1 addition & 25 deletions v2/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import (
"time"

"github.com/tailscale/hujson"
"golang.org/x/oauth2/clientcredentials"
)

type (
Expand All @@ -29,13 +28,13 @@ type (
// UserAgent configures the User-Agent HTTP header for requests. Defaults to "tailscale-client-go".
UserAgent string
// APIKey allows specifying an APIKey to use for authentication.
// To use OAuth Client credentials, construct an [http.Client] using [OAuthConfig] and specify that below.
APIKey string
// Tailnet allows specifying a specific Tailnet by name, to which this Client will connect by default.
Tailnet string

// HTTP is the [http.Client] to use for requests to the API server.
// If not specified, a new [http.Client] with a Timeout of 1 minute will be used.
// This will be ignored if using [Client].UseOAuth.
HTTP *http.Client

initOnce sync.Once
Expand Down Expand Up @@ -72,25 +71,17 @@ const defaultHttpClientTimeout = time.Minute
const defaultUserAgent = "tailscale-client-go"

var defaultBaseURL *url.URL
var oauthRelTokenURL *url.URL

func init() {
var err error
defaultBaseURL, err = url.Parse("https://api.tailscale.com")
if err != nil {
panic(fmt.Errorf("failed to parse defaultBaseURL: %w", err))
}

oauthRelTokenURL, err = url.Parse("/api/v2/oauth/token")
if err != nil {
panic(fmt.Errorf("failed to parse oauthRelTokenURL: %s", err))
}
}

// init returns a new instance of the Client type that will perform operations against a chosen tailnet and will
// provide the apiKey for authorization.
//
// To use OAuth Client credentials, call [Client].UseOAuth.
func (c *Client) init() {
c.initOnce.Do(func() {
if c.BaseURL == nil {
Expand All @@ -115,21 +106,6 @@ func (c *Client) init() {
})
}

// UseOAuth configures the client to use the specified OAuth credentials.
// If [Client].HTTP was previously specified, this replaces it.
func (c *Client) UseOAuth(clientID, clientSecret string, scopes []string) {
oauthConfig := clientcredentials.Config{
ClientID: clientID,
ClientSecret: clientSecret,
TokenURL: c.BaseURL.ResolveReference(oauthRelTokenURL).String(),
Scopes: scopes,
}

// use context.Background() here, since this is used to refresh the token in the future
c.HTTP = oauthConfig.Client(context.Background())
c.HTTP.Timeout = defaultHttpClientTimeout
}

// Contacts() provides access to https://tailscale.com/api#tag/contacts.
func (c *Client) Contacts() *ContactsResource {
c.init()
Expand Down
43 changes: 43 additions & 0 deletions v2/oauth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Copyright (c) David Bond, Tailscale Inc, & Contributors
// SPDX-License-Identifier: MIT

package tsclient

import (
"context"
"net/http"
"path"

"golang.org/x/oauth2/clientcredentials"
)

// OAuthConfig provides a mechanism for configuring OAuth authentication.
type OAuthConfig struct {
// ClientID is the client ID of the OAuth client.
ClientID string
// ClientSecret is the client secret of the OAuth client.
ClientSecret string
// Scopes are the scopes to request when generating tokens for this OAuth client.
Scopes []string
// BaseURL is an optional base URL for the API server to which we'll connect. Defaults to https://api.tailscale.com.
BaseURL string
}

// HTTPClient constructs an HTTP client that authenticates using OAuth.
func (ocfg OAuthConfig) HTTPClient() *http.Client {
baseURL := ocfg.BaseURL
if baseURL == "" {
baseURL = defaultBaseURL.String()
}
oauthConfig := clientcredentials.Config{
ClientID: ocfg.ClientID,
ClientSecret: ocfg.ClientSecret,
Scopes: ocfg.Scopes,
TokenURL: path.Join(baseURL, "/api/v2/oauth/token"),
}

// Use context.Background() here, since this is used to refresh the token in the future.
client := oauthConfig.Client(context.Background())
client.Timeout = defaultHttpClientTimeout
return client
}

0 comments on commit 5613d6b

Please sign in to comment.