From 5049905dbd9005b63e13d87b7809d2cb17d3b246 Mon Sep 17 00:00:00 2001 From: Rubens Farias Date: Wed, 15 Feb 2023 17:17:45 -0500 Subject: [PATCH] Add Oauth2 TokenSource support PagerDuty currently uses Oauth2 tokens that need to be acquired and refreshed with some frequency. The current setup only supports static string auth tokens. While uses could do the Oauth2 handling themselves, it'd lead to a quite challenging maneuvering to re-create the PagerDuty client with the new token once the previous expires, which is unnecessary when there are official canned libraries to do that for us. Another option could almost be to just use the oauth2 created Client [1], but the way `prepRequest` works would interfere with it. Fixes #407 1: https://pkg.go.dev/golang.org/x/oauth2#Config.Client --- client.go | 40 ++++++++++++++++++++++++++++++---------- client_test.go | 3 ++- go.mod | 7 ++++++- go.sum | 20 ++++++++++++++++++++ 4 files changed, 58 insertions(+), 12 deletions(-) diff --git a/client.go b/client.go index 559331ef..1b9cd8fd 100644 --- a/client.go +++ b/client.go @@ -22,6 +22,8 @@ import ( "strings" "sync/atomic" "time" + + "golang.org/x/oauth2" ) // Version is current version of this client. @@ -270,7 +272,7 @@ type Client struct { lastRequest *atomic.Value lastResponse *atomic.Value - authToken string + oauthTokenSource oauth2.TokenSource apiEndpoint string v2EventsAPIEndpoint string @@ -283,13 +285,12 @@ type Client struct { HTTPClient HTTPClient } -// NewClient creates an API client using an account/user API token -func NewClient(authToken string, options ...ClientOptions) *Client { +func newClient(oauthTokenSource oauth2.TokenSource, options ...ClientOptions) *Client { client := Client{ debugFlag: new(uint64), lastRequest: &atomic.Value{}, lastResponse: &atomic.Value{}, - authToken: authToken, + oauthTokenSource: oauthTokenSource, apiEndpoint: apiEndpoint, v2EventsAPIEndpoint: v2EventsAPIEndpoint, authType: apiToken, @@ -303,9 +304,19 @@ func NewClient(authToken string, options ...ClientOptions) *Client { return &client } +// NewClient creates an API client using an account/user API token +func NewClient(authToken string, options ...ClientOptions) *Client { + return newClient(oauth2.StaticTokenSource(&oauth2.Token{AccessToken: authToken}), options...) +} + // NewOAuthClient creates an API client using an OAuth token func NewOAuthClient(authToken string, options ...ClientOptions) *Client { - return NewClient(authToken, WithOAuth()) + return newClient(oauth2.StaticTokenSource(&oauth2.Token{AccessToken: authToken}), append(options, WithOAuth())...) +} + +// NewOAuthClientWithTokenSource creates an API client using an OAuth token +func NewOAuthClientWithTokenSource(authToken oauth2.TokenSource, options ...ClientOptions) *Client { + return newClient(authToken, append(options, WithOAuth())...) } // ClientOptions allows for options to be passed into the Client for customization @@ -440,7 +451,9 @@ func (c *Client) LastAPIResponse() (*http.Response, bool) { // assumes any request body is in JSON format and sets the Content-Type to // application/json. func (c *Client) Do(r *http.Request, authRequired bool) (*http.Response, error) { - c.prepRequest(r, authRequired, nil) + if err := c.prepRequest(r, authRequired, nil); err != nil { + return nil, fmt.Errorf("failed to prepare request: %w", err) + } return c.HTTPClient.Do(r) } @@ -478,7 +491,7 @@ const ( contentTypeHeader = "application/json" ) -func (c *Client) prepRequest(req *http.Request, authRequired bool, headers map[string]string) { +func (c *Client) prepRequest(req *http.Request, authRequired bool, headers map[string]string) error { req.Header.Set("Accept", acceptHeader) for k, v := range headers { @@ -486,16 +499,21 @@ func (c *Client) prepRequest(req *http.Request, authRequired bool, headers map[s } if authRequired { + token, err := c.oauthTokenSource.Token() + if err != nil { + return err + } switch c.authType { case oauthToken: - req.Header.Set("Authorization", "Bearer "+c.authToken) + req.Header.Set("Authorization", "Bearer "+token.AccessToken) default: - req.Header.Set("Authorization", "Token token="+c.authToken) + req.Header.Set("Authorization", "Token token="+token.AccessToken) } } req.Header.Set("User-Agent", userAgentHeader) req.Header.Set("Content-Type", contentTypeHeader) + return nil } func dupeRequest(r *http.Request) (*http.Request, error) { @@ -541,7 +559,9 @@ func (c *Client) doWithEndpoint(ctx context.Context, endpoint, method, path stri return nil, fmt.Errorf("failed to build request: %w", err) } - c.prepRequest(req, authRequired, headers) + if err := c.prepRequest(req, authRequired, headers); err != nil { + return nil, fmt.Errorf("failed to prepare request: %w", err) + } // if in debug mode, copy request before making it if c.debugCaptureRequest() { diff --git a/client_test.go b/client_test.go index f2dced86..45bc1f65 100644 --- a/client_test.go +++ b/client_test.go @@ -14,6 +14,7 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "golang.org/x/oauth2" ) var ( @@ -42,7 +43,7 @@ func defaultTestClient(serverURL, authToken string) *Client { return &Client{ v2EventsAPIEndpoint: serverURL, apiEndpoint: serverURL, - authToken: authToken, + oauthTokenSource: oauth2.StaticTokenSource(&oauth2.Token{AccessToken: authToken}), HTTPClient: defaultHTTPClient, debugFlag: new(uint64), lastRequest: &atomic.Value{}, diff --git a/go.mod b/go.mod index cca9e8b5..473749c6 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,7 @@ require ( github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310 // indirect github.com/bgentry/speakeasy v0.1.0 // indirect github.com/fatih/color v1.7.0 // indirect + github.com/golang/protobuf v1.5.2 // indirect github.com/google/uuid v1.1.2 // indirect github.com/hashicorp/errwrap v1.0.0 // indirect github.com/hashicorp/go-multierror v1.0.0 // indirect @@ -31,5 +32,9 @@ require ( github.com/shopspring/decimal v1.2.0 // indirect github.com/spf13/cast v1.3.1 // indirect golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a // indirect - golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect + golang.org/x/net v0.6.0 // indirect + golang.org/x/oauth2 v0.5.0 // indirect + golang.org/x/sys v0.5.0 // indirect + google.golang.org/appengine v1.6.7 // indirect + google.golang.org/protobuf v1.28.0 // indirect ) diff --git a/go.sum b/go.sum index 0984e2a2..e648783a 100644 --- a/go.sum +++ b/go.sum @@ -13,7 +13,12 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= @@ -63,12 +68,27 @@ golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a h1:vclmkQCjlDX5OydZ9wv8rBCcS0QyQY66Mpf/7BZbInM= golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.6.0 h1:L4ZwwTvKW9gr0ZMS1yrHD9GZhIuVjOBBnaKH+SPQK0Q= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/oauth2 v0.5.0 h1:HuArIo48skDwlrvM3sEdHXElYslAMsf3KwRkkW4MC4s= +golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=