diff --git a/README.md b/README.md index 884c02f..8f1323d 100644 --- a/README.md +++ b/README.md @@ -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()) diff --git a/client.go b/client.go index d6aefb1..a99419f 100644 --- a/client.go +++ b/client.go @@ -21,6 +21,14 @@ 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. @@ -28,8 +36,11 @@ type Client struct { // 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. @@ -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} diff --git a/oauth.go b/oauth.go index dcfa321..46eb79c 100644 --- a/oauth.go +++ b/oauth.go @@ -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 @@ -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 == "" {