Added JWT and oauth2 outbound authenticators
Proxy auth definitions for the UI
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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{})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
+2
-130
@@ -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{}
|
||||
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
+5
-1
@@ -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"
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
}
|
||||
|
||||
+5
-5
@@ -1,7 +1,7 @@
|
||||
# OAuth2 for Go
|
||||
|
||||
[](https://pkg.go.dev/golang.org/x/oauth2)
|
||||
[](https://travis-ci.org/golang/oauth2)
|
||||
[](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
|
||||
|
||||
+56
@@ -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)
|
||||
}
|
||||
+120
@@ -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
|
||||
}
|
||||
+1
@@ -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).
|
||||
|
||||
+1
@@ -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.
|
||||
|
||||
+81
-14
@@ -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)
|
||||
}
|
||||
|
||||
+41
-2
@@ -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.
|
||||
//
|
||||
|
||||
+45
-8
@@ -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:
|
||||
|
||||
+470
@@ -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
|
||||
}
|
||||
+184
@@ -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
|
||||
}
|
||||
+44
@@ -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)
|
||||
}
|
||||
}
|
||||
+18
@@ -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)
|
||||
}
|
||||
+57
@@ -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")
|
||||
}
|
||||
|
||||
}
|
||||
+83
@@ -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
|
||||
}
|
||||
+104
@@ -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"`
|
||||
}
|
||||
+74
@@ -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")
|
||||
}
|
||||
|
||||
}
|
||||
+32
-5
@@ -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",
|
||||
|
||||
+1
@@ -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
|
||||
|
||||
Vendored
+5
-1
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user