Skip to content

Commit 5613d6b

Browse files
committed
v2: move OAuth support into the OAuthConfig type
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]>
1 parent cab510f commit 5613d6b

File tree

3 files changed

+56
-36
lines changed

3 files changed

+56
-36
lines changed

v2/README.md

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,9 @@ import (
2424
)
2525

2626
func main() {
27-
apiKey := os.Getenv("TAILSCALE_API_KEY")
28-
tailnet := os.Getenv("TAILSCALE_TAILNET")
29-
3027
&tsclient.Client{
31-
APIKey: apiKey,
32-
Tailnet: tailnet,
28+
Tailnet: os.Getenv("TAILSCALE_TAILNET"),
29+
APIKey: os.Getenv("TAILSCALE_API_KEY"),
3330
}
3431

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

5249
func main() {
53-
oauthClientID := os.Getenv("TAILSCALE_OAUTH_CLIENT_ID")
54-
tailnet := os.Getenv("TAILSCALE_OAUTH_CLIENT_SECRET")
50+
oauthClientID :=
51+
oauthClientID :=
5552
oauthScopes := []string{"all:write"}
53+
tailnet := os.Getenv("TAILSCALE_TAILNET")
5654

5755
&tsclient.Client{
58-
APIKey: apiKey,
59-
Tailnet: tailnet,
56+
Tailnet: os.Getenv("TAILSCALE_TAILNET"),
57+
HTTP: tsclient.OAuthConfig{
58+
ClientID: os.Getenv("TAILSCALE_OAUTH_CLIENT_ID"),
59+
ClientSecret: os.Getenv("TAILSCALE_OAUTH_CLIENT_SECRET"),
60+
Scopes: []string{"all:write"},
61+
}.HTTPClient(),
6062
}
61-
clientV2.UseOAuth(oauthClientID, oauthClientSecret, oauthScopes)
62-
63+
6364
devices, err := client.Devices().List(context.Background())
6465
}
6566
```

v2/client.go

Lines changed: 1 addition & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import (
1818
"time"
1919

2020
"github.com/tailscale/hujson"
21-
"golang.org/x/oauth2/clientcredentials"
2221
)
2322

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

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

4140
initOnce sync.Once
@@ -72,25 +71,17 @@ const defaultHttpClientTimeout = time.Minute
7271
const defaultUserAgent = "tailscale-client-go"
7372

7473
var defaultBaseURL *url.URL
75-
var oauthRelTokenURL *url.URL
7674

7775
func init() {
7876
var err error
7977
defaultBaseURL, err = url.Parse("https://api.tailscale.com")
8078
if err != nil {
8179
panic(fmt.Errorf("failed to parse defaultBaseURL: %w", err))
8280
}
83-
84-
oauthRelTokenURL, err = url.Parse("/api/v2/oauth/token")
85-
if err != nil {
86-
panic(fmt.Errorf("failed to parse oauthRelTokenURL: %s", err))
87-
}
8881
}
8982

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

118-
// UseOAuth configures the client to use the specified OAuth credentials.
119-
// If [Client].HTTP was previously specified, this replaces it.
120-
func (c *Client) UseOAuth(clientID, clientSecret string, scopes []string) {
121-
oauthConfig := clientcredentials.Config{
122-
ClientID: clientID,
123-
ClientSecret: clientSecret,
124-
TokenURL: c.BaseURL.ResolveReference(oauthRelTokenURL).String(),
125-
Scopes: scopes,
126-
}
127-
128-
// use context.Background() here, since this is used to refresh the token in the future
129-
c.HTTP = oauthConfig.Client(context.Background())
130-
c.HTTP.Timeout = defaultHttpClientTimeout
131-
}
132-
133109
// Contacts() provides access to https://tailscale.com/api#tag/contacts.
134110
func (c *Client) Contacts() *ContactsResource {
135111
c.init()

v2/oauth.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Copyright (c) David Bond, Tailscale Inc, & Contributors
2+
// SPDX-License-Identifier: MIT
3+
4+
package tsclient
5+
6+
import (
7+
"context"
8+
"net/http"
9+
"path"
10+
11+
"golang.org/x/oauth2/clientcredentials"
12+
)
13+
14+
// OAuthConfig provides a mechanism for configuring OAuth authentication.
15+
type OAuthConfig struct {
16+
// ClientID is the client ID of the OAuth client.
17+
ClientID string
18+
// ClientSecret is the client secret of the OAuth client.
19+
ClientSecret string
20+
// Scopes are the scopes to request when generating tokens for this OAuth client.
21+
Scopes []string
22+
// BaseURL is an optional base URL for the API server to which we'll connect. Defaults to https://api.tailscale.com.
23+
BaseURL string
24+
}
25+
26+
// HTTPClient constructs an HTTP client that authenticates using OAuth.
27+
func (ocfg OAuthConfig) HTTPClient() *http.Client {
28+
baseURL := ocfg.BaseURL
29+
if baseURL == "" {
30+
baseURL = defaultBaseURL.String()
31+
}
32+
oauthConfig := clientcredentials.Config{
33+
ClientID: ocfg.ClientID,
34+
ClientSecret: ocfg.ClientSecret,
35+
Scopes: ocfg.Scopes,
36+
TokenURL: path.Join(baseURL, "/api/v2/oauth/token"),
37+
}
38+
39+
// Use context.Background() here, since this is used to refresh the token in the future.
40+
client := oauthConfig.Client(context.Background())
41+
client.Timeout = defaultHttpClientTimeout
42+
return client
43+
}

0 commit comments

Comments
 (0)