From ea9dd6155990d098538029d9322583f0f094cc6c Mon Sep 17 00:00:00 2001 From: Peter Grlica Date: Wed, 4 Aug 2021 10:25:12 +0200 Subject: [PATCH] Added JWT and oauth2 outbound authenticators Proxy auth definitions for the UI --- go.mod | 1 + go.sum | 2 + pkg/apigw/apigw.go | 5 + pkg/apigw/auth.go | 128 ----- pkg/apigw/processer.go | 132 +---- pkg/apigw/processer_test.go | 2 +- pkg/apigw/proxy.go | 145 ++++++ pkg/apigw/proxy_auth.go | 330 ++++++++++++ .../{auth_test.go => proxy_auth_test.go} | 26 +- pkg/apigw/registry.go | 4 +- system/rest.yaml | 6 +- system/rest/apigw_function.go | 12 +- system/rest/handlers/apigwFunction.go | 43 +- system/rest/request/apigwFunction.go | 33 +- system/service/apigw_function.go | 22 +- vendor/golang.org/x/oauth2/README.md | 10 +- .../x/oauth2/authhandler/authhandler.go | 56 +++ .../clientcredentials/clientcredentials.go | 120 +++++ .../x/oauth2/google/appengine_gen1.go | 1 + .../x/oauth2/google/appengine_gen2_flex.go | 1 + vendor/golang.org/x/oauth2/google/default.go | 95 +++- vendor/golang.org/x/oauth2/google/doc.go | 43 +- vendor/golang.org/x/oauth2/google/google.go | 53 +- .../google/internal/externalaccount/aws.go | 470 ++++++++++++++++++ .../externalaccount/basecredentials.go | 184 +++++++ .../internal/externalaccount/clientauth.go | 44 ++ .../google/internal/externalaccount/err.go | 18 + .../externalaccount/filecredsource.go | 57 +++ .../internal/externalaccount/impersonate.go | 83 ++++ .../internal/externalaccount/sts_exchange.go | 104 ++++ .../internal/externalaccount/urlcredsource.go | 74 +++ vendor/golang.org/x/oauth2/google/jwt.go | 37 +- .../x/oauth2/internal/client_appengine.go | 1 + vendor/modules.txt | 6 +- 34 files changed, 2016 insertions(+), 332 deletions(-) delete mode 100644 pkg/apigw/auth.go create mode 100644 pkg/apigw/proxy.go create mode 100644 pkg/apigw/proxy_auth.go rename pkg/apigw/{auth_test.go => proxy_auth_test.go} (83%) create mode 100644 vendor/golang.org/x/oauth2/authhandler/authhandler.go create mode 100644 vendor/golang.org/x/oauth2/clientcredentials/clientcredentials.go create mode 100644 vendor/golang.org/x/oauth2/google/internal/externalaccount/aws.go create mode 100644 vendor/golang.org/x/oauth2/google/internal/externalaccount/basecredentials.go create mode 100644 vendor/golang.org/x/oauth2/google/internal/externalaccount/clientauth.go create mode 100644 vendor/golang.org/x/oauth2/google/internal/externalaccount/err.go create mode 100644 vendor/golang.org/x/oauth2/google/internal/externalaccount/filecredsource.go create mode 100644 vendor/golang.org/x/oauth2/google/internal/externalaccount/impersonate.go create mode 100644 vendor/golang.org/x/oauth2/google/internal/externalaccount/sts_exchange.go create mode 100644 vendor/golang.org/x/oauth2/google/internal/externalaccount/urlcredsource.go diff --git a/go.mod b/go.mod index de19c9805..43adf06ec 100644 --- a/go.mod +++ b/go.mod @@ -79,6 +79,7 @@ require ( go.uber.org/zap v1.16.0 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a // indirect + golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914 google.golang.org/grpc v1.32.0 google.golang.org/protobuf v1.25.0 gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect diff --git a/go.sum b/go.sum index e1a33a2bc..af7f1ec24 100644 --- a/go.sum +++ b/go.sum @@ -544,6 +544,8 @@ golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4Iltr golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43 h1:ld7aEMNHoBnnDAX15v1T6z31v8HwR2A9FYOuAhWqkwc= golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914 h1:3B43BWw0xEBsLZ/NO1VALz6fppU3481pik+2Ksv45z8= +golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= diff --git a/pkg/apigw/apigw.go b/pkg/apigw/apigw.go index 5f5314946..5ef18f88b 100644 --- a/pkg/apigw/apigw.go +++ b/pkg/apigw/apigw.go @@ -210,6 +210,11 @@ func (s *apigw) Funcs(kind string) (list functionMetaList) { return } +func (s *apigw) ProxyAuthDef() (list []*proxyAuthDefinition) { + list = ProxyAuthDef() + return +} + func NewWorkflow() (wf WfExecer) { return as.Workflow(&zap.Logger{}, options.CorredorOpt{}) } diff --git a/pkg/apigw/auth.go b/pkg/apigw/auth.go deleted file mode 100644 index e776ba68a..000000000 --- a/pkg/apigw/auth.go +++ /dev/null @@ -1,128 +0,0 @@ -package apigw - -import ( - "encoding/base64" - "fmt" - "net/http" -) - -const ( - authTypeOauth2 authType = "oauth2" - authTypeHeader authType = "header" - authTypeQuery authType = "query" - authTypeBasic authType = "basic" - authTypeJwt authType = "jwt" - authTypeNoop authType = "noop" -) - -type ( - AuthServicer interface { - Do(*http.Request) error - } - - authServicerNoop struct{} - - authServicerHeader struct { - params map[string]interface{} - } - - authServicerQuery struct { - params map[string]interface{} - } - - authServicerBasic struct { - user string - pass string - } - - authType string - - authParams struct { - Type authType `json:"type"` - Params map[string]interface{} `json:"params"` - } -) - -func NewAuthHeader(p authParams) (s authServicerHeader, err error) { - s = authServicerHeader{ - params: p.Params, - } - - return -} - -func NewAuthQuery(p authParams) (s authServicerQuery, err error) { - s = authServicerQuery{ - params: p.Params, - } - - return -} - -func NewAuthBasic(p authParams) (s authServicerBasic, err error) { - var ( - ok bool - user, pass string - ) - - if user, ok = p.Params["username"].(string); !ok { - err = fmt.Errorf("invalid param username") - return - } - - if pass, ok = p.Params["password"].(string); !ok { - err = fmt.Errorf("invalid param password") - return - } - - s = authServicerBasic{user: user, pass: pass} - - return -} - -func NewAuthServicer(c *http.Client, p authParams) (AuthServicer, error) { - switch p.Type { - case authTypeHeader: - return NewAuthHeader(p) - case authTypeQuery: - return NewAuthQuery(p) - case authTypeBasic: - return NewAuthBasic(p) - default: - return authServicerNoop{}, nil - } -} - -func (s authServicerHeader) Do(r *http.Request) error { - for k, v := range s.params { - r.Header.Add(k, v.(string)) - } - return nil -} - -func (s authServicerQuery) Do(r *http.Request) error { - if len(s.params) == 0 { - return nil - } - - q := r.URL.Query() - - for k, v := range s.params { - q.Set(k, v.(string)) - } - - r.URL.RawQuery = q.Encode() - - return nil -} - -func (s authServicerBasic) Do(r *http.Request) error { - bs := base64.URLEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", s.user, s.pass))) - - r.Header.Set("Authorization", fmt.Sprintf("Basic %s", bs)) - return nil -} - -func (s authServicerNoop) Do(r *http.Request) error { - return nil -} diff --git a/pkg/apigw/processer.go b/pkg/apigw/processer.go index dcca23377..46716746e 100644 --- a/pkg/apigw/processer.go +++ b/pkg/apigw/processer.go @@ -9,9 +9,6 @@ import ( "fmt" "io" "net/http" - "net/http/httputil" - "net/url" - "time" atypes "github.com/cortezaproject/corteza-server/automation/types" "github.com/cortezaproject/corteza-server/pkg/expr" @@ -37,6 +34,8 @@ type ( Exec(ctx context.Context, workflowID uint64, p atypes.WorkflowExecParams) (*expr.Vars, atypes.Stacktrace, error) } + SecureStorager interface{} + processerWorkflow struct { functionMeta d WfExecer @@ -46,18 +45,6 @@ type ( } } - processerProxy struct { - functionMeta - a AuthServicer - c *http.Client - log *zap.Logger - - params struct { - Location string `json:"location"` - Auth authParams `json:"auth"` - } - } - processerPayload struct { functionMeta vm jsenv.Vm @@ -193,121 +180,6 @@ func (h processerWorkflow) Exec(ctx context.Context, scope *scp) error { return err } -func NewProcesserProxy(l *zap.Logger, c *http.Client) (p *processerProxy) { - p = &processerProxy{} - - p.c = c - p.log = l - - p.Step = 2 - p.Name = "processerProxy" - p.Label = "Proxy processer" - p.Kind = FunctionKindProcesser - - p.Args = []*functionMetaArg{ - { - Type: "text", - Label: "location", - Options: map[string]interface{}{}, - }, - } - - return -} - -func (h processerProxy) String() string { - return fmt.Sprintf("apigw function %s (%s)", h.Name, h.Label) -} - -func (h processerProxy) Meta() functionMeta { - return h.functionMeta -} - -func (f *processerProxy) Merge(params []byte) (Handler, error) { - err := json.NewDecoder(bytes.NewBuffer(params)).Decode(&f.params) - - if err != nil { - return nil, err - } - - // get the auth mechanism - f.a, err = NewAuthServicer(f.c, f.params.Auth) - - if err != nil { - return nil, fmt.Errorf("could not load auth servicer for proxying: %s", err) - } - - return f, err -} - -func (h processerProxy) Exec(ctx context.Context, scope *scp) (err error) { - ctx, cancel := context.WithTimeout(ctx, scope.Opts().ProxyOutboundTimeout) - defer cancel() - - req := scope.Request() - log := h.log.With(zap.String("ref", h.Name)) - - outreq := req.Clone(ctx) - - l, err := url.ParseRequestURI(h.params.Location) - - if err != nil { - return fmt.Errorf("could not parse destination location for proxying: %s", err) - } - - // should we preserve query params? headers? post data? - outreq.URL = l - outreq.RequestURI = "" - outreq.Method = req.Method - outreq.Host = l.Hostname() - - // use authservicer, set any additional headers - err = h.a.Do(outreq) - - if err != nil { - return fmt.Errorf("errors setting auth for proxying: %s", err) - } - - // merge the old query params to the new request - // do not overwrite old ones - // do it after the authServicer, since we also may add them there - mergeQueryParams(req, outreq) - - if scope.Opts().ProxyEnableDebugLog { - o, _ := httputil.DumpRequestOut(outreq, false) - log.Debug("proxy outbound request", zap.Any("request", string(o))) - } - - // temporary metrics before the proper functionality - startTime := time.Now() - - // todo - disable / enable follow redirects, already - // added to options - resp, err := h.c.Do(outreq) - - if err != nil { - return fmt.Errorf("could not proxy request: %s", err) - } - - if scope.Opts().ProxyEnableDebugLog { - o, _ := httputil.DumpResponse(resp, false) - log.Debug("proxy outbound response", zap.Any("request", string(o)), zap.Duration("duration", time.Since(startTime))) - } - - b, err := io.ReadAll(resp.Body) - - if err != nil { - return fmt.Errorf("could not read get body on proxy request: %s", err) - } - - mergeHeaders(resp.Header, scope.Writer().Header()) - - // add to writer - scope.Writer().Write(b) - - return nil -} - func NewProcesserPayload(l *zap.Logger) (p *processerPayload) { p = &processerPayload{} diff --git a/pkg/apigw/processer_test.go b/pkg/apigw/processer_test.go index acb47bac5..59ea6544f 100644 --- a/pkg/apigw/processer_test.go +++ b/pkg/apigw/processer_test.go @@ -174,7 +174,7 @@ func Test_processerProxy(t *testing.T) { rq, _ = http.NewRequest("POST", "/foo", strings.NewReader(`custom request body`)) } - proxy := NewProcesserProxy(zap.NewNop(), c) + proxy := NewProcesserProxy(zap.NewNop(), c, secureStorageTodo{}) proxy.Merge([]byte(tc.params)) scope := &scp{ diff --git a/pkg/apigw/proxy.go b/pkg/apigw/proxy.go new file mode 100644 index 000000000..dc171313d --- /dev/null +++ b/pkg/apigw/proxy.go @@ -0,0 +1,145 @@ +package apigw + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/http/httputil" + "net/url" + "time" + + "go.uber.org/zap" +) + +type ( + processerProxy struct { + functionMeta + a ProxyAuthServicer + s SecureStorager + c *http.Client + log *zap.Logger + + params struct { + Location string `json:"location"` + Auth proxyAuthParams `json:"auth"` + } + } +) + +func NewProcesserProxy(l *zap.Logger, c *http.Client, s SecureStorager) (p *processerProxy) { + p = &processerProxy{} + + p.c = c + p.s = s + p.log = l + + p.Step = 2 + p.Name = "processerProxy" + p.Label = "Proxy processer" + p.Kind = FunctionKindProcesser + + p.Args = []*functionMetaArg{ + { + Type: "text", + Label: "location", + Options: map[string]interface{}{}, + }, + } + + return +} + +func (h processerProxy) String() string { + return fmt.Sprintf("apigw function %s (%s)", h.Name, h.Label) +} + +func (h processerProxy) Meta() functionMeta { + return h.functionMeta +} + +func (f *processerProxy) Merge(params []byte) (Handler, error) { + err := json.NewDecoder(bytes.NewBuffer(params)).Decode(&f.params) + + if err != nil { + return nil, err + } + + // get the auth mechanism + f.a, err = NewProxyAuthServicer(f.c, f.params.Auth, f.s) + + if err != nil { + return nil, fmt.Errorf("could not load auth servicer for proxying: %s", err) + } + + return f, err +} + +func (h processerProxy) Exec(ctx context.Context, scope *scp) (err error) { + ctx, cancel := context.WithTimeout(ctx, scope.Opts().ProxyOutboundTimeout) + defer cancel() + + req := scope.Request() + log := h.log.With(zap.String("ref", h.Name)) + + outreq := req.Clone(ctx) + + l, err := url.ParseRequestURI(h.params.Location) + + if err != nil { + return fmt.Errorf("could not parse destination location for proxying: %s", err) + } + + outreq.URL = l + outreq.RequestURI = "" + outreq.Method = req.Method + outreq.Host = l.Hostname() + + // use authservicer, set any additional headers + err = h.a.Do(outreq) + + if err != nil { + return fmt.Errorf("errors setting auth for proxying: %s", err) + } + + // merge the old query params to the new request + // do not overwrite old ones + // do it after the authServicer, since we also may add them there + mergeQueryParams(req, outreq) + + if scope.Opts().ProxyEnableDebugLog { + o, _ := httputil.DumpRequestOut(outreq, false) + log.Debug("proxy outbound request", zap.Any("request", string(o))) + } + + // temporary metrics before the proper functionality + startTime := time.Now() + + // todo - disable / enable follow redirects, already + // added to options + resp, err := h.c.Do(outreq) + + if err != nil { + return fmt.Errorf("could not proxy request: %s", err) + } + + if scope.Opts().ProxyEnableDebugLog { + o, _ := httputil.DumpResponse(resp, false) + log.Debug("proxy outbound response", zap.Any("request", string(o)), zap.Duration("duration", time.Since(startTime))) + } + + b, err := io.ReadAll(resp.Body) + + if err != nil { + return fmt.Errorf("could not read get body on proxy request: %s", err) + } + + mergeHeaders(resp.Header, scope.Writer().Header()) + + // add to writer + scope.Writer().Write(b) + + return nil +} diff --git a/pkg/apigw/proxy_auth.go b/pkg/apigw/proxy_auth.go new file mode 100644 index 000000000..14a1bab15 --- /dev/null +++ b/pkg/apigw/proxy_auth.go @@ -0,0 +1,330 @@ +package apigw + +import ( + "context" + "fmt" + "net/http" + "strings" + + "golang.org/x/oauth2" + "golang.org/x/oauth2/clientcredentials" +) + +const ( + proxyAuthTypeOauth2 proxyAuthType = "oauth2" + proxyAuthTypeHeader proxyAuthType = "header" + proxyAuthTypeQuery proxyAuthType = "query" + proxyAuthTypeBasic proxyAuthType = "basic" + proxyAuthTypeJWT proxyAuthType = "jwt" + proxyAuthTypeNoop proxyAuthType = "noop" +) + +type ( + ProxyAuthServicer interface { + Do(*http.Request) error + } + + proxyAuthServicerNoop struct{} + + proxyAuthServicerHeader struct { + params map[string]interface{} + } + + proxyAuthServicerQuery struct { + params map[string]interface{} + } + + proxyAuthServicerBasic struct { + user string + pass string + } + + proxyAuthServicerOauth2 struct { + c *http.Client + + client string + secret string + scope []string + tokenUrl string + + Args []*proxyAuthMetaArg `json:"params"` + } + + proxyAuthServicerJWT struct { + JWT string + } + + proxyAuthType string + + proxyAuthParams struct { + Type proxyAuthType `json:"type"` + Params map[string]interface{} `json:"params"` + } + + proxyAuthMetaArg struct { + Label string `json:"label"` + Type string `json:"type"` + Example string `json:"example,omitempty"` + Options map[string]interface{} `json:"options,omitempty"` + } + + proxyAuthDefinition struct { + Type proxyAuthType `json:"type"` + Params []*proxyAuthMetaArg `json:"params"` + } +) + +func NewProxyAuthHeader(p proxyAuthParams) (s proxyAuthServicerHeader, err error) { + s = proxyAuthServicerHeader{ + params: p.Params, + } + + return +} + +func NewProxyAuthQuery(p proxyAuthParams) (s proxyAuthServicerQuery, err error) { + s = proxyAuthServicerQuery{ + params: p.Params, + } + + return +} + +func NewProxyAuthBasic(p proxyAuthParams) (s proxyAuthServicerBasic, err error) { + var ( + ok bool + user, pass string + ) + + if user, ok = p.Params["username"].(string); !ok { + err = fmt.Errorf("invalid param username") + return + } + + if pass, ok = p.Params["password"].(string); !ok { + err = fmt.Errorf("invalid param password") + return + } + + s = proxyAuthServicerBasic{user: user, pass: pass} + + return +} + +func NewProxyAuthOauth2(p proxyAuthParams, c *http.Client, s SecureStorager) (ss proxyAuthServicerOauth2, err error) { + var ( + ok bool + client, secret, tokenUrl string + scope []string = []string{} + ) + + if client, ok = p.Params["client"].(string); !ok { + err = fmt.Errorf("invalid param client") + return + } + + if secret, ok = p.Params["secret"].(string); !ok { + err = fmt.Errorf("invalid param secret") + return + } + + if scopes, ok := p.Params["scope"].(string); ok { + scope = strings.Fields(scopes) + } + + if tokenUrl, ok = p.Params["token_url"].(string); !ok { + err = fmt.Errorf("invalid param token url") + return + } + + ss = proxyAuthServicerOauth2{ + c: c, + client: client, + secret: secret, + scope: scope, + tokenUrl: tokenUrl, + } + + return +} + +func NewProxyAuthJWT(p proxyAuthParams) (ss proxyAuthServicerJWT, err error) { + var ( + ok bool + jwt string + ) + + if jwt, ok = p.Params["jwt"].(string); !ok { + err = fmt.Errorf("invalid param jwt") + return + } + + ss = proxyAuthServicerJWT{ + JWT: jwt, + } + + return +} + +func NewProxyAuthServicer(c *http.Client, p proxyAuthParams, s SecureStorager) (ProxyAuthServicer, error) { + switch p.Type { + case proxyAuthTypeHeader: + return NewProxyAuthHeader(p) + case proxyAuthTypeQuery: + return NewProxyAuthQuery(p) + case proxyAuthTypeBasic: + return NewProxyAuthBasic(p) + case proxyAuthTypeOauth2: + return NewProxyAuthOauth2(p, c, s) + case proxyAuthTypeJWT: + return NewProxyAuthJWT(p) + default: + return proxyAuthServicerNoop{}, nil + } +} + +func (s proxyAuthServicerHeader) Do(r *http.Request) error { + for k, v := range s.params { + r.Header.Add(k, v.(string)) + } + return nil +} + +func (s proxyAuthServicerQuery) Do(r *http.Request) error { + if len(s.params) == 0 { + return nil + } + + q := r.URL.Query() + + for k, v := range s.params { + q.Set(k, v.(string)) + } + + r.URL.RawQuery = q.Encode() + + return nil +} + +func (s proxyAuthServicerBasic) Do(r *http.Request) error { + r.SetBasicAuth(s.user, s.pass) + return nil +} + +func (s proxyAuthServicerOauth2) Do(r *http.Request) error { + c := &clientcredentials.Config{ + ClientID: s.client, + ClientSecret: s.secret, + Scopes: s.scope, + TokenURL: s.tokenUrl, + } + + ctx := context.WithValue(context.Background(), oauth2.HTTPClient, s.c) + + t, err := c.Token(ctx) + + if err != nil { + return err + } + + // todo - store this to secure storager + r.Header.Set("Authorization", "Bearer "+t.AccessToken) + + return nil +} + +func (s proxyAuthServicerJWT) Do(r *http.Request) (err error) { + // set up the request and get the jwt + r.Header.Set("Authorization", "Bearer "+s.JWT) + return +} + +func (s proxyAuthServicerNoop) Do(r *http.Request) error { + return nil +} + +func (s proxyAuthServicerOauth2) Def() *proxyAuthDefinition { + return &proxyAuthDefinition{ + Type: proxyAuthTypeOauth2, + Params: []*proxyAuthMetaArg{ + { + Label: "client", + Type: "text", + Example: "", + }, + { + Label: "secret", + Type: "text", + Example: "", + }, + { + Label: "scope", + Type: "text", + Example: "Keep the scopes separated with spaces ie.: 'scope1 scope2'", + }, + { + Label: "token_url", + Type: "text", + Example: "", + }, + }, + } +} + +func (s proxyAuthServicerHeader) Def() *proxyAuthDefinition { + return &proxyAuthDefinition{ + Type: proxyAuthTypeHeader, + Params: []*proxyAuthMetaArg{ + { + Label: "header", + Type: "text", + Example: "", + }, + { + Label: "key", + Type: "text", + Example: "", + }, + }, + } +} + +func (s proxyAuthServicerBasic) Def() *proxyAuthDefinition { + return &proxyAuthDefinition{ + Type: proxyAuthTypeBasic, + Params: []*proxyAuthMetaArg{ + { + Label: "username", + Type: "text", + Example: "", + }, + { + Label: "password", + Type: "text", + Example: "", + }, + }, + } +} + +func (s proxyAuthServicerJWT) Def() *proxyAuthDefinition { + return &proxyAuthDefinition{ + Type: proxyAuthTypeJWT, + Params: []*proxyAuthMetaArg{ + { + Label: "jwt", + Type: "text", + Example: "", + }, + }, + } +} + +func ProxyAuthDef() []*proxyAuthDefinition { + return []*proxyAuthDefinition{ + proxyAuthServicerOauth2{}.Def(), + proxyAuthServicerHeader{}.Def(), + proxyAuthServicerBasic{}.Def(), + proxyAuthServicerJWT{}.Def(), + } +} diff --git a/pkg/apigw/auth_test.go b/pkg/apigw/proxy_auth_test.go similarity index 83% rename from pkg/apigw/auth_test.go rename to pkg/apigw/proxy_auth_test.go index 2c6313e7f..fa7be4750 100644 --- a/pkg/apigw/auth_test.go +++ b/pkg/apigw/proxy_auth_test.go @@ -21,7 +21,7 @@ func Test_authDo(t *testing.T) { name string err string errv string - params authParams + params proxyAuthParams exp http.Header } ) @@ -30,8 +30,8 @@ func Test_authDo(t *testing.T) { tcc = []tf{ { name: "auth header match headers", - params: authParams{ - Type: authTypeHeader, + params: proxyAuthParams{ + Type: proxyAuthTypeHeader, Params: map[string]interface{}{ "Client-Id": "123455", "Client_credentials": "pass1234", @@ -44,8 +44,8 @@ func Test_authDo(t *testing.T) { }, { name: "auth header match canonicalized headers", - params: authParams{ - Type: authTypeHeader, + params: proxyAuthParams{ + Type: proxyAuthTypeHeader, Params: map[string]interface{}{ "camelCaseHeader": "123455", }, @@ -56,8 +56,8 @@ func Test_authDo(t *testing.T) { }, { name: "auth basic match headers", - params: authParams{ - Type: authTypeBasic, + params: proxyAuthParams{ + Type: proxyAuthTypeBasic, Params: map[string]interface{}{ "username": "user", "password": "pass1234", @@ -67,8 +67,8 @@ func Test_authDo(t *testing.T) { }, { name: "auth basic match headers fail user validation", - params: authParams{ - Type: authTypeBasic, + params: proxyAuthParams{ + Type: proxyAuthTypeBasic, Params: map[string]interface{}{"password": "pass1234"}, }, exp: http.Header{}, @@ -76,8 +76,8 @@ func Test_authDo(t *testing.T) { }, { name: "auth basic match headers fail pass validation", - params: authParams{ - Type: authTypeBasic, + params: proxyAuthParams{ + Type: proxyAuthTypeBasic, Params: map[string]interface{}{"username": "user"}, }, exp: http.Header{}, @@ -85,7 +85,7 @@ func Test_authDo(t *testing.T) { }, { name: "noop default fallback", - params: authParams{}, + params: proxyAuthParams{}, exp: http.Header{}, }, } @@ -102,7 +102,7 @@ func Test_authDo(t *testing.T) { rq, _ := http.NewRequest("POST", "/foo", http.NoBody) - authServicer, err := NewAuthServicer(c, tc.params) + authServicer, err := NewProxyAuthServicer(c, tc.params, secureStorageTodo{}) if tc.errv != "" { req.EqualError(err, tc.errv) diff --git a/pkg/apigw/registry.go b/pkg/apigw/registry.go index 734cb3b8e..04568acac 100644 --- a/pkg/apigw/registry.go +++ b/pkg/apigw/registry.go @@ -11,6 +11,8 @@ type ( registry struct { h map[string]Handler } + + secureStorageTodo struct{} ) func NewRegistry() *registry { @@ -56,6 +58,6 @@ func (r *registry) Preload() { r.Add("validatorHeader", NewValidatorHeader()) r.Add("expediterRedirection", NewExpediterRedirection()) r.Add("processerWorkflow", NewProcesserWorkflow(NewWorkflow())) - r.Add("processerProxy", NewProcesserProxy(service.DefaultLogger, http.DefaultClient)) + r.Add("processerProxy", NewProcesserProxy(service.DefaultLogger, http.DefaultClient, secureStorageTodo{})) r.Add("processerPayload", NewProcesserPayload(service.DefaultLogger)) } diff --git a/system/rest.yaml b/system/rest.yaml index fb6bd5b09..c3dad9a1e 100644 --- a/system/rest.yaml +++ b/system/rest.yaml @@ -1635,10 +1635,14 @@ endpoints: title: Undelete function path: "/{functionID}/undelete" parameters: { path: [ { name: functionID, type: uint64, required: true, title: "Function ID" } ] } - - name: definitions + - name: defFunction method: GET title: Function definitions path: "/def" parameters: get: - { name: kind, type: "string", title: "Filter functions by kind" } + - name: defProxyAuth + method: GET + title: Proxu auth definitions + path: "/proxy_auth/def" diff --git a/system/rest/apigw_function.go b/system/rest/apigw_function.go index a3a4946dc..fbfd0701c 100644 --- a/system/rest/apigw_function.go +++ b/system/rest/apigw_function.go @@ -32,7 +32,9 @@ type ( Update(ctx context.Context, upd *types.ApigwFunction) (*types.ApigwFunction, error) DeleteByID(ctx context.Context, ID uint64) error UndeleteByID(ctx context.Context, ID uint64) error - Definitions(context.Context, string) (interface{}, error) + + DefFunction(context.Context, string) (interface{}, error) + DefProxyAuth(context.Context) (interface{}, error) } ) @@ -108,8 +110,12 @@ func (ctrl *ApigwFunction) Delete(ctx context.Context, r *request.ApigwFunctionD return api.OK(), ctrl.svc.DeleteByID(ctx, r.FunctionID) } -func (ctrl *ApigwFunction) Definitions(ctx context.Context, r *request.ApigwFunctionDefinitions) (interface{}, error) { - return ctrl.svc.Definitions(ctx, r.Kind) +func (ctrl *ApigwFunction) DefFunction(ctx context.Context, r *request.ApigwFunctionDefFunction) (interface{}, error) { + return ctrl.svc.DefFunction(ctx, r.Kind) +} + +func (ctrl *ApigwFunction) DefProxyAuth(ctx context.Context, r *request.ApigwFunctionDefProxyAuth) (interface{}, error) { + return ctrl.svc.DefProxyAuth(ctx) } func (ctrl *ApigwFunction) Undelete(ctx context.Context, r *request.ApigwFunctionUndelete) (interface{}, error) { diff --git a/system/rest/handlers/apigwFunction.go b/system/rest/handlers/apigwFunction.go index fc583c45c..3627159ec 100644 --- a/system/rest/handlers/apigwFunction.go +++ b/system/rest/handlers/apigwFunction.go @@ -25,18 +25,20 @@ type ( Read(context.Context, *request.ApigwFunctionRead) (interface{}, error) Delete(context.Context, *request.ApigwFunctionDelete) (interface{}, error) Undelete(context.Context, *request.ApigwFunctionUndelete) (interface{}, error) - Definitions(context.Context, *request.ApigwFunctionDefinitions) (interface{}, error) + DefFunction(context.Context, *request.ApigwFunctionDefFunction) (interface{}, error) + DefProxyAuth(context.Context, *request.ApigwFunctionDefProxyAuth) (interface{}, error) } // HTTP API interface ApigwFunction struct { - List func(http.ResponseWriter, *http.Request) - Create func(http.ResponseWriter, *http.Request) - Update func(http.ResponseWriter, *http.Request) - Read func(http.ResponseWriter, *http.Request) - Delete func(http.ResponseWriter, *http.Request) - Undelete func(http.ResponseWriter, *http.Request) - Definitions func(http.ResponseWriter, *http.Request) + List func(http.ResponseWriter, *http.Request) + Create func(http.ResponseWriter, *http.Request) + Update func(http.ResponseWriter, *http.Request) + Read func(http.ResponseWriter, *http.Request) + Delete func(http.ResponseWriter, *http.Request) + Undelete func(http.ResponseWriter, *http.Request) + DefFunction func(http.ResponseWriter, *http.Request) + DefProxyAuth func(http.ResponseWriter, *http.Request) } ) @@ -138,15 +140,31 @@ func NewApigwFunction(h ApigwFunctionAPI) *ApigwFunction { api.Send(w, r, value) }, - Definitions: func(w http.ResponseWriter, r *http.Request) { + DefFunction: func(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() - params := request.NewApigwFunctionDefinitions() + params := request.NewApigwFunctionDefFunction() if err := params.Fill(r); err != nil { api.Send(w, r, err) return } - value, err := h.Definitions(r.Context(), params) + value, err := h.DefFunction(r.Context(), params) + if err != nil { + api.Send(w, r, err) + return + } + + api.Send(w, r, value) + }, + DefProxyAuth: func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + params := request.NewApigwFunctionDefProxyAuth() + if err := params.Fill(r); err != nil { + api.Send(w, r, err) + return + } + + value, err := h.DefProxyAuth(r.Context(), params) if err != nil { api.Send(w, r, err) return @@ -166,6 +184,7 @@ func (h ApigwFunction) MountRoutes(r chi.Router, middlewares ...func(http.Handle r.Get("/apigw/function/{functionID}", h.Read) r.Delete("/apigw/function/{functionID}", h.Delete) r.Post("/apigw/function/{functionID}/undelete", h.Undelete) - r.Get("/apigw/function/def", h.Definitions) + r.Get("/apigw/function/def", h.DefFunction) + r.Get("/apigw/function/proxy_auth/def", h.DefProxyAuth) }) } diff --git a/system/rest/request/apigwFunction.go b/system/rest/request/apigwFunction.go index 40d697951..c0754374c 100644 --- a/system/rest/request/apigwFunction.go +++ b/system/rest/request/apigwFunction.go @@ -151,12 +151,15 @@ type ( FunctionID uint64 `json:",string"` } - ApigwFunctionDefinitions struct { + ApigwFunctionDefFunction struct { // Kind GET parameter // // Filter functions by kind Kind string } + + ApigwFunctionDefProxyAuth struct { + } ) // NewApigwFunctionList request @@ -601,25 +604,25 @@ func (r *ApigwFunctionUndelete) Fill(req *http.Request) (err error) { return err } -// NewApigwFunctionDefinitions request -func NewApigwFunctionDefinitions() *ApigwFunctionDefinitions { - return &ApigwFunctionDefinitions{} +// NewApigwFunctionDefFunction request +func NewApigwFunctionDefFunction() *ApigwFunctionDefFunction { + return &ApigwFunctionDefFunction{} } // Auditable returns all auditable/loggable parameters -func (r ApigwFunctionDefinitions) Auditable() map[string]interface{} { +func (r ApigwFunctionDefFunction) Auditable() map[string]interface{} { return map[string]interface{}{ "kind": r.Kind, } } // Auditable returns all auditable/loggable parameters -func (r ApigwFunctionDefinitions) GetKind() string { +func (r ApigwFunctionDefFunction) GetKind() string { return r.Kind } // Fill processes request and fills internal variables -func (r *ApigwFunctionDefinitions) Fill(req *http.Request) (err error) { +func (r *ApigwFunctionDefFunction) Fill(req *http.Request) (err error) { { // GET params @@ -635,3 +638,19 @@ func (r *ApigwFunctionDefinitions) Fill(req *http.Request) (err error) { return err } + +// NewApigwFunctionDefProxyAuth request +func NewApigwFunctionDefProxyAuth() *ApigwFunctionDefProxyAuth { + return &ApigwFunctionDefProxyAuth{} +} + +// Auditable returns all auditable/loggable parameters +func (r ApigwFunctionDefProxyAuth) Auditable() map[string]interface{} { + return map[string]interface{}{} +} + +// Fill processes request and fills internal variables +func (r *ApigwFunctionDefProxyAuth) Fill(req *http.Request) (err error) { + + return err +} diff --git a/system/service/apigw_function.go b/system/service/apigw_function.go index b50195394..f04e7ad95 100644 --- a/system/service/apigw_function.go +++ b/system/service/apigw_function.go @@ -228,7 +228,7 @@ func (svc *apigwFunction) Search(ctx context.Context, filter types.ApigwFunction return r, f, svc.recordAction(ctx, aProps, ApigwFunctionActionSearch, err) } -func (svc *apigwFunction) Definitions(ctx context.Context, kind string) (l interface{}, err error) { +func (svc *apigwFunction) DefFunction(ctx context.Context, kind string) (l interface{}, err error) { var ( qProps = &apigwFunctionActionProps{} ) @@ -247,3 +247,23 @@ func (svc *apigwFunction) Definitions(ctx context.Context, kind string) (l inter return l, svc.recordAction(ctx, qProps, ApigwFunctionActionSearch, err) } + +func (svc *apigwFunction) DefProxyAuth(ctx context.Context) (l interface{}, err error) { + var ( + qProps = &apigwFunctionActionProps{} + ) + + err = func() error { + if !svc.ac.CanSearchApiGwFunctions(ctx) { + return ApigwFunctionErrNotAllowedToRead(qProps) + } + + // get the definitions from registry + l = apigw.Service().ProxyAuthDef() + + return nil + }() + + return l, svc.recordAction(ctx, qProps, ApigwFunctionActionSearch, err) + +} diff --git a/vendor/golang.org/x/oauth2/README.md b/vendor/golang.org/x/oauth2/README.md index 8cfd6063e..1473e1296 100644 --- a/vendor/golang.org/x/oauth2/README.md +++ b/vendor/golang.org/x/oauth2/README.md @@ -1,7 +1,7 @@ # OAuth2 for Go +[![Go Reference](https://pkg.go.dev/badge/golang.org/x/oauth2.svg)](https://pkg.go.dev/golang.org/x/oauth2) [![Build Status](https://travis-ci.org/golang/oauth2.svg?branch=master)](https://travis-ci.org/golang/oauth2) -[![GoDoc](https://godoc.org/golang.org/x/oauth2?status.svg)](https://godoc.org/golang.org/x/oauth2) oauth2 package contains a client implementation for OAuth 2.0 spec. @@ -14,17 +14,17 @@ go get golang.org/x/oauth2 Or you can manually git clone the repository to `$(go env GOPATH)/src/golang.org/x/oauth2`. -See godoc for further documentation and examples. +See pkg.go.dev for further documentation and examples. -* [godoc.org/golang.org/x/oauth2](https://godoc.org/golang.org/x/oauth2) -* [godoc.org/golang.org/x/oauth2/google](https://godoc.org/golang.org/x/oauth2/google) +* [pkg.go.dev/golang.org/x/oauth2](https://pkg.go.dev/golang.org/x/oauth2) +* [pkg.go.dev/golang.org/x/oauth2/google](https://pkg.go.dev/golang.org/x/oauth2/google) ## Policy for new packages We no longer accept new provider-specific packages in this repo if all they do is add a single endpoint variable. If you just want to add a single endpoint, add it to the -[godoc.org/golang.org/x/oauth2/endpoints](https://godoc.org/golang.org/x/oauth2/endpoints) +[pkg.go.dev/golang.org/x/oauth2/endpoints](https://pkg.go.dev/golang.org/x/oauth2/endpoints) package. ## Report Issues / Send Patches diff --git a/vendor/golang.org/x/oauth2/authhandler/authhandler.go b/vendor/golang.org/x/oauth2/authhandler/authhandler.go new file mode 100644 index 000000000..69967cf87 --- /dev/null +++ b/vendor/golang.org/x/oauth2/authhandler/authhandler.go @@ -0,0 +1,56 @@ +// Copyright 2021 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package authhandler implements a TokenSource to support +// "three-legged OAuth 2.0" via a custom AuthorizationHandler. +package authhandler + +import ( + "context" + "errors" + + "golang.org/x/oauth2" +) + +// AuthorizationHandler is a 3-legged-OAuth helper that prompts +// the user for OAuth consent at the specified auth code URL +// and returns an auth code and state upon approval. +type AuthorizationHandler func(authCodeURL string) (code string, state string, err error) + +// TokenSource returns an oauth2.TokenSource that fetches access tokens +// using 3-legged-OAuth flow. +// +// The provided context.Context is used for oauth2 Exchange operation. +// +// The provided oauth2.Config should be a full configuration containing AuthURL, +// TokenURL, and Scope. +// +// An environment-specific AuthorizationHandler is used to obtain user consent. +// +// Per the OAuth protocol, a unique "state" string should be specified here. +// This token source will verify that the "state" is identical in the request +// and response before exchanging the auth code for OAuth token to prevent CSRF +// attacks. +func TokenSource(ctx context.Context, config *oauth2.Config, state string, authHandler AuthorizationHandler) oauth2.TokenSource { + return oauth2.ReuseTokenSource(nil, authHandlerSource{config: config, ctx: ctx, authHandler: authHandler, state: state}) +} + +type authHandlerSource struct { + ctx context.Context + config *oauth2.Config + authHandler AuthorizationHandler + state string +} + +func (source authHandlerSource) Token() (*oauth2.Token, error) { + url := source.config.AuthCodeURL(source.state) + code, state, err := source.authHandler(url) + if err != nil { + return nil, err + } + if state != source.state { + return nil, errors.New("state mismatch in 3-legged-OAuth flow") + } + return source.config.Exchange(source.ctx, code) +} diff --git a/vendor/golang.org/x/oauth2/clientcredentials/clientcredentials.go b/vendor/golang.org/x/oauth2/clientcredentials/clientcredentials.go new file mode 100644 index 000000000..7a0b9ed10 --- /dev/null +++ b/vendor/golang.org/x/oauth2/clientcredentials/clientcredentials.go @@ -0,0 +1,120 @@ +// Copyright 2014 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package clientcredentials implements the OAuth2.0 "client credentials" token flow, +// also known as the "two-legged OAuth 2.0". +// +// This should be used when the client is acting on its own behalf or when the client +// is the resource owner. It may also be used when requesting access to protected +// resources based on an authorization previously arranged with the authorization +// server. +// +// See https://tools.ietf.org/html/rfc6749#section-4.4 +package clientcredentials // import "golang.org/x/oauth2/clientcredentials" + +import ( + "context" + "fmt" + "net/http" + "net/url" + "strings" + + "golang.org/x/oauth2" + "golang.org/x/oauth2/internal" +) + +// Config describes a 2-legged OAuth2 flow, with both the +// client application information and the server's endpoint URLs. +type Config struct { + // ClientID is the application's ID. + ClientID string + + // ClientSecret is the application's secret. + ClientSecret string + + // TokenURL is the resource server's token endpoint + // URL. This is a constant specific to each server. + TokenURL string + + // Scope specifies optional requested permissions. + Scopes []string + + // EndpointParams specifies additional parameters for requests to the token endpoint. + EndpointParams url.Values + + // AuthStyle optionally specifies how the endpoint wants the + // client ID & client secret sent. The zero value means to + // auto-detect. + AuthStyle oauth2.AuthStyle +} + +// Token uses client credentials to retrieve a token. +// +// The provided context optionally controls which HTTP client is used. See the oauth2.HTTPClient variable. +func (c *Config) Token(ctx context.Context) (*oauth2.Token, error) { + return c.TokenSource(ctx).Token() +} + +// Client returns an HTTP client using the provided token. +// The token will auto-refresh as necessary. +// +// The provided context optionally controls which HTTP client +// is returned. See the oauth2.HTTPClient variable. +// +// The returned Client and its Transport should not be modified. +func (c *Config) Client(ctx context.Context) *http.Client { + return oauth2.NewClient(ctx, c.TokenSource(ctx)) +} + +// TokenSource returns a TokenSource that returns t until t expires, +// automatically refreshing it as necessary using the provided context and the +// client ID and client secret. +// +// Most users will use Config.Client instead. +func (c *Config) TokenSource(ctx context.Context) oauth2.TokenSource { + source := &tokenSource{ + ctx: ctx, + conf: c, + } + return oauth2.ReuseTokenSource(nil, source) +} + +type tokenSource struct { + ctx context.Context + conf *Config +} + +// Token refreshes the token by using a new client credentials request. +// tokens received this way do not include a refresh token +func (c *tokenSource) Token() (*oauth2.Token, error) { + v := url.Values{ + "grant_type": {"client_credentials"}, + } + if len(c.conf.Scopes) > 0 { + v.Set("scope", strings.Join(c.conf.Scopes, " ")) + } + for k, p := range c.conf.EndpointParams { + // Allow grant_type to be overridden to allow interoperability with + // non-compliant implementations. + if _, ok := v[k]; ok && k != "grant_type" { + return nil, fmt.Errorf("oauth2: cannot overwrite parameter %q", k) + } + v[k] = p + } + + tk, err := internal.RetrieveToken(c.ctx, c.conf.ClientID, c.conf.ClientSecret, c.conf.TokenURL, v, internal.AuthStyle(c.conf.AuthStyle)) + if err != nil { + if rErr, ok := err.(*internal.RetrieveError); ok { + return nil, (*oauth2.RetrieveError)(rErr) + } + return nil, err + } + t := &oauth2.Token{ + AccessToken: tk.AccessToken, + TokenType: tk.TokenType, + RefreshToken: tk.RefreshToken, + Expiry: tk.Expiry, + } + return t.WithExtra(tk.Raw), nil +} diff --git a/vendor/golang.org/x/oauth2/google/appengine_gen1.go b/vendor/golang.org/x/oauth2/google/appengine_gen1.go index 83dacac32..16c6c6b90 100644 --- a/vendor/golang.org/x/oauth2/google/appengine_gen1.go +++ b/vendor/golang.org/x/oauth2/google/appengine_gen1.go @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +//go:build appengine // +build appengine // This file applies to App Engine first generation runtimes (<= Go 1.9). diff --git a/vendor/golang.org/x/oauth2/google/appengine_gen2_flex.go b/vendor/golang.org/x/oauth2/google/appengine_gen2_flex.go index 04c2c2216..a7e27b3d2 100644 --- a/vendor/golang.org/x/oauth2/google/appengine_gen2_flex.go +++ b/vendor/golang.org/x/oauth2/google/appengine_gen2_flex.go @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +//go:build !appengine // +build !appengine // This file applies to App Engine second generation runtimes (>= Go 1.11) and App Engine flexible. diff --git a/vendor/golang.org/x/oauth2/google/default.go b/vendor/golang.org/x/oauth2/google/default.go index ad2c09236..880dd7b59 100644 --- a/vendor/golang.org/x/oauth2/google/default.go +++ b/vendor/golang.org/x/oauth2/google/default.go @@ -16,11 +16,16 @@ import ( "cloud.google.com/go/compute/metadata" "golang.org/x/oauth2" + "golang.org/x/oauth2/authhandler" ) // Credentials holds Google credentials, including "Application Default Credentials". // For more details, see: // https://developers.google.com/accounts/docs/application-default-credentials +// Credentials from external accounts (workload identity federation) are used to +// identify a particular application from an on-prem or non-Google Cloud platform +// including Amazon Web Services (AWS), Microsoft Azure or any identity provider +// that supports OpenID Connect (OIDC). type Credentials struct { ProjectID string // may be empty TokenSource oauth2.TokenSource @@ -37,6 +42,32 @@ type Credentials struct { // Deprecated: use Credentials instead. type DefaultCredentials = Credentials +// CredentialsParams holds user supplied parameters that are used together +// with a credentials file for building a Credentials object. +type CredentialsParams struct { + // Scopes is the list OAuth scopes. Required. + // Example: https://www.googleapis.com/auth/cloud-platform + Scopes []string + + // Subject is the user email used for domain wide delegation (see + // https://developers.google.com/identity/protocols/oauth2/service-account#delegatingauthority). + // Optional. + Subject string + + // AuthHandler is the AuthorizationHandler used for 3-legged OAuth flow. Optional. + AuthHandler authhandler.AuthorizationHandler + + // State is a unique string used with AuthHandler. Optional. + State string +} + +func (params CredentialsParams) deepCopy() CredentialsParams { + paramsCopy := params + paramsCopy.Scopes = make([]string, len(params.Scopes)) + copy(paramsCopy.Scopes, params.Scopes) + return paramsCopy +} + // DefaultClient returns an HTTP Client that uses the // DefaultTokenSource to obtain authentication credentials. func DefaultClient(ctx context.Context, scope ...string) (*http.Client, error) { @@ -58,13 +89,17 @@ func DefaultTokenSource(ctx context.Context, scope ...string) (oauth2.TokenSourc return creds.TokenSource, nil } -// FindDefaultCredentials searches for "Application Default Credentials". +// FindDefaultCredentialsWithParams searches for "Application Default Credentials". // // It looks for credentials in the following places, // preferring the first location found: // // 1. A JSON file whose path is specified by the // GOOGLE_APPLICATION_CREDENTIALS environment variable. +// For workload identity federation, refer to +// https://cloud.google.com/iam/docs/how-to#using-workload-identity-federation on +// how to generate the JSON configuration file for on-prem/non-Google cloud +// platforms. // 2. A JSON file in a location known to the gcloud command-line tool. // On Windows, this is %APPDATA%/gcloud/application_default_credentials.json. // On other systems, $HOME/.config/gcloud/application_default_credentials.json. @@ -73,11 +108,14 @@ func DefaultTokenSource(ctx context.Context, scope ...string) (oauth2.TokenSourc // 4. On Google Compute Engine, Google App Engine standard second generation runtimes // (>= Go 1.11), and Google App Engine flexible environment, it fetches // credentials from the metadata server. -func FindDefaultCredentials(ctx context.Context, scopes ...string) (*Credentials, error) { +func FindDefaultCredentialsWithParams(ctx context.Context, params CredentialsParams) (*Credentials, error) { + // Make defensive copy of the slices in params. + params = params.deepCopy() + // First, try the environment variable. const envVar = "GOOGLE_APPLICATION_CREDENTIALS" if filename := os.Getenv(envVar); filename != "" { - creds, err := readCredentialsFile(ctx, filename, scopes) + creds, err := readCredentialsFile(ctx, filename, params) if err != nil { return nil, fmt.Errorf("google: error getting credentials using %v environment variable: %v", envVar, err) } @@ -86,7 +124,7 @@ func FindDefaultCredentials(ctx context.Context, scopes ...string) (*Credentials // Second, try a well-known file. filename := wellKnownFile() - if creds, err := readCredentialsFile(ctx, filename, scopes); err == nil { + if creds, err := readCredentialsFile(ctx, filename, params); err == nil { return creds, nil } else if !os.IsNotExist(err) { return nil, fmt.Errorf("google: error getting credentials using well-known file (%v): %v", filename, err) @@ -98,7 +136,7 @@ func FindDefaultCredentials(ctx context.Context, scopes ...string) (*Credentials if appengineTokenFunc != nil { return &DefaultCredentials{ ProjectID: appengineAppIDFunc(ctx), - TokenSource: AppEngineTokenSource(ctx, scopes...), + TokenSource: AppEngineTokenSource(ctx, params.Scopes...), }, nil } @@ -108,7 +146,7 @@ func FindDefaultCredentials(ctx context.Context, scopes ...string) (*Credentials id, _ := metadata.ProjectID() return &DefaultCredentials{ ProjectID: id, - TokenSource: ComputeTokenSource("", scopes...), + TokenSource: ComputeTokenSource("", params.Scopes...), }, nil } @@ -117,16 +155,38 @@ func FindDefaultCredentials(ctx context.Context, scopes ...string) (*Credentials return nil, fmt.Errorf("google: could not find default credentials. See %v for more information.", url) } -// CredentialsFromJSON obtains Google credentials from a JSON value. The JSON can -// represent either a Google Developers Console client_credentials.json file (as in -// ConfigFromJSON) or a Google Developers service account key file (as in -// JWTConfigFromJSON). -func CredentialsFromJSON(ctx context.Context, jsonData []byte, scopes ...string) (*Credentials, error) { +// FindDefaultCredentials invokes FindDefaultCredentialsWithParams with the specified scopes. +func FindDefaultCredentials(ctx context.Context, scopes ...string) (*Credentials, error) { + var params CredentialsParams + params.Scopes = scopes + return FindDefaultCredentialsWithParams(ctx, params) +} + +// CredentialsFromJSONWithParams obtains Google credentials from a JSON value. The JSON can +// represent either a Google Developers Console client_credentials.json file (as in ConfigFromJSON), +// a Google Developers service account key file, a gcloud user credentials file (a.k.a. refresh +// token JSON), or the JSON configuration file for workload identity federation in non-Google cloud +// platforms (see https://cloud.google.com/iam/docs/how-to#using-workload-identity-federation). +func CredentialsFromJSONWithParams(ctx context.Context, jsonData []byte, params CredentialsParams) (*Credentials, error) { + // Make defensive copy of the slices in params. + params = params.deepCopy() + + // First, attempt to parse jsonData as a Google Developers Console client_credentials.json. + config, _ := ConfigFromJSON(jsonData, params.Scopes...) + if config != nil { + return &Credentials{ + ProjectID: "", + TokenSource: authhandler.TokenSource(ctx, config, params.State, params.AuthHandler), + JSON: jsonData, + }, nil + } + + // Otherwise, parse jsonData as one of the other supported credentials files. var f credentialsFile if err := json.Unmarshal(jsonData, &f); err != nil { return nil, err } - ts, err := f.tokenSource(ctx, append([]string(nil), scopes...)) + ts, err := f.tokenSource(ctx, params) if err != nil { return nil, err } @@ -137,6 +197,13 @@ func CredentialsFromJSON(ctx context.Context, jsonData []byte, scopes ...string) }, nil } +// CredentialsFromJSON invokes CredentialsFromJSONWithParams with the specified scopes. +func CredentialsFromJSON(ctx context.Context, jsonData []byte, scopes ...string) (*Credentials, error) { + var params CredentialsParams + params.Scopes = scopes + return CredentialsFromJSONWithParams(ctx, jsonData, params) +} + func wellKnownFile() string { const f = "application_default_credentials.json" if runtime.GOOS == "windows" { @@ -145,10 +212,10 @@ func wellKnownFile() string { return filepath.Join(guessUnixHomeDir(), ".config", "gcloud", f) } -func readCredentialsFile(ctx context.Context, filename string, scopes []string) (*DefaultCredentials, error) { +func readCredentialsFile(ctx context.Context, filename string, params CredentialsParams) (*DefaultCredentials, error) { b, err := ioutil.ReadFile(filename) if err != nil { return nil, err } - return CredentialsFromJSON(ctx, b, scopes...) + return CredentialsFromJSONWithParams(ctx, b, params) } diff --git a/vendor/golang.org/x/oauth2/google/doc.go b/vendor/golang.org/x/oauth2/google/doc.go index 73be62903..8e6a57ce9 100644 --- a/vendor/golang.org/x/oauth2/google/doc.go +++ b/vendor/golang.org/x/oauth2/google/doc.go @@ -4,13 +4,16 @@ // Package google provides support for making OAuth2 authorized and authenticated // HTTP requests to Google APIs. It supports the Web server flow, client-side -// credentials, service accounts, Google Compute Engine service accounts, and Google -// App Engine service accounts. +// credentials, service accounts, Google Compute Engine service accounts, +// Google App Engine service accounts and workload identity federation +// from non-Google cloud platforms. // // A brief overview of the package follows. For more information, please read // https://developers.google.com/accounts/docs/OAuth2 // and // https://developers.google.com/accounts/docs/application-default-credentials. +// For more information on using workload identity federation, refer to +// https://cloud.google.com/iam/docs/how-to#using-workload-identity-federation. // // OAuth2 Configs // @@ -19,6 +22,35 @@ // the other by JWTConfigFromJSON. The returned Config can be used to obtain a TokenSource or // create an http.Client. // +// Workload Identity Federation +// +// Using workload identity federation, your application can access Google Cloud +// resources from Amazon Web Services (AWS), Microsoft Azure or any identity +// provider that supports OpenID Connect (OIDC). +// Traditionally, applications running outside Google Cloud have used service +// account keys to access Google Cloud resources. Using identity federation, +// you can allow your workload to impersonate a service account. +// This lets you access Google Cloud resources directly, eliminating the +// maintenance and security burden associated with service account keys. +// +// Follow the detailed instructions on how to configure Workload Identity Federation +// in various platforms: +// +// Amazon Web Services (AWS): https://cloud.google.com/iam/docs/access-resources-aws +// Microsoft Azure: https://cloud.google.com/iam/docs/access-resources-azure +// OIDC identity provider: https://cloud.google.com/iam/docs/access-resources-oidc +// +// For OIDC providers, the library can retrieve OIDC tokens either from a +// local file location (file-sourced credentials) or from a local server +// (URL-sourced credentials). +// For file-sourced credentials, a background process needs to be continuously +// refreshing the file location with a new OIDC token prior to expiration. +// For tokens with one hour lifetimes, the token needs to be updated in the file +// every hour. The token can be stored directly as plain text or in JSON format. +// For URL-sourced credentials, a local server needs to host a GET endpoint to +// return the OIDC token. The response can be in plain text or JSON. +// Additional required request headers can also be specified. +// // // Credentials // @@ -29,6 +61,13 @@ // FindDefaultCredentials looks in some well-known places for a credentials file, and // will call AppEngineTokenSource or ComputeTokenSource as needed. // +// Application Default Credentials also support workload identity federation to +// access Google Cloud resources from non-Google Cloud platforms including Amazon +// Web Services (AWS), Microsoft Azure or any identity provider that supports +// OpenID Connect (OIDC). Workload identity federation is recommended for +// non-Google Cloud environments as it avoids the need to download, manage and +// store service account private keys locally. +// // DefaultClient and DefaultTokenSource are convenience methods. They first call FindDefaultCredentials, // then use the credentials to construct an http.Client or an oauth2.TokenSource. // diff --git a/vendor/golang.org/x/oauth2/google/google.go b/vendor/golang.org/x/oauth2/google/google.go index 81de32b36..2b631f522 100644 --- a/vendor/golang.org/x/oauth2/google/google.go +++ b/vendor/golang.org/x/oauth2/google/google.go @@ -15,10 +15,11 @@ import ( "cloud.google.com/go/compute/metadata" "golang.org/x/oauth2" + "golang.org/x/oauth2/google/internal/externalaccount" "golang.org/x/oauth2/jwt" ) -// Endpoint is Google's OAuth 2.0 endpoint. +// Endpoint is Google's OAuth 2.0 default endpoint. var Endpoint = oauth2.Endpoint{ AuthURL: "https://accounts.google.com/o/oauth2/auth", TokenURL: "https://oauth2.googleapis.com/token", @@ -86,23 +87,25 @@ func JWTConfigFromJSON(jsonKey []byte, scope ...string) (*jwt.Config, error) { return nil, fmt.Errorf("google: read JWT from JSON credentials: 'type' field is %q (expected %q)", f.Type, serviceAccountKey) } scope = append([]string(nil), scope...) // copy - return f.jwtConfig(scope), nil + return f.jwtConfig(scope, ""), nil } // JSON key file types. const ( serviceAccountKey = "service_account" userCredentialsKey = "authorized_user" + externalAccountKey = "external_account" ) // credentialsFile is the unmarshalled representation of a credentials file. type credentialsFile struct { - Type string `json:"type"` // serviceAccountKey or userCredentialsKey + Type string `json:"type"` // Service Account fields ClientEmail string `json:"client_email"` PrivateKeyID string `json:"private_key_id"` PrivateKey string `json:"private_key"` + AuthURL string `json:"auth_uri"` TokenURL string `json:"token_uri"` ProjectID string `json:"project_id"` @@ -111,15 +114,25 @@ type credentialsFile struct { ClientSecret string `json:"client_secret"` ClientID string `json:"client_id"` RefreshToken string `json:"refresh_token"` + + // External Account fields + Audience string `json:"audience"` + SubjectTokenType string `json:"subject_token_type"` + TokenURLExternal string `json:"token_url"` + TokenInfoURL string `json:"token_info_url"` + ServiceAccountImpersonationURL string `json:"service_account_impersonation_url"` + CredentialSource externalaccount.CredentialSource `json:"credential_source"` + QuotaProjectID string `json:"quota_project_id"` } -func (f *credentialsFile) jwtConfig(scopes []string) *jwt.Config { +func (f *credentialsFile) jwtConfig(scopes []string, subject string) *jwt.Config { cfg := &jwt.Config{ Email: f.ClientEmail, PrivateKey: []byte(f.PrivateKey), PrivateKeyID: f.PrivateKeyID, Scopes: scopes, TokenURL: f.TokenURL, + Subject: subject, // This is the user email to impersonate } if cfg.TokenURL == "" { cfg.TokenURL = JWTTokenURL @@ -127,20 +140,44 @@ func (f *credentialsFile) jwtConfig(scopes []string) *jwt.Config { return cfg } -func (f *credentialsFile) tokenSource(ctx context.Context, scopes []string) (oauth2.TokenSource, error) { +func (f *credentialsFile) tokenSource(ctx context.Context, params CredentialsParams) (oauth2.TokenSource, error) { switch f.Type { case serviceAccountKey: - cfg := f.jwtConfig(scopes) + cfg := f.jwtConfig(params.Scopes, params.Subject) return cfg.TokenSource(ctx), nil case userCredentialsKey: cfg := &oauth2.Config{ ClientID: f.ClientID, ClientSecret: f.ClientSecret, - Scopes: scopes, - Endpoint: Endpoint, + Scopes: params.Scopes, + Endpoint: oauth2.Endpoint{ + AuthURL: f.AuthURL, + TokenURL: f.TokenURL, + AuthStyle: oauth2.AuthStyleInParams, + }, + } + if cfg.Endpoint.AuthURL == "" { + cfg.Endpoint.AuthURL = Endpoint.AuthURL + } + if cfg.Endpoint.TokenURL == "" { + cfg.Endpoint.TokenURL = Endpoint.TokenURL } tok := &oauth2.Token{RefreshToken: f.RefreshToken} return cfg.TokenSource(ctx, tok), nil + case externalAccountKey: + cfg := &externalaccount.Config{ + Audience: f.Audience, + SubjectTokenType: f.SubjectTokenType, + TokenURL: f.TokenURLExternal, + TokenInfoURL: f.TokenInfoURL, + ServiceAccountImpersonationURL: f.ServiceAccountImpersonationURL, + ClientSecret: f.ClientSecret, + ClientID: f.ClientID, + CredentialSource: f.CredentialSource, + QuotaProjectID: f.QuotaProjectID, + Scopes: params.Scopes, + } + return cfg.TokenSource(ctx), nil case "": return nil, errors.New("missing 'type' field in credentials") default: diff --git a/vendor/golang.org/x/oauth2/google/internal/externalaccount/aws.go b/vendor/golang.org/x/oauth2/google/internal/externalaccount/aws.go new file mode 100644 index 000000000..a5a5423c6 --- /dev/null +++ b/vendor/golang.org/x/oauth2/google/internal/externalaccount/aws.go @@ -0,0 +1,470 @@ +// Copyright 2021 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package externalaccount + +import ( + "bytes" + "context" + "crypto/hmac" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/url" + "os" + "path" + "sort" + "strings" + "time" + + "golang.org/x/oauth2" +) + +type awsSecurityCredentials struct { + AccessKeyID string `json:"AccessKeyID"` + SecretAccessKey string `json:"SecretAccessKey"` + SecurityToken string `json:"Token"` +} + +// awsRequestSigner is a utility class to sign http requests using a AWS V4 signature. +type awsRequestSigner struct { + RegionName string + AwsSecurityCredentials awsSecurityCredentials +} + +// getenv aliases os.Getenv for testing +var getenv = os.Getenv + +const ( + // AWS Signature Version 4 signing algorithm identifier. + awsAlgorithm = "AWS4-HMAC-SHA256" + + // The termination string for the AWS credential scope value as defined in + // https://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html + awsRequestType = "aws4_request" + + // The AWS authorization header name for the security session token if available. + awsSecurityTokenHeader = "x-amz-security-token" + + // The AWS authorization header name for the auto-generated date. + awsDateHeader = "x-amz-date" + + awsTimeFormatLong = "20060102T150405Z" + awsTimeFormatShort = "20060102" +) + +func getSha256(input []byte) (string, error) { + hash := sha256.New() + if _, err := hash.Write(input); err != nil { + return "", err + } + return hex.EncodeToString(hash.Sum(nil)), nil +} + +func getHmacSha256(key, input []byte) ([]byte, error) { + hash := hmac.New(sha256.New, key) + if _, err := hash.Write(input); err != nil { + return nil, err + } + return hash.Sum(nil), nil +} + +func cloneRequest(r *http.Request) *http.Request { + r2 := new(http.Request) + *r2 = *r + if r.Header != nil { + r2.Header = make(http.Header, len(r.Header)) + + // Find total number of values. + headerCount := 0 + for _, headerValues := range r.Header { + headerCount += len(headerValues) + } + copiedHeaders := make([]string, headerCount) // shared backing array for headers' values + + for headerKey, headerValues := range r.Header { + headerCount = copy(copiedHeaders, headerValues) + r2.Header[headerKey] = copiedHeaders[:headerCount:headerCount] + copiedHeaders = copiedHeaders[headerCount:] + } + } + return r2 +} + +func canonicalPath(req *http.Request) string { + result := req.URL.EscapedPath() + if result == "" { + return "/" + } + return path.Clean(result) +} + +func canonicalQuery(req *http.Request) string { + queryValues := req.URL.Query() + for queryKey := range queryValues { + sort.Strings(queryValues[queryKey]) + } + return queryValues.Encode() +} + +func canonicalHeaders(req *http.Request) (string, string) { + // Header keys need to be sorted alphabetically. + var headers []string + lowerCaseHeaders := make(http.Header) + for k, v := range req.Header { + k := strings.ToLower(k) + if _, ok := lowerCaseHeaders[k]; ok { + // include additional values + lowerCaseHeaders[k] = append(lowerCaseHeaders[k], v...) + } else { + headers = append(headers, k) + lowerCaseHeaders[k] = v + } + } + sort.Strings(headers) + + var fullHeaders bytes.Buffer + for _, header := range headers { + headerValue := strings.Join(lowerCaseHeaders[header], ",") + fullHeaders.WriteString(header) + fullHeaders.WriteRune(':') + fullHeaders.WriteString(headerValue) + fullHeaders.WriteRune('\n') + } + + return strings.Join(headers, ";"), fullHeaders.String() +} + +func requestDataHash(req *http.Request) (string, error) { + var requestData []byte + if req.Body != nil { + requestBody, err := req.GetBody() + if err != nil { + return "", err + } + defer requestBody.Close() + + requestData, err = ioutil.ReadAll(io.LimitReader(requestBody, 1<<20)) + if err != nil { + return "", err + } + } + + return getSha256(requestData) +} + +func requestHost(req *http.Request) string { + if req.Host != "" { + return req.Host + } + return req.URL.Host +} + +func canonicalRequest(req *http.Request, canonicalHeaderColumns, canonicalHeaderData string) (string, error) { + dataHash, err := requestDataHash(req) + if err != nil { + return "", err + } + + return fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s", req.Method, canonicalPath(req), canonicalQuery(req), canonicalHeaderData, canonicalHeaderColumns, dataHash), nil +} + +// SignRequest adds the appropriate headers to an http.Request +// or returns an error if something prevented this. +func (rs *awsRequestSigner) SignRequest(req *http.Request) error { + signedRequest := cloneRequest(req) + timestamp := now() + + signedRequest.Header.Add("host", requestHost(req)) + + if rs.AwsSecurityCredentials.SecurityToken != "" { + signedRequest.Header.Add(awsSecurityTokenHeader, rs.AwsSecurityCredentials.SecurityToken) + } + + if signedRequest.Header.Get("date") == "" { + signedRequest.Header.Add(awsDateHeader, timestamp.Format(awsTimeFormatLong)) + } + + authorizationCode, err := rs.generateAuthentication(signedRequest, timestamp) + if err != nil { + return err + } + signedRequest.Header.Set("Authorization", authorizationCode) + + req.Header = signedRequest.Header + return nil +} + +func (rs *awsRequestSigner) generateAuthentication(req *http.Request, timestamp time.Time) (string, error) { + canonicalHeaderColumns, canonicalHeaderData := canonicalHeaders(req) + + dateStamp := timestamp.Format(awsTimeFormatShort) + serviceName := "" + if splitHost := strings.Split(requestHost(req), "."); len(splitHost) > 0 { + serviceName = splitHost[0] + } + + credentialScope := fmt.Sprintf("%s/%s/%s/%s", dateStamp, rs.RegionName, serviceName, awsRequestType) + + requestString, err := canonicalRequest(req, canonicalHeaderColumns, canonicalHeaderData) + if err != nil { + return "", err + } + requestHash, err := getSha256([]byte(requestString)) + if err != nil { + return "", err + } + + stringToSign := fmt.Sprintf("%s\n%s\n%s\n%s", awsAlgorithm, timestamp.Format(awsTimeFormatLong), credentialScope, requestHash) + + signingKey := []byte("AWS4" + rs.AwsSecurityCredentials.SecretAccessKey) + for _, signingInput := range []string{ + dateStamp, rs.RegionName, serviceName, awsRequestType, stringToSign, + } { + signingKey, err = getHmacSha256(signingKey, []byte(signingInput)) + if err != nil { + return "", err + } + } + + return fmt.Sprintf("%s Credential=%s/%s, SignedHeaders=%s, Signature=%s", awsAlgorithm, rs.AwsSecurityCredentials.AccessKeyID, credentialScope, canonicalHeaderColumns, hex.EncodeToString(signingKey)), nil +} + +type awsCredentialSource struct { + EnvironmentID string + RegionURL string + RegionalCredVerificationURL string + CredVerificationURL string + TargetResource string + requestSigner *awsRequestSigner + region string + ctx context.Context + client *http.Client +} + +type awsRequestHeader struct { + Key string `json:"key"` + Value string `json:"value"` +} + +type awsRequest struct { + URL string `json:"url"` + Method string `json:"method"` + Headers []awsRequestHeader `json:"headers"` +} + +func (cs awsCredentialSource) doRequest(req *http.Request) (*http.Response, error) { + if cs.client == nil { + cs.client = oauth2.NewClient(cs.ctx, nil) + } + return cs.client.Do(req.WithContext(cs.ctx)) +} + +func (cs awsCredentialSource) subjectToken() (string, error) { + if cs.requestSigner == nil { + awsSecurityCredentials, err := cs.getSecurityCredentials() + if err != nil { + return "", err + } + + if cs.region, err = cs.getRegion(); err != nil { + return "", err + } + + cs.requestSigner = &awsRequestSigner{ + RegionName: cs.region, + AwsSecurityCredentials: awsSecurityCredentials, + } + } + + // Generate the signed request to AWS STS GetCallerIdentity API. + // Use the required regional endpoint. Otherwise, the request will fail. + req, err := http.NewRequest("POST", strings.Replace(cs.RegionalCredVerificationURL, "{region}", cs.region, 1), nil) + if err != nil { + return "", err + } + // The full, canonical resource name of the workload identity pool + // provider, with or without the HTTPS prefix. + // Including this header as part of the signature is recommended to + // ensure data integrity. + if cs.TargetResource != "" { + req.Header.Add("x-goog-cloud-target-resource", cs.TargetResource) + } + cs.requestSigner.SignRequest(req) + + /* + The GCP STS endpoint expects the headers to be formatted as: + # [ + # {key: 'x-amz-date', value: '...'}, + # {key: 'Authorization', value: '...'}, + # ... + # ] + # And then serialized as: + # quote(json.dumps({ + # url: '...', + # method: 'POST', + # headers: [{key: 'x-amz-date', value: '...'}, ...] + # })) + */ + + awsSignedReq := awsRequest{ + URL: req.URL.String(), + Method: "POST", + } + for headerKey, headerList := range req.Header { + for _, headerValue := range headerList { + awsSignedReq.Headers = append(awsSignedReq.Headers, awsRequestHeader{ + Key: headerKey, + Value: headerValue, + }) + } + } + sort.Slice(awsSignedReq.Headers, func(i, j int) bool { + headerCompare := strings.Compare(awsSignedReq.Headers[i].Key, awsSignedReq.Headers[j].Key) + if headerCompare == 0 { + return strings.Compare(awsSignedReq.Headers[i].Value, awsSignedReq.Headers[j].Value) < 0 + } + return headerCompare < 0 + }) + + result, err := json.Marshal(awsSignedReq) + if err != nil { + return "", err + } + return url.QueryEscape(string(result)), nil +} + +func (cs *awsCredentialSource) getRegion() (string, error) { + if envAwsRegion := getenv("AWS_REGION"); envAwsRegion != "" { + return envAwsRegion, nil + } + if envAwsRegion := getenv("AWS_DEFAULT_REGION"); envAwsRegion != "" { + return envAwsRegion, nil + } + + if cs.RegionURL == "" { + return "", errors.New("oauth2/google: unable to determine AWS region") + } + + req, err := http.NewRequest("GET", cs.RegionURL, nil) + if err != nil { + return "", err + } + + resp, err := cs.doRequest(req) + if err != nil { + return "", err + } + defer resp.Body.Close() + + respBody, err := ioutil.ReadAll(io.LimitReader(resp.Body, 1<<20)) + if err != nil { + return "", err + } + + if resp.StatusCode != 200 { + return "", fmt.Errorf("oauth2/google: unable to retrieve AWS region - %s", string(respBody)) + } + + // This endpoint will return the region in format: us-east-2b. + // Only the us-east-2 part should be used. + respBodyEnd := 0 + if len(respBody) > 1 { + respBodyEnd = len(respBody) - 1 + } + return string(respBody[:respBodyEnd]), nil +} + +func (cs *awsCredentialSource) getSecurityCredentials() (result awsSecurityCredentials, err error) { + if accessKeyID := getenv("AWS_ACCESS_KEY_ID"); accessKeyID != "" { + if secretAccessKey := getenv("AWS_SECRET_ACCESS_KEY"); secretAccessKey != "" { + return awsSecurityCredentials{ + AccessKeyID: accessKeyID, + SecretAccessKey: secretAccessKey, + SecurityToken: getenv("AWS_SESSION_TOKEN"), + }, nil + } + } + + roleName, err := cs.getMetadataRoleName() + if err != nil { + return + } + + credentials, err := cs.getMetadataSecurityCredentials(roleName) + if err != nil { + return + } + + if credentials.AccessKeyID == "" { + return result, errors.New("oauth2/google: missing AccessKeyId credential") + } + + if credentials.SecretAccessKey == "" { + return result, errors.New("oauth2/google: missing SecretAccessKey credential") + } + + return credentials, nil +} + +func (cs *awsCredentialSource) getMetadataSecurityCredentials(roleName string) (awsSecurityCredentials, error) { + var result awsSecurityCredentials + + req, err := http.NewRequest("GET", fmt.Sprintf("%s/%s", cs.CredVerificationURL, roleName), nil) + if err != nil { + return result, err + } + req.Header.Add("Content-Type", "application/json") + + resp, err := cs.doRequest(req) + if err != nil { + return result, err + } + defer resp.Body.Close() + + respBody, err := ioutil.ReadAll(io.LimitReader(resp.Body, 1<<20)) + if err != nil { + return result, err + } + + if resp.StatusCode != 200 { + return result, fmt.Errorf("oauth2/google: unable to retrieve AWS security credentials - %s", string(respBody)) + } + + err = json.Unmarshal(respBody, &result) + return result, err +} + +func (cs *awsCredentialSource) getMetadataRoleName() (string, error) { + if cs.CredVerificationURL == "" { + return "", errors.New("oauth2/google: unable to determine the AWS metadata server security credentials endpoint") + } + + req, err := http.NewRequest("GET", cs.CredVerificationURL, nil) + if err != nil { + return "", err + } + + resp, err := cs.doRequest(req) + if err != nil { + return "", err + } + defer resp.Body.Close() + + respBody, err := ioutil.ReadAll(io.LimitReader(resp.Body, 1<<20)) + if err != nil { + return "", err + } + + if resp.StatusCode != 200 { + return "", fmt.Errorf("oauth2/google: unable to retrieve AWS role name - %s", string(respBody)) + } + + return string(respBody), nil +} diff --git a/vendor/golang.org/x/oauth2/google/internal/externalaccount/basecredentials.go b/vendor/golang.org/x/oauth2/google/internal/externalaccount/basecredentials.go new file mode 100644 index 000000000..a4d45d920 --- /dev/null +++ b/vendor/golang.org/x/oauth2/google/internal/externalaccount/basecredentials.go @@ -0,0 +1,184 @@ +// Copyright 2020 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package externalaccount + +import ( + "context" + "fmt" + "golang.org/x/oauth2" + "net/http" + "strconv" + "time" +) + +// now aliases time.Now for testing +var now = func() time.Time { + return time.Now().UTC() +} + +// Config stores the configuration for fetching tokens with external credentials. +type Config struct { + // Audience is the Secure Token Service (STS) audience which contains the resource name for the workload + // identity pool or the workforce pool and the provider identifier in that pool. + Audience string + // SubjectTokenType is the STS token type based on the Oauth2.0 token exchange spec + // e.g. `urn:ietf:params:oauth:token-type:jwt`. + SubjectTokenType string + // TokenURL is the STS token exchange endpoint. + TokenURL string + // TokenInfoURL is the token_info endpoint used to retrieve the account related information ( + // user attributes like account identifier, eg. email, username, uid, etc). This is + // needed for gCloud session account identification. + TokenInfoURL string + // ServiceAccountImpersonationURL is the URL for the service account impersonation request. This is only + // required for workload identity pools when APIs to be accessed have not integrated with UberMint. + ServiceAccountImpersonationURL string + // ClientSecret is currently only required if token_info endpoint also + // needs to be called with the generated GCP access token. When provided, STS will be + // called with additional basic authentication using client_id as username and client_secret as password. + ClientSecret string + // ClientID is only required in conjunction with ClientSecret, as described above. + ClientID string + // CredentialSource contains the necessary information to retrieve the token itself, as well + // as some environmental information. + CredentialSource CredentialSource + // QuotaProjectID is injected by gCloud. If the value is non-empty, the Auth libraries + // will set the x-goog-user-project which overrides the project associated with the credentials. + QuotaProjectID string + // Scopes contains the desired scopes for the returned access token. + Scopes []string +} + +// TokenSource Returns an external account TokenSource struct. This is to be called by package google to construct a google.Credentials. +func (c *Config) TokenSource(ctx context.Context) oauth2.TokenSource { + ts := tokenSource{ + ctx: ctx, + conf: c, + } + if c.ServiceAccountImpersonationURL == "" { + return oauth2.ReuseTokenSource(nil, ts) + } + scopes := c.Scopes + ts.conf.Scopes = []string{"https://www.googleapis.com/auth/cloud-platform"} + imp := impersonateTokenSource{ + ctx: ctx, + url: c.ServiceAccountImpersonationURL, + scopes: scopes, + ts: oauth2.ReuseTokenSource(nil, ts), + } + return oauth2.ReuseTokenSource(nil, imp) +} + +// Subject token file types. +const ( + fileTypeText = "text" + fileTypeJSON = "json" +) + +type format struct { + // Type is either "text" or "json". When not provided "text" type is assumed. + Type string `json:"type"` + // SubjectTokenFieldName is only required for JSON format. This would be "access_token" for azure. + SubjectTokenFieldName string `json:"subject_token_field_name"` +} + +// CredentialSource stores the information necessary to retrieve the credentials for the STS exchange. +// Either the File or the URL field should be filled, depending on the kind of credential in question. +// The EnvironmentID should start with AWS if being used for an AWS credential. +type CredentialSource struct { + File string `json:"file"` + + URL string `json:"url"` + Headers map[string]string `json:"headers"` + + EnvironmentID string `json:"environment_id"` + RegionURL string `json:"region_url"` + RegionalCredVerificationURL string `json:"regional_cred_verification_url"` + CredVerificationURL string `json:"cred_verification_url"` + Format format `json:"format"` +} + +// parse determines the type of CredentialSource needed +func (c *Config) parse(ctx context.Context) (baseCredentialSource, error) { + if len(c.CredentialSource.EnvironmentID) > 3 && c.CredentialSource.EnvironmentID[:3] == "aws" { + if awsVersion, err := strconv.Atoi(c.CredentialSource.EnvironmentID[3:]); err == nil { + if awsVersion != 1 { + return nil, fmt.Errorf("oauth2/google: aws version '%d' is not supported in the current build", awsVersion) + } + return awsCredentialSource{ + EnvironmentID: c.CredentialSource.EnvironmentID, + RegionURL: c.CredentialSource.RegionURL, + RegionalCredVerificationURL: c.CredentialSource.RegionalCredVerificationURL, + CredVerificationURL: c.CredentialSource.URL, + TargetResource: c.Audience, + ctx: ctx, + }, nil + } + } else if c.CredentialSource.File != "" { + return fileCredentialSource{File: c.CredentialSource.File, Format: c.CredentialSource.Format}, nil + } else if c.CredentialSource.URL != "" { + return urlCredentialSource{URL: c.CredentialSource.URL, Headers: c.CredentialSource.Headers, Format: c.CredentialSource.Format, ctx: ctx}, nil + } + return nil, fmt.Errorf("oauth2/google: unable to parse credential source") +} + +type baseCredentialSource interface { + subjectToken() (string, error) +} + +// tokenSource is the source that handles external credentials. It is used to retrieve Tokens. +type tokenSource struct { + ctx context.Context + conf *Config +} + +// Token allows tokenSource to conform to the oauth2.TokenSource interface. +func (ts tokenSource) Token() (*oauth2.Token, error) { + conf := ts.conf + + credSource, err := conf.parse(ts.ctx) + if err != nil { + return nil, err + } + subjectToken, err := credSource.subjectToken() + + if err != nil { + return nil, err + } + stsRequest := stsTokenExchangeRequest{ + GrantType: "urn:ietf:params:oauth:grant-type:token-exchange", + Audience: conf.Audience, + Scope: conf.Scopes, + RequestedTokenType: "urn:ietf:params:oauth:token-type:access_token", + SubjectToken: subjectToken, + SubjectTokenType: conf.SubjectTokenType, + } + header := make(http.Header) + header.Add("Content-Type", "application/x-www-form-urlencoded") + clientAuth := clientAuthentication{ + AuthStyle: oauth2.AuthStyleInHeader, + ClientID: conf.ClientID, + ClientSecret: conf.ClientSecret, + } + stsResp, err := exchangeToken(ts.ctx, conf.TokenURL, &stsRequest, clientAuth, header, nil) + if err != nil { + return nil, err + } + + accessToken := &oauth2.Token{ + AccessToken: stsResp.AccessToken, + TokenType: stsResp.TokenType, + } + if stsResp.ExpiresIn < 0 { + return nil, fmt.Errorf("oauth2/google: got invalid expiry from security token service") + } else if stsResp.ExpiresIn >= 0 { + accessToken.Expiry = now().Add(time.Duration(stsResp.ExpiresIn) * time.Second) + } + + if stsResp.RefreshToken != "" { + accessToken.RefreshToken = stsResp.RefreshToken + } + return accessToken, nil +} diff --git a/vendor/golang.org/x/oauth2/google/internal/externalaccount/clientauth.go b/vendor/golang.org/x/oauth2/google/internal/externalaccount/clientauth.go new file mode 100644 index 000000000..62c2e36cc --- /dev/null +++ b/vendor/golang.org/x/oauth2/google/internal/externalaccount/clientauth.go @@ -0,0 +1,44 @@ +// Copyright 2020 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package externalaccount + +import ( + "encoding/base64" + "golang.org/x/oauth2" + "net/http" + "net/url" +) + +// clientAuthentication represents an OAuth client ID and secret and the mechanism for passing these credentials as stated in rfc6749#2.3.1. +type clientAuthentication struct { + // AuthStyle can be either basic or request-body + AuthStyle oauth2.AuthStyle + ClientID string + ClientSecret string +} + +// InjectAuthentication is used to add authentication to a Secure Token Service exchange +// request. It modifies either the passed url.Values or http.Header depending on the desired +// authentication format. +func (c *clientAuthentication) InjectAuthentication(values url.Values, headers http.Header) { + if c.ClientID == "" || c.ClientSecret == "" || values == nil || headers == nil { + return + } + + switch c.AuthStyle { + case oauth2.AuthStyleInHeader: // AuthStyleInHeader corresponds to basic authentication as defined in rfc7617#2 + plainHeader := c.ClientID + ":" + c.ClientSecret + headers.Add("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(plainHeader))) + case oauth2.AuthStyleInParams: // AuthStyleInParams corresponds to request-body authentication with ClientID and ClientSecret in the message body. + values.Set("client_id", c.ClientID) + values.Set("client_secret", c.ClientSecret) + case oauth2.AuthStyleAutoDetect: + values.Set("client_id", c.ClientID) + values.Set("client_secret", c.ClientSecret) + default: + values.Set("client_id", c.ClientID) + values.Set("client_secret", c.ClientSecret) + } +} diff --git a/vendor/golang.org/x/oauth2/google/internal/externalaccount/err.go b/vendor/golang.org/x/oauth2/google/internal/externalaccount/err.go new file mode 100644 index 000000000..233a78cef --- /dev/null +++ b/vendor/golang.org/x/oauth2/google/internal/externalaccount/err.go @@ -0,0 +1,18 @@ +// Copyright 2020 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package externalaccount + +import "fmt" + +// Error for handling OAuth related error responses as stated in rfc6749#5.2. +type Error struct { + Code string + URI string + Description string +} + +func (err *Error) Error() string { + return fmt.Sprintf("got error code %s from %s: %s", err.Code, err.URI, err.Description) +} diff --git a/vendor/golang.org/x/oauth2/google/internal/externalaccount/filecredsource.go b/vendor/golang.org/x/oauth2/google/internal/externalaccount/filecredsource.go new file mode 100644 index 000000000..e953ddb47 --- /dev/null +++ b/vendor/golang.org/x/oauth2/google/internal/externalaccount/filecredsource.go @@ -0,0 +1,57 @@ +// Copyright 2020 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package externalaccount + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "io/ioutil" + "os" +) + +type fileCredentialSource struct { + File string + Format format +} + +func (cs fileCredentialSource) subjectToken() (string, error) { + tokenFile, err := os.Open(cs.File) + if err != nil { + return "", fmt.Errorf("oauth2/google: failed to open credential file %q", cs.File) + } + defer tokenFile.Close() + tokenBytes, err := ioutil.ReadAll(io.LimitReader(tokenFile, 1<<20)) + if err != nil { + return "", fmt.Errorf("oauth2/google: failed to read credential file: %v", err) + } + tokenBytes = bytes.TrimSpace(tokenBytes) + switch cs.Format.Type { + case "json": + jsonData := make(map[string]interface{}) + err = json.Unmarshal(tokenBytes, &jsonData) + if err != nil { + return "", fmt.Errorf("oauth2/google: failed to unmarshal subject token file: %v", err) + } + val, ok := jsonData[cs.Format.SubjectTokenFieldName] + if !ok { + return "", errors.New("oauth2/google: provided subject_token_field_name not found in credentials") + } + token, ok := val.(string) + if !ok { + return "", errors.New("oauth2/google: improperly formatted subject token") + } + return token, nil + case "text": + return string(tokenBytes), nil + case "": + return string(tokenBytes), nil + default: + return "", errors.New("oauth2/google: invalid credential_source file format type") + } + +} diff --git a/vendor/golang.org/x/oauth2/google/internal/externalaccount/impersonate.go b/vendor/golang.org/x/oauth2/google/internal/externalaccount/impersonate.go new file mode 100644 index 000000000..1f6009b38 --- /dev/null +++ b/vendor/golang.org/x/oauth2/google/internal/externalaccount/impersonate.go @@ -0,0 +1,83 @@ +// Copyright 2021 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package externalaccount + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "golang.org/x/oauth2" + "io" + "io/ioutil" + "net/http" + "time" +) + +// generateAccesstokenReq is used for service account impersonation +type generateAccessTokenReq struct { + Delegates []string `json:"delegates,omitempty"` + Lifetime string `json:"lifetime,omitempty"` + Scope []string `json:"scope,omitempty"` +} + +type impersonateTokenResponse struct { + AccessToken string `json:"accessToken"` + ExpireTime string `json:"expireTime"` +} + +type impersonateTokenSource struct { + ctx context.Context + ts oauth2.TokenSource + + url string + scopes []string +} + +// Token performs the exchange to get a temporary service account token to allow access to GCP. +func (its impersonateTokenSource) Token() (*oauth2.Token, error) { + reqBody := generateAccessTokenReq{ + Lifetime: "3600s", + Scope: its.scopes, + } + b, err := json.Marshal(reqBody) + if err != nil { + return nil, fmt.Errorf("oauth2/google: unable to marshal request: %v", err) + } + client := oauth2.NewClient(its.ctx, its.ts) + req, err := http.NewRequest("POST", its.url, bytes.NewReader(b)) + if err != nil { + return nil, fmt.Errorf("oauth2/google: unable to create impersonation request: %v", err) + } + req = req.WithContext(its.ctx) + req.Header.Set("Content-Type", "application/json") + + resp, err := client.Do(req) + if err != nil { + return nil, fmt.Errorf("oauth2/google: unable to generate access token: %v", err) + } + defer resp.Body.Close() + body, err := ioutil.ReadAll(io.LimitReader(resp.Body, 1<<20)) + if err != nil { + return nil, fmt.Errorf("oauth2/google: unable to read body: %v", err) + } + if c := resp.StatusCode; c < 200 || c > 299 { + return nil, fmt.Errorf("oauth2/google: status code %d: %s", c, body) + } + + var accessTokenResp impersonateTokenResponse + if err := json.Unmarshal(body, &accessTokenResp); err != nil { + return nil, fmt.Errorf("oauth2/google: unable to parse response: %v", err) + } + expiry, err := time.Parse(time.RFC3339, accessTokenResp.ExpireTime) + if err != nil { + return nil, fmt.Errorf("oauth2/google: unable to parse expiry: %v", err) + } + return &oauth2.Token{ + AccessToken: accessTokenResp.AccessToken, + Expiry: expiry, + TokenType: "Bearer", + }, nil +} diff --git a/vendor/golang.org/x/oauth2/google/internal/externalaccount/sts_exchange.go b/vendor/golang.org/x/oauth2/google/internal/externalaccount/sts_exchange.go new file mode 100644 index 000000000..a8a704bb4 --- /dev/null +++ b/vendor/golang.org/x/oauth2/google/internal/externalaccount/sts_exchange.go @@ -0,0 +1,104 @@ +// Copyright 2020 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package externalaccount + +import ( + "context" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/url" + "strconv" + "strings" + + "golang.org/x/oauth2" +) + +// exchangeToken performs an oauth2 token exchange with the provided endpoint. +// The first 4 fields are all mandatory. headers can be used to pass additional +// headers beyond the bare minimum required by the token exchange. options can +// be used to pass additional JSON-structured options to the remote server. +func exchangeToken(ctx context.Context, endpoint string, request *stsTokenExchangeRequest, authentication clientAuthentication, headers http.Header, options map[string]interface{}) (*stsTokenExchangeResponse, error) { + + client := oauth2.NewClient(ctx, nil) + + data := url.Values{} + data.Set("audience", request.Audience) + data.Set("grant_type", "urn:ietf:params:oauth:grant-type:token-exchange") + data.Set("requested_token_type", "urn:ietf:params:oauth:token-type:access_token") + data.Set("subject_token_type", request.SubjectTokenType) + data.Set("subject_token", request.SubjectToken) + data.Set("scope", strings.Join(request.Scope, " ")) + if options != nil { + opts, err := json.Marshal(options) + if err != nil { + return nil, fmt.Errorf("oauth2/google: failed to marshal additional options: %v", err) + } + data.Set("options", string(opts)) + } + + authentication.InjectAuthentication(data, headers) + encodedData := data.Encode() + + req, err := http.NewRequest("POST", endpoint, strings.NewReader(encodedData)) + if err != nil { + return nil, fmt.Errorf("oauth2/google: failed to properly build http request: %v", err) + + } + req = req.WithContext(ctx) + for key, list := range headers { + for _, val := range list { + req.Header.Add(key, val) + } + } + req.Header.Add("Content-Length", strconv.Itoa(len(encodedData))) + + resp, err := client.Do(req) + + if err != nil { + return nil, fmt.Errorf("oauth2/google: invalid response from Secure Token Server: %v", err) + } + defer resp.Body.Close() + + body, err := ioutil.ReadAll(io.LimitReader(resp.Body, 1<<20)) + if c := resp.StatusCode; c < 200 || c > 299 { + return nil, fmt.Errorf("oauth2/google: status code %d: %s", c, body) + } + var stsResp stsTokenExchangeResponse + err = json.Unmarshal(body, &stsResp) + if err != nil { + return nil, fmt.Errorf("oauth2/google: failed to unmarshal response body from Secure Token Server: %v", err) + + } + + return &stsResp, nil +} + +// stsTokenExchangeRequest contains fields necessary to make an oauth2 token exchange. +type stsTokenExchangeRequest struct { + ActingParty struct { + ActorToken string + ActorTokenType string + } + GrantType string + Resource string + Audience string + Scope []string + RequestedTokenType string + SubjectToken string + SubjectTokenType string +} + +// stsTokenExchangeResponse is used to decode the remote server response during an oauth2 token exchange. +type stsTokenExchangeResponse struct { + AccessToken string `json:"access_token"` + IssuedTokenType string `json:"issued_token_type"` + TokenType string `json:"token_type"` + ExpiresIn int `json:"expires_in"` + Scope string `json:"scope"` + RefreshToken string `json:"refresh_token"` +} diff --git a/vendor/golang.org/x/oauth2/google/internal/externalaccount/urlcredsource.go b/vendor/golang.org/x/oauth2/google/internal/externalaccount/urlcredsource.go new file mode 100644 index 000000000..91b8f2002 --- /dev/null +++ b/vendor/golang.org/x/oauth2/google/internal/externalaccount/urlcredsource.go @@ -0,0 +1,74 @@ +// Copyright 2020 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package externalaccount + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "golang.org/x/oauth2" + "io" + "io/ioutil" + "net/http" +) + +type urlCredentialSource struct { + URL string + Headers map[string]string + Format format + ctx context.Context +} + +func (cs urlCredentialSource) subjectToken() (string, error) { + client := oauth2.NewClient(cs.ctx, nil) + req, err := http.NewRequest("GET", cs.URL, nil) + if err != nil { + return "", fmt.Errorf("oauth2/google: HTTP request for URL-sourced credential failed: %v", err) + } + req = req.WithContext(cs.ctx) + + for key, val := range cs.Headers { + req.Header.Add(key, val) + } + resp, err := client.Do(req) + if err != nil { + return "", fmt.Errorf("oauth2/google: invalid response when retrieving subject token: %v", err) + } + defer resp.Body.Close() + + respBody, err := ioutil.ReadAll(io.LimitReader(resp.Body, 1<<20)) + if err != nil { + return "", fmt.Errorf("oauth2/google: invalid body in subject token URL query: %v", err) + } + if c := resp.StatusCode; c < 200 || c > 299 { + return "", fmt.Errorf("oauth2/google: status code %d: %s", c, respBody) + } + + switch cs.Format.Type { + case "json": + jsonData := make(map[string]interface{}) + err = json.Unmarshal(respBody, &jsonData) + if err != nil { + return "", fmt.Errorf("oauth2/google: failed to unmarshal subject token file: %v", err) + } + val, ok := jsonData[cs.Format.SubjectTokenFieldName] + if !ok { + return "", errors.New("oauth2/google: provided subject_token_field_name not found in credentials") + } + token, ok := val.(string) + if !ok { + return "", errors.New("oauth2/google: improperly formatted subject token") + } + return token, nil + case "text": + return string(respBody), nil + case "": + return string(respBody), nil + default: + return "", errors.New("oauth2/google: invalid credential_source file format type") + } + +} diff --git a/vendor/golang.org/x/oauth2/google/jwt.go b/vendor/golang.org/x/oauth2/google/jwt.go index b0fdb3a88..67d97b990 100644 --- a/vendor/golang.org/x/oauth2/google/jwt.go +++ b/vendor/golang.org/x/oauth2/google/jwt.go @@ -7,6 +7,7 @@ package google import ( "crypto/rsa" "fmt" + "strings" "time" "golang.org/x/oauth2" @@ -24,6 +25,28 @@ import ( // optimization supported by a few Google services. // Unless you know otherwise, you should use JWTConfigFromJSON instead. func JWTAccessTokenSourceFromJSON(jsonKey []byte, audience string) (oauth2.TokenSource, error) { + return newJWTSource(jsonKey, audience, nil) +} + +// JWTAccessTokenSourceWithScope uses a Google Developers service account JSON +// key file to read the credentials that authorize and authenticate the +// requests, and returns a TokenSource that does not use any OAuth2 flow but +// instead creates a JWT and sends that as the access token. +// The scope is typically a list of URLs that specifies the scope of the +// credentials. +// +// Note that this is not a standard OAuth flow, but rather an +// optimization supported by a few Google services. +// Unless you know otherwise, you should use JWTConfigFromJSON instead. +func JWTAccessTokenSourceWithScope(jsonKey []byte, scope ...string) (oauth2.TokenSource, error) { + return newJWTSource(jsonKey, "", scope) +} + +func newJWTSource(jsonKey []byte, audience string, scopes []string) (oauth2.TokenSource, error) { + if len(scopes) == 0 && audience == "" { + return nil, fmt.Errorf("google: missing scope/audience for JWT access token") + } + cfg, err := JWTConfigFromJSON(jsonKey) if err != nil { return nil, fmt.Errorf("google: could not parse JSON key: %v", err) @@ -35,6 +58,7 @@ func JWTAccessTokenSourceFromJSON(jsonKey []byte, audience string) (oauth2.Token ts := &jwtAccessTokenSource{ email: cfg.Email, audience: audience, + scopes: scopes, pk: pk, pkID: cfg.PrivateKeyID, } @@ -47,6 +71,7 @@ func JWTAccessTokenSourceFromJSON(jsonKey []byte, audience string) (oauth2.Token type jwtAccessTokenSource struct { email, audience string + scopes []string pk *rsa.PrivateKey pkID string } @@ -54,12 +79,14 @@ type jwtAccessTokenSource struct { func (ts *jwtAccessTokenSource) Token() (*oauth2.Token, error) { iat := time.Now() exp := iat.Add(time.Hour) + scope := strings.Join(ts.scopes, " ") cs := &jws.ClaimSet{ - Iss: ts.email, - Sub: ts.email, - Aud: ts.audience, - Iat: iat.Unix(), - Exp: exp.Unix(), + Iss: ts.email, + Sub: ts.email, + Aud: ts.audience, + Scope: scope, + Iat: iat.Unix(), + Exp: exp.Unix(), } hdr := &jws.Header{ Algorithm: "RS256", diff --git a/vendor/golang.org/x/oauth2/internal/client_appengine.go b/vendor/golang.org/x/oauth2/internal/client_appengine.go index 743487188..e1755d1d9 100644 --- a/vendor/golang.org/x/oauth2/internal/client_appengine.go +++ b/vendor/golang.org/x/oauth2/internal/client_appengine.go @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +//go:build appengine // +build appengine package internal diff --git a/vendor/modules.txt b/vendor/modules.txt index 367f9286e..2e9353bc1 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -371,9 +371,13 @@ golang.org/x/net/idna golang.org/x/net/internal/timeseries golang.org/x/net/publicsuffix golang.org/x/net/trace -# golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43 +# golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914 +## explicit golang.org/x/oauth2 +golang.org/x/oauth2/authhandler +golang.org/x/oauth2/clientcredentials golang.org/x/oauth2/google +golang.org/x/oauth2/google/internal/externalaccount golang.org/x/oauth2/internal golang.org/x/oauth2/jws golang.org/x/oauth2/jwt