Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 31 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,40 @@ import (
func main() {
client := &tailscale.Client{
Tailnet: os.Getenv("TAILSCALE_TAILNET"),
HTTP: tailscale.OAuthConfig{
Auth: &tailscale.OAuth{
ClientID: os.Getenv("TAILSCALE_OAUTH_CLIENT_ID"),
ClientSecret: os.Getenv("TAILSCALE_OAUTH_CLIENT_SECRET"),
Scopes: []string{"all:write"},
}.HTTPClient(),
},
}

devices, err := client.Devices().List(context.Background())
}
```

## Example (Using Your Own Authentication Mechanism)

```go
package main

import (
"context"
"os"

"tailscale.com/client/tailscale/v2"
)

type MyAuth struct {...}

func (a *MyAuth) HTTPClient(orig *http.Client, baseURL string) *http.Client {
// build an HTTP client that adds authentication to outgoing requests
// see tailscale.OAuth for an example.
}

func main() {
client := &tailscale.Client{
Tailnet: os.Getenv("TAILSCALE_TAILNET"),
Auth: &MyAuth{...},
}

devices, err := client.Devices().List(context.Background())
Expand Down
17 changes: 16 additions & 1 deletion client.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,26 @@ import (
"github.com/tailscale/hujson"
)

// Auth is a pluggable mechanism for authenticating requests.
type Auth interface {
// HTTPClient builds an http.Client that uses orig as a starting point and
// adds its own authentication to outgoing requests. baseURL is the base URL
// of the API server to which we will be authenticating.
HTTPClient(orig *http.Client, baseURL string) *http.Client
}

// Client is used to perform actions against the Tailscale API.
type Client struct {
// BaseURL is the base URL for accessing the Tailscale API server. Defaults to https://api.tailscale.com.
BaseURL *url.URL
// 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.
// To use OAuth Client credentials, specify OAuthCredentials instead.
APIKey string
// Auth specifies a mechanism for adding authentication to outgoing requests.
// If provided, APIKey is ignored.
Auth Auth
// Tailnet allows specifying a specific tailnet by name, to which this Client will connect by default.
// If Tailnet is left blank, the client will connect to default tailnet based on the client's credential,
// using the "-" (dash) default tailnet path.
Expand Down Expand Up @@ -97,6 +108,10 @@ func (c *Client) init() {
if c.Tailnet == "" {
c.Tailnet = "-"
}
if c.Auth != nil {
c.APIKey = ""
c.HTTP = c.Auth.HTTPClient(c.HTTP, c.BaseURL.String())
}
c.contacts = &ContactsResource{c}
c.devicePosture = &DevicePostureResource{c}
c.devices = &DevicesResource{c}
Expand Down
41 changes: 41 additions & 0 deletions oauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,50 @@ import (
"context"
"net/http"

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

// Ensure that [OAuth] implements the [Auth] interface.
var _ Auth = &OAuth{}

// OAuth configures OAuth authentication.
type OAuth 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
}

// HTTPClient constructs an HTTP client based on the provided HTTP client, authenticating
// every request using OAuth and fetching tokens from baseURL + "/api/v2/oauth/token" as
// necessary based on when the token expires.
func (o *OAuth) HTTPClient(orig *http.Client, baseURL string) *http.Client {
oauthConfig := clientcredentials.Config{
ClientID: o.ClientID,
ClientSecret: o.ClientSecret,
Scopes: o.Scopes,
TokenURL: baseURL + "/api/v2/oauth/token",
}

// Use context.Background() here, since this is used to refresh the token in the future.
tokenSource := oauthConfig.TokenSource(context.Background())

return &http.Client{
Transport: &oauth2.Transport{
Base: orig.Transport,
Source: oauth2.ReuseTokenSource(nil, tokenSource),
},
CheckRedirect: orig.CheckRedirect,
Jar: orig.Jar,
Timeout: orig.Timeout,
}
}

// OAuthConfig provides a mechanism for configuring OAuth authentication.
// Deprecated: use [OAuth] instead.
type OAuthConfig struct {
// ClientID is the client ID of the OAuth client.
ClientID string
Expand All @@ -23,6 +63,7 @@ type OAuthConfig struct {
}

// HTTPClient constructs an HTTP client that authenticates using OAuth.
// Deprecated: use [OAuth] instead.
func (ocfg OAuthConfig) HTTPClient() *http.Client {
baseURL := ocfg.BaseURL
if baseURL == "" {
Expand Down