@@ -14,6 +14,8 @@ import (
1414 "time"
1515
1616 "github.com/tailscale/hujson"
17+
18+ "golang.org/x/oauth2/clientcredentials"
1719)
1820
1921type (
@@ -44,28 +46,45 @@ type (
4446
4547const baseURL = "https://api.tailscale.com"
4648const contentType = "application/json"
49+ const defaultHttpClientTimeout = time .Minute
4750
4851// NewClient returns a new instance of the Client type that will perform operations against a chosen tailnet and will
4952// provide the apiKey for authorization. Additional options can be provided, see ClientOption for more details.
53+ //
54+ // To use OAuth Client credentials pass an empty string as apiKey and use WithOAuthClientCredentials() as below:
55+ //
56+ // client, err := tailscale.NewClient(
57+ // "",
58+ // tailnet,
59+ // tailscale.WithOAuthClientCredentials(oauthClientID, oauthClientSecret, oauthScopes),
60+ // )
5061func NewClient (apiKey , tailnet string , options ... ClientOption ) (* Client , error ) {
5162 u , err := url .Parse (baseURL )
5263 if err != nil {
5364 return nil , err
5465 }
5566
5667 c := & Client {
57- apiKey : apiKey ,
58- http : & http.Client {Timeout : time .Minute },
5968 baseURL : u ,
6069 tailnet : tailnet ,
6170 }
6271
72+ if apiKey != "" {
73+ c .apiKey = apiKey
74+ c .http = & http.Client {Timeout : defaultHttpClientTimeout }
75+ }
76+
6377 for _ , option := range options {
6478 if err = option (c ); err != nil {
6579 return nil , err
6680 }
6781 }
6882
83+ // apiKey or WithOAuthClientCredentials will initialize the http client. Fail here if both are not set.
84+ if c .apiKey == "" && c .http == nil {
85+ return nil , errors .New ("no authentication credentials provided" )
86+ }
87+
6988 return c , nil
7089}
7190
@@ -82,6 +101,27 @@ func WithBaseURL(baseURL string) ClientOption {
82101 }
83102}
84103
104+ // WithOAuthClientCredentials sets the OAuth Client Credentials to use for the Tailscale API.
105+ func WithOAuthClientCredentials (clientID , clientSecret string , scopes []string ) ClientOption {
106+ return func (c * Client ) error {
107+ relTokenURL , err := url .Parse ("/api/v2/oauth/token" )
108+ if err != nil {
109+ return err
110+ }
111+ oauthConfig := clientcredentials.Config {
112+ ClientID : clientID ,
113+ ClientSecret : clientSecret ,
114+ TokenURL : c .baseURL .ResolveReference (relTokenURL ).String (),
115+ Scopes : scopes ,
116+ }
117+
118+ // use context.Background() here, since this is used to refresh the token in the future
119+ c .http = oauthConfig .Client (context .Background ())
120+ c .http .Timeout = defaultHttpClientTimeout
121+ return nil
122+ }
123+ }
124+
85125// TODO: consider setting `headers` and `body` via opts to decrease the number of arguments.
86126func (c * Client ) buildRequest (ctx context.Context , method , uri string , headers map [string ]string , body interface {}) (* http.Request , error ) {
87127 u , err := c .baseURL .Parse (uri )
@@ -113,7 +153,11 @@ func (c *Client) buildRequest(ctx context.Context, method, uri string, headers m
113153 req .Header .Set ("Content-Type" , contentType )
114154 }
115155
116- req .SetBasicAuth (c .apiKey , "" )
156+ // c.apiKey will not be set on the client was configured with WithOAuthClientCredentials()
157+ if c .apiKey != "" {
158+ req .SetBasicAuth (c .apiKey , "" )
159+ }
160+
117161 return req , nil
118162}
119163
0 commit comments