diff --git a/example/client/app/app.go b/example/client/app/app.go index f1b99d7..4c0831b 100644 --- a/example/client/app/app.go +++ b/example/client/app/app.go @@ -6,10 +6,10 @@ import ( "fmt" "net/http" "os" - - "github.com/sirupsen/logrus" + "time" "github.com/google/uuid" + "github.com/sirupsen/logrus" "github.com/caos/oidc/pkg/oidc" "github.com/caos/oidc/pkg/rp" @@ -29,41 +29,30 @@ func main() { ctx := context.Background() - rpConfig := &rp.Config{ - ClientID: clientID, - ClientSecret: clientSecret, - Issuer: issuer, - CallbackURL: fmt.Sprintf("http://localhost:%v%v", port, callbackPath), - Scopes: []string{"openid", "profile", "email"}, - } + redirectURI := fmt.Sprintf("http://localhost:%v%v", port, callbackPath) + scopes := []string{oidc.ScopeOpenID, oidc.ScopeProfile, oidc.ScopeEmail} cookieHandler := utils.NewCookieHandler(key, key, utils.WithUnsecure()) - provider, err := rp.NewDefaultRP(rpConfig, rp.WithCookieHandler(cookieHandler)) //rp.WithPKCE(cookieHandler)) //, + provider, err := rp.NewRelayingPartyOIDC(issuer, clientID, clientSecret, redirectURI, scopes, + rp.WithPKCE(cookieHandler), + rp.WithVerifierOpts(rp.WithIssuedAtOffset(5*time.Second)), + ) if err != nil { logrus.Fatalf("error creating provider %s", err.Error()) } - // state := "foobar" - state := uuid.New().String() + //generate some state (representing the state of the user in your application, + //e.g. the page where he was before sending him to login + state := func() string { + return uuid.New().String() + } - http.Handle("/login", provider.AuthURLHandler(state)) - // http.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) { - // http.Redirect(w, r, provider.AuthURL(state), http.StatusFound) - // }) - - // http.HandleFunc(callbackPath, func(w http.ResponseWriter, r *http.Request) { - // tokens, err := provider.CodeExchange(ctx, r.URL.Query().Get("code")) - // if err != nil { - // http.Error(w, "failed to exchange token: "+err.Error(), http.StatusUnauthorized) - // return - // } - // data, err := json.Marshal(tokens) - // if err != nil { - // http.Error(w, err.Error(), http.StatusInternalServerError) - // return - // } - // w.Write(data) - // }) + //register the AuthURLHandler at your preferred path + //the AuthURLHandler creates the auth request and redirects the user to the auth server + //including state handling with secure cookie and the possibility to use PKCE + http.Handle("/login", rp.AuthURLHandler(state, provider)) + //for demonstration purposes the returned tokens (access token, id_token an its parsed claims) + //are written as JSON objects onto response marshal := func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens, state string) { _ = state data, err := json.Marshal(tokens) @@ -74,10 +63,13 @@ func main() { w.Write(data) } - http.Handle(callbackPath, provider.CodeExchangeHandler(marshal)) + //register the CodeExchangeHandler at the callbackPath + //the CodeExchangeHandler handles the auth response, creates the token request and calls the callback function + //with the returned tokens from the token endpoint + http.Handle(callbackPath, rp.CodeExchangeHandler(marshal, provider)) http.HandleFunc("/test", func(w http.ResponseWriter, r *http.Request) { - tokens, err := provider.ClientCredentials(ctx, "scope") + tokens, err := rp.ClientCredentials(ctx, provider, "scope") if err != nil { http.Error(w, "failed to exchange token: "+err.Error(), http.StatusUnauthorized) return @@ -92,5 +84,5 @@ func main() { }) lis := fmt.Sprintf("127.0.0.1:%s", port) logrus.Infof("listening on http://%s/", lis) - logrus.Fatal(http.ListenAndServe("127.0.0.1:5556", nil)) + logrus.Fatal(http.ListenAndServe("127.0.0.1:"+port, nil)) } diff --git a/example/client/github/github.go b/example/client/github/github.go index 4afa2fb..5489389 100644 --- a/example/client/github/github.go +++ b/example/client/github/github.go @@ -3,11 +3,16 @@ package main import ( "context" "fmt" - "github.com/caos/oidc/pkg/cli" - "github.com/caos/oidc/pkg/rp" - "github.com/google/go-github/v31/github" - githubOAuth "golang.org/x/oauth2/github" "os" + + "github.com/google/go-github/v31/github" + "github.com/google/uuid" + "golang.org/x/oauth2" + githubOAuth "golang.org/x/oauth2/github" + + "github.com/caos/oidc/pkg/rp" + "github.com/caos/oidc/pkg/rp/cli" + "github.com/caos/oidc/pkg/utils" ) var ( @@ -20,24 +25,32 @@ func main() { clientSecret := os.Getenv("CLIENT_SECRET") port := os.Getenv("PORT") - rpConfig := &rp.Config{ + rpConfig := &oauth2.Config{ ClientID: clientID, ClientSecret: clientSecret, - CallbackURL: fmt.Sprintf("http://localhost:%v%v", port, callbackPath), + RedirectURL: fmt.Sprintf("http://localhost:%v%v", port, callbackPath), Scopes: []string{"repo", "repo_deployment"}, - Endpoints: githubOAuth.Endpoint, + Endpoint: githubOAuth.Endpoint, } - oauth2Client := cli.CodeFlowForClient(rpConfig, key, callbackPath, port) - - client := github.NewClient(oauth2Client) - ctx := context.Background() - _, _, err := client.Users.Get(ctx, "") + cookieHandler := utils.NewCookieHandler(key, key, utils.WithUnsecure()) + relayingParty, err := rp.NewRelayingPartyOAuth(rpConfig, rp.WithCookieHandler(cookieHandler)) if err != nil { - fmt.Println("OAuth flow failed") - } else { - - fmt.Println("OAuth flow success") + fmt.Printf("error creating relaying party: %v", err) + return } + state := func() string { + return uuid.New().String() + } + token := cli.CodeFlow(relayingParty, callbackPath, port, state) + + client := github.NewClient(relayingParty.Client(ctx, token.Token)) + + _, _, err = client.Users.Get(ctx, "") + if err != nil { + fmt.Printf("error %v", err) + return + } + fmt.Println("call succeeded") } diff --git a/example/internal/mock/storage.go b/example/internal/mock/storage.go index febb28c..f20fb9b 100644 --- a/example/internal/mock/storage.go +++ b/example/internal/mock/storage.go @@ -151,8 +151,8 @@ func (s *AuthStorage) AuthRequestByID(_ context.Context, id string) (op.AuthRequ } return a, nil } -func (s *AuthStorage) CreateToken(_ context.Context, authReq op.AuthRequest) (string, time.Time, error) { - return authReq.GetID(), time.Now().UTC().Add(5 * time.Minute), nil +func (s *AuthStorage) CreateToken(_ context.Context, authReq op.TokenRequest) (string, time.Time, error) { + return "id", time.Now().UTC().Add(5 * time.Minute), nil } func (s *AuthStorage) TerminateSession(_ context.Context, userID, clientID string) error { return nil @@ -174,6 +174,10 @@ func (s *AuthStorage) GetKeySet(_ context.Context) (*jose.JSONWebKeySet, error) }, }, nil } +func (s *AuthStorage) GetKeyByIDAndUserID(_ context.Context, _, _ string) (*jose.JSONWebKey, error) { + pubkey := s.key.Public() + return &jose.JSONWebKey{Key: pubkey, Use: "sig", Algorithm: "RS256", KeyID: "1"}, nil +} func (s *AuthStorage) GetClientByClientID(_ context.Context, id string) (op.Client, error) { if id == "none" { @@ -182,20 +186,24 @@ func (s *AuthStorage) GetClientByClientID(_ context.Context, id string) (op.Clie var appType op.ApplicationType var authMethod op.AuthMethod var accessTokenType op.AccessTokenType + var responseTypes []oidc.ResponseType if id == "web" { appType = op.ApplicationTypeWeb authMethod = op.AuthMethodBasic accessTokenType = op.AccessTokenTypeBearer + responseTypes = []oidc.ResponseType{oidc.ResponseTypeCode} } else if id == "native" { appType = op.ApplicationTypeNative authMethod = op.AuthMethodNone accessTokenType = op.AccessTokenTypeBearer + responseTypes = []oidc.ResponseType{oidc.ResponseTypeCode} } else { appType = op.ApplicationTypeUserAgent authMethod = op.AuthMethodNone accessTokenType = op.AccessTokenTypeJWT + responseTypes = []oidc.ResponseType{oidc.ResponseTypeIDToken, oidc.ResponseTypeIDTokenOnly} } - return &ConfClient{ID: id, applicationType: appType, authMethod: authMethod, accessTokenType: accessTokenType, devMode: false}, nil + return &ConfClient{ID: id, applicationType: appType, authMethod: authMethod, accessTokenType: accessTokenType, responseTypes: responseTypes, devMode: false}, nil } func (s *AuthStorage) AuthorizeClientIDSecret(_ context.Context, id string, _ string) error { diff --git a/example/server/default/default.go b/example/server/default/default.go index 421c7f7..d5922d4 100644 --- a/example/server/default/default.go +++ b/example/server/default/default.go @@ -21,7 +21,7 @@ func main() { CryptoKey: sha256.Sum256([]byte("test")), } storage := mock.NewAuthStorage() - handler, err := op.NewDefaultOP(ctx, config, storage, op.WithCustomTokenEndpoint(op.NewEndpoint("test"))) + handler, err := op.NewOpenIDProvider(ctx, config, storage, op.WithCustomTokenEndpoint(op.NewEndpoint("test"))) if err != nil { log.Fatal(err) } diff --git a/go.mod b/go.mod index cc9f60b..f162e70 100644 --- a/go.mod +++ b/go.mod @@ -3,23 +3,21 @@ module github.com/caos/oidc go 1.15 require ( - github.com/caos/logging v0.0.0-20191210002624-b3260f690a6a + github.com/caos/logging v0.0.2 github.com/golang/mock v1.4.4 - github.com/google/go-cmp v0.4.1 // indirect + github.com/google/go-cmp v0.5.2 // indirect github.com/google/go-github/v31 v31.0.0 github.com/google/uuid v1.1.2 - github.com/gorilla/handlers v1.5.0 + github.com/gorilla/handlers v1.5.1 github.com/gorilla/mux v1.8.0 github.com/gorilla/schema v1.2.0 github.com/gorilla/securecookie v1.1.1 - github.com/kr/pretty v0.1.0 // indirect + github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect github.com/sirupsen/logrus v1.6.0 github.com/stretchr/testify v1.6.1 - golang.org/x/crypto v0.0.0-20191128160524-b544559bb6d1 // indirect - golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933 - golang.org/x/oauth2 v0.0.0-20191122200657-5d9234df094c + golang.org/x/net v0.0.0-20200904194848-62affa334b73 + golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43 golang.org/x/text v0.3.3 - google.golang.org/appengine v1.6.5 // indirect - gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect + gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b // indirect gopkg.in/square/go-jose.v2 v2.5.1 ) diff --git a/go.sum b/go.sum index eee02f9..f7b4f76 100644 --- a/go.sum +++ b/go.sum @@ -1,33 +1,128 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -github.com/caos/logging v0.0.0-20191210002624-b3260f690a6a h1:HOU/3xL/afsZ+2aCstfJlrzRkwYMTFR1TIEgps5ny8s= -github.com/caos/logging v0.0.0-20191210002624-b3260f690a6a/go.mod h1:9LKiDE2ChuGv6CHYif/kiugrfEXu9AwDiFWSreX7Wp0= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/caos/logging v0.0.2 h1:ebg5C/HN0ludYR+WkvnFjwSExF4wvyiWPyWGcKMYsoo= +github.com/caos/logging v0.0.2/go.mod h1:9LKiDE2ChuGv6CHYif/kiugrfEXu9AwDiFWSreX7Wp0= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ= github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/google/go-cmp v0.4.1 h1:/exdXoGamhu5ONeUJH0deniYLWYvQwW66yvlfiiKTu0= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-github/v31 v31.0.0 h1:JJUxlP9lFK+ziXKimTCprajMApV1ecWD4NB6CCb0plo= github.com/google/go-github/v31 v31.0.0/go.mod h1:NQPZol8/1sMoWYGN2yaALIBytu17gAWfhbweiEed3pM= github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gorilla/handlers v1.5.0 h1:4wjo3sf9azi99c8hTmyaxp9y5S+pFszsy3pP0rAw/lw= -github.com/gorilla/handlers v1.5.0/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4= +github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/schema v1.2.0 h1:YufUaxZYCKGFuAq3c96BOhjgd5nmXiOY9NGzF247Tsc= github.com/gorilla/schema v1.2.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU= github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= @@ -38,8 +133,12 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I= @@ -48,50 +147,281 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191128160524-b544559bb6d1 h1:anGSYQpPhQwXlwsu5wmfq0nWkCNaMEMUwAv13Y92hd8= -golang.org/x/crypto v0.0.0-20191128160524-b544559bb6d1/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933 h1:e6HwijUxhDe+hPNjZQQn9bA5PW3vNmnN64U2ZW759Lk= -golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202 h1:VvcQYSHwXgi7W+TpUR6A9g6Up98WAHf3f/ulnJ62IyA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200904194848-62affa334b73 h1:MXfv8rhZWmFeqX3GNZRsd6vOLoaCHjYEX3qkRo3YBUA= +golang.org/x/net v0.0.0-20200904194848-62affa334b73/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20191122200657-5d9234df094c h1:HjRaKPaiWks0f5tA6ELVF7ZfqSppfPwOEEAvsrKUTO4= -golang.org/x/oauth2 v0.0.0-20191122200657-5d9234df094c/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +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/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= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191206220618-eeba5f6aabab h1:FvshnhkKW+LO3HWHodML8kuVX8rnJTxKm9dFPuI68UM= golang.org/x/sys v0.0.0-20191206220618-eeba5f6aabab/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642 h1:B6caxRw+hozq68X2MY7jEpZh/cr4/aHLv9xU8Kkadrw= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b h1:QRR6H1YWRnHb4Y/HeNFCTJLFVxaq6wH4YuVdsUOr75U= +gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/square/go-jose.v2 v2.5.1 h1:7odma5RETjNHWJnR32wx8t+Io4djHE1PqxCFx3iiZ2w= gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo= gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/pkg/cli/cli.go b/pkg/cli/cli.go deleted file mode 100644 index c7328e4..0000000 --- a/pkg/cli/cli.go +++ /dev/null @@ -1,117 +0,0 @@ -package cli - -import ( - "context" - "fmt" - "github.com/caos/oidc/pkg/oidc" - "github.com/caos/oidc/pkg/rp" - "github.com/caos/oidc/pkg/utils" - "github.com/google/uuid" - "github.com/sirupsen/logrus" - "log" - "net/http" - "strings" - "time" -) - -func CodeFlow(rpc *rp.Config, key []byte, callbackPath string, port string) *oidc.Tokens { - cookieHandler := utils.NewCookieHandler(key, key, utils.WithUnsecure()) - provider, err := rp.NewDefaultRP(rpc, rp.WithCookieHandler(cookieHandler)) //rp.WithPKCE(cookieHandler)) //, - if err != nil { - logrus.Fatalf("error creating provider %s", err.Error()) - } - - return codeFlow(provider, callbackPath, port) -} - -func TokenForClient(rpc *rp.Config, key []byte, token *oidc.Tokens) *http.Client { - cookieHandler := utils.NewCookieHandler(key, key, utils.WithUnsecure()) - provider, err := rp.NewDefaultRP(rpc, rp.WithCookieHandler(cookieHandler)) //rp.WithPKCE(cookieHandler)) //, - if err != nil { - logrus.Fatalf("error creating provider %s", err.Error()) - } - - return provider.Client(context.Background(), token.Token) -} - -func CodeFlowForClient(rpc *rp.Config, key []byte, callbackPath string, port string) *http.Client { - cookieHandler := utils.NewCookieHandler(key, key, utils.WithUnsecure()) - provider, err := rp.NewDefaultRP(rpc, rp.WithCookieHandler(cookieHandler)) //rp.WithPKCE(cookieHandler)) //, - if err != nil { - logrus.Fatalf("error creating provider %s", err.Error()) - } - token := codeFlow(provider, callbackPath, port) - - return provider.Client(context.Background(), token.Token) -} - -func codeFlow(provider rp.DelegationTokenExchangeRP, callbackPath string, port string) *oidc.Tokens { - loginPath := "/login" - portStr := port - if !strings.HasPrefix(port, ":") { - portStr = strings.Join([]string{":", portStr}, "") - } - - getToken, setToken := getAndSetTokens() - - state := uuid.New().String() - http.Handle(loginPath, provider.AuthURLHandler(state)) - - marshal := func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens, state string) { - setToken(w, tokens) - } - http.Handle(callbackPath, provider.CodeExchangeHandler(marshal)) - - // start http-server - stopHttpServer := startHttpServer(portStr) - - // open browser in different window - utils.OpenBrowser(strings.Join([]string{"http://localhost", portStr, loginPath}, "")) - - // wait until user is logged into browser - ret := getToken() - - // stop http-server as no callback is needed anymore - stopHttpServer() - - // return tokens - return ret -} - -func startHttpServer(port string) func() { - srv := &http.Server{Addr: port} - go func() { - - // always returns error. ErrServerClosed on graceful close - if err := srv.ListenAndServe(); err != http.ErrServerClosed { - // unexpected error. port in use? - log.Fatalf("ListenAndServe(): %v", err) - } - }() - - return func() { - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - - if err := srv.Shutdown(ctx); err != nil { - log.Fatalf("Shutdown(): %v", err) - } - } -} - -func getAndSetTokens() (func() *oidc.Tokens, func(w http.ResponseWriter, tokens *oidc.Tokens)) { - marshalChan := make(chan *oidc.Tokens) - - getToken := func() *oidc.Tokens { - return <-marshalChan - } - setToken := func(w http.ResponseWriter, tokens *oidc.Tokens) { - marshalChan <- tokens - - msg := "

Success!

" - msg = msg + "

You are authenticated and can now return to the CLI.

" - fmt.Fprintf(w, msg) - } - - return getToken, setToken -} diff --git a/pkg/oidc/authorization.go b/pkg/oidc/authorization.go index 3398f51..35da2fb 100644 --- a/pkg/oidc/authorization.go +++ b/pkg/oidc/authorization.go @@ -1,10 +1,13 @@ package oidc import ( + "encoding/json" "errors" "strings" + "time" "golang.org/x/text/language" + "gopkg.in/square/go-jose.v2" ) const ( @@ -64,6 +67,8 @@ const ( //GrantTypeCode defines the grant_type `authorization_code` used for the Token Request in the Authorization Code Flow GrantTypeCode GrantType = "authorization_code" + //GrantTypeBearer define the grant_type `urn:ietf:params:oauth:grant-type:jwt-bearer` used for the JWT Authorization Grant + GrantTypeBearer GrantType = "urn:ietf:params:oauth:grant-type:jwt-bearer" //BearerToken defines the token_type `Bearer`, which is returned in a successful token response BearerToken = "Bearer" @@ -144,6 +149,72 @@ type AccessTokenResponse struct { IDToken string `json:"id_token,omitempty" schema:"id_token,omitempty"` } +type JWTTokenRequest struct { + Issuer string `json:"iss"` + Subject string `json:"sub"` + Scopes Scopes `json:"scope"` + Audience interface{} `json:"aud"` + IssuedAt Time `json:"iat"` + ExpiresAt Time `json:"exp"` +} + +func (j *JWTTokenRequest) GetClientID() string { + return j.Subject +} + +func (j *JWTTokenRequest) GetSubject() string { + return j.Subject +} + +func (j *JWTTokenRequest) GetScopes() []string { + return j.Scopes +} + +type Time time.Time + +func (t *Time) UnmarshalJSON(data []byte) error { + var i int64 + if err := json.Unmarshal(data, &i); err != nil { + return err + } + *t = Time(time.Unix(i, 0).UTC()) + return nil +} + +func (j *JWTTokenRequest) GetIssuer() string { + return j.Issuer +} + +func (j *JWTTokenRequest) GetAudience() []string { + return audienceFromJSON(j.Audience) +} + +func (j *JWTTokenRequest) GetExpiration() time.Time { + return time.Time(j.ExpiresAt) +} + +func (j *JWTTokenRequest) GetIssuedAt() time.Time { + return time.Time(j.IssuedAt) +} + +func (j *JWTTokenRequest) GetNonce() string { + return "" +} + +func (j *JWTTokenRequest) GetAuthenticationContextClassReference() string { + return "" +} + +func (j *JWTTokenRequest) GetAuthTime() time.Time { + return time.Time{} +} + +func (j *JWTTokenRequest) GetAuthorizedParty() string { + return "" +} + +func (j *JWTTokenRequest) SetSignature(algorithm jose.SignatureAlgorithm) {} + type TokenExchangeRequest struct { subjectToken string `schema:"subject_token"` subjectTokenType string `schema:"subject_token_type"` diff --git a/pkg/oidc/grants/tokenexchange/tokenexchange.go b/pkg/oidc/grants/tokenexchange/tokenexchange.go index 02a9808..9464605 100644 --- a/pkg/oidc/grants/tokenexchange/tokenexchange.go +++ b/pkg/oidc/grants/tokenexchange/tokenexchange.go @@ -22,6 +22,10 @@ type TokenExchangeRequest struct { requestedTokenType string `schema:"requested_token_type"` } +type JWTProfileRequest struct { + Assertion string `schema:"assertion"` +} + func NewTokenExchangeRequest(subjectToken, subjectTokenType string, opts ...TokenExchangeOption) *TokenExchangeRequest { t := &TokenExchangeRequest{ grantType: TokenExchangeGrantType, diff --git a/pkg/oidc/keyset.go b/pkg/oidc/keyset.go index f9bed2f..abe55d1 100644 --- a/pkg/oidc/keyset.go +++ b/pkg/oidc/keyset.go @@ -20,3 +20,13 @@ type KeySet interface { // use any HTTP client associated with the context through ClientContext. VerifySignature(ctx context.Context, jws *jose.JSONWebSignature) (payload []byte, err error) } + +func CheckKey(keyID string, jws *jose.JSONWebSignature, keys ...jose.JSONWebKey) ([]byte, error, bool) { + for _, key := range keys { + if keyID == "" || key.KeyID == keyID { + payload, err := jws.Verify(&key) + return payload, err, true + } + } + return nil, nil, false +} diff --git a/pkg/oidc/token.go b/pkg/oidc/token.go index a4b8a3d..21b0419 100644 --- a/pkg/oidc/token.go +++ b/pkg/oidc/token.go @@ -2,6 +2,7 @@ package oidc import ( "encoding/json" + "io/ioutil" "strings" "time" @@ -59,6 +60,47 @@ type IDTokenClaims struct { Signature jose.SignatureAlgorithm //TODO: ??? } +type JWTProfileAssertion struct { + PrivateKeyID string `json:"keyId"` + PrivateKey []byte `json:"key"` + Scopes []string `json:"-"` + Issuer string `json:"-"` + Subject string `json:"userId"` + Audience []string `json:"-"` + Expiration time.Time `json:"-"` + IssuedAt time.Time `json:"-"` +} + +func NewJWTProfileAssertionFromKeyJSON(filename string, audience []string) (*JWTProfileAssertion, error) { + data, err := ioutil.ReadFile(filename) + if err != nil { + return nil, err + } + keyData := new(struct { + KeyID string `json:"keyId"` + Key string `json:"key"` + UserID string `json:"userId"` + }) + err = json.Unmarshal(data, keyData) + if err != nil { + return nil, err + } + return NewJWTProfileAssertion(keyData.UserID, keyData.KeyID, audience, []byte(keyData.Key)), nil +} + +func NewJWTProfileAssertion(userID, keyID string, audience []string, key []byte) *JWTProfileAssertion { + return &JWTProfileAssertion{ + PrivateKey: key, + PrivateKeyID: keyID, + Issuer: userID, + Scopes: []string{ScopeOpenID}, + Subject: userID, + IssuedAt: time.Now().UTC(), + Expiration: time.Now().Add(1 * time.Hour).UTC(), + Audience: audience, + } +} + type jsonToken struct { Issuer string `json:"iss,omitempty"` Subject string `json:"sub,omitempty"` @@ -177,6 +219,70 @@ func (t *IDTokenClaims) UnmarshalJSON(b []byte) error { return nil } +func (t *IDTokenClaims) GetIssuer() string { + return t.Issuer +} + +func (t *IDTokenClaims) GetAudience() []string { + return t.Audiences +} + +func (t *IDTokenClaims) GetExpiration() time.Time { + return t.Expiration +} + +func (t *IDTokenClaims) GetIssuedAt() time.Time { + return t.IssuedAt +} + +func (t *IDTokenClaims) GetNonce() string { + return t.Nonce +} + +func (t *IDTokenClaims) GetAuthenticationContextClassReference() string { + return t.AuthenticationContextClassReference +} + +func (t *IDTokenClaims) GetAuthTime() time.Time { + return t.AuthTime +} + +func (t *IDTokenClaims) GetAuthorizedParty() string { + return t.AuthorizedParty +} + +func (t *IDTokenClaims) SetSignature(alg jose.SignatureAlgorithm) { + t.Signature = alg +} + +func (t *JWTProfileAssertion) MarshalJSON() ([]byte, error) { + j := jsonToken{ + Issuer: t.Issuer, + Subject: t.Subject, + Audiences: t.Audience, + Expiration: timeToJSON(t.Expiration), + IssuedAt: timeToJSON(t.IssuedAt), + Scopes: strings.Join(t.Scopes, " "), + } + return json.Marshal(j) +} + +func (t *JWTProfileAssertion) UnmarshalJSON(b []byte) error { + var j jsonToken + if err := json.Unmarshal(b, &j); err != nil { + return err + } + + t.Issuer = j.Issuer + t.Subject = j.Subject + t.Audience = audienceFromJSON(j.Audiences) + t.Expiration = time.Unix(j.Expiration, 0).UTC() + t.IssuedAt = time.Unix(j.IssuedAt, 0).UTC() + t.Scopes = strings.Split(j.Scopes, " ") + + return nil +} + func (j *jsonToken) UnmarshalUserinfoProfile() UserinfoProfile { locale, _ := language.Parse(j.Locale) return UserinfoProfile{ diff --git a/pkg/oidc/userinfo.go b/pkg/oidc/userinfo.go index d0fe4a8..eb670fb 100644 --- a/pkg/oidc/userinfo.go +++ b/pkg/oidc/userinfo.go @@ -105,7 +105,7 @@ func (i *Userinfo) UnmmarshalJSON(data []byte) error { if err := json.Unmarshal(data, i); err != nil { return err } - return json.Unmarshal(data, i.claims) + return json.Unmarshal(data, &i.claims) } type jsonUserinfo struct { diff --git a/pkg/oidc/verifier.go b/pkg/oidc/verifier.go new file mode 100644 index 0000000..492664b --- /dev/null +++ b/pkg/oidc/verifier.go @@ -0,0 +1,203 @@ +package oidc + +import ( + "bytes" + "context" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "strings" + "time" + + "gopkg.in/square/go-jose.v2" + + "github.com/caos/oidc/pkg/utils" +) + +type Claims interface { + GetIssuer() string + GetAudience() []string + GetExpiration() time.Time + GetIssuedAt() time.Time + GetNonce() string + GetAuthenticationContextClassReference() string + GetAuthTime() time.Time + GetAuthorizedParty() string + SetSignature(algorithm jose.SignatureAlgorithm) +} + +var ( + ErrParse = errors.New("parsing of request failed") + ErrIssuerInvalid = errors.New("issuer does not match") + ErrAudience = errors.New("audience is not valid") + ErrAzpMissing = errors.New("authorized party is not set. If Token is valid for multiple audiences, azp must not be empty") + ErrAzpInvalid = errors.New("authorized party is not valid") + ErrSignatureMissing = errors.New("id_token does not contain a signature") + ErrSignatureMultiple = errors.New("id_token contains multiple signatures") + ErrSignatureUnsupportedAlg = errors.New("signature algorithm not supported") + ErrSignatureInvalidPayload = errors.New("signature does not match Payload") + ErrExpired = errors.New("token has expired") + ErrIatInFuture = errors.New("issuedAt of token is in the future") + ErrIatToOld = errors.New("issuedAt of token is to old") + ErrNonceInvalid = errors.New("nonce does not match") + ErrAcrInvalid = errors.New("acr is invalid") + ErrAuthTimeNotPresent = errors.New("claim `auth_time` of token is missing") + ErrAuthTimeToOld = errors.New("auth time of token is to old") + ErrAtHash = errors.New("at_hash does not correspond to access token") +) + +type Verifier interface { + Issuer() string + MaxAgeIAT() time.Duration + Offset() time.Duration +} + +//ACRVerifier specifies the function to be used by the `DefaultVerifier` for validating the acr claim +type ACRVerifier func(string) error + +//DefaultACRVerifier implements `ACRVerifier` returning an error +//if non of the provided values matches the acr claim +func DefaultACRVerifier(possibleValues []string) ACRVerifier { + return func(acr string) error { + if !utils.Contains(possibleValues, acr) { + return fmt.Errorf("expected one of: %v, got: %q", possibleValues, acr) + } + return nil + } +} + +func DecryptToken(tokenString string) (string, error) { + return tokenString, nil //TODO: impl +} + +func ParseToken(tokenString string, claims interface{}) ([]byte, error) { + parts := strings.Split(tokenString, ".") + if len(parts) != 3 { + return nil, fmt.Errorf("%w: token contains an invalid number of segments", ErrParse) + } + payload, err := base64.RawURLEncoding.DecodeString(parts[1]) + if err != nil { + return nil, fmt.Errorf("%w: malformed jwt payload: %v", ErrParse, err) + } + err = json.Unmarshal(payload, claims) + return payload, err +} + +func CheckIssuer(claims Claims, issuer string) error { + if claims.GetIssuer() != issuer { + return fmt.Errorf("%w: Expected: %s, got: %s", ErrIssuerInvalid, issuer, claims.GetIssuer()) + } + return nil +} + +func CheckAudience(claims Claims, clientID string) error { + if !utils.Contains(claims.GetAudience(), clientID) { + return fmt.Errorf("%w: Audience must contain client_id %q", ErrAudience, clientID) + } + + //TODO: check aud trusted + return nil +} + +func CheckAuthorizedParty(claims Claims, clientID string) error { + if len(claims.GetAudience()) > 1 { + if claims.GetAuthorizedParty() == "" { + return ErrAzpMissing + } + } + if claims.GetAuthorizedParty() != "" && claims.GetAuthorizedParty() != clientID { + return fmt.Errorf("%w: azp %q must be equal to client_id %q", ErrAzpInvalid, claims.GetAuthorizedParty(), clientID) + } + return nil +} + +func CheckSignature(ctx context.Context, token string, payload []byte, claims Claims, supportedSigAlgs []string, set KeySet) error { + jws, err := jose.ParseSigned(token) + if err != nil { + return ErrParse + } + if len(jws.Signatures) == 0 { + return ErrSignatureMissing + } + if len(jws.Signatures) > 1 { + return ErrSignatureMultiple + } + sig := jws.Signatures[0] + if len(supportedSigAlgs) == 0 { + supportedSigAlgs = []string{"RS256"} + } + if !utils.Contains(supportedSigAlgs, sig.Header.Algorithm) { + return fmt.Errorf("%w: id token signed with unsupported algorithm, expected %q got %q", ErrSignatureUnsupportedAlg, supportedSigAlgs, sig.Header.Algorithm) + } + + signedPayload, err := set.VerifySignature(ctx, jws) + if err != nil { + return err + } + + if !bytes.Equal(signedPayload, payload) { + return ErrSignatureInvalidPayload + } + + claims.SetSignature(jose.SignatureAlgorithm(sig.Header.Algorithm)) + + return nil +} + +func CheckExpiration(claims Claims, offset time.Duration) error { + expiration := claims.GetExpiration().Round(time.Second) + if !time.Now().UTC().Add(offset).Before(expiration) { + return ErrExpired + } + return nil +} + +func CheckIssuedAt(claims Claims, maxAgeIAT, offset time.Duration) error { + issuedAt := claims.GetIssuedAt().Round(time.Second) + nowWithOffset := time.Now().UTC().Add(offset).Round(time.Second) + if issuedAt.After(nowWithOffset) { + return fmt.Errorf("%w: (iat: %v, now with offset: %v)", ErrIatInFuture, issuedAt, nowWithOffset) + } + if maxAgeIAT == 0 { + return nil + } + maxAge := time.Now().UTC().Add(-maxAgeIAT).Round(time.Second) + if issuedAt.Before(maxAge) { + return fmt.Errorf("%w: must not be older than %v, but was %v (%v to old)", ErrIatToOld, maxAge, issuedAt, maxAge.Sub(issuedAt)) + } + return nil +} + +func CheckNonce(claims Claims, nonce string) error { + if nonce == "" { + return nil + } + if claims.GetNonce() != nonce { + return fmt.Errorf("%w: expected %q but was %q", ErrNonceInvalid, nonce, claims.GetNonce()) + } + return nil +} + +func CheckAuthorizationContextClassReference(claims Claims, acr ACRVerifier) error { + if acr != nil { + if err := acr(claims.GetAuthenticationContextClassReference()); err != nil { + return fmt.Errorf("%w: %v", ErrAcrInvalid, err) + } + } + return nil +} +func CheckAuthTime(claims Claims, maxAge time.Duration) error { + if maxAge == 0 { + return nil + } + if claims.GetAuthTime().IsZero() { + return ErrAuthTimeNotPresent + } + authTime := claims.GetAuthTime().Round(time.Second) + maxAuthTime := time.Now().UTC().Add(-maxAge).Round(time.Second) + if authTime.Before(maxAuthTime) { + return fmt.Errorf("%w: must not be older than %v, but was %v (%v to old)", ErrAuthTimeToOld, maxAge, authTime, maxAuthTime.Sub(authTime)) + } + return nil +} diff --git a/pkg/op/authrequest.go b/pkg/op/authrequest.go index 743da68..cf40e62 100644 --- a/pkg/op/authrequest.go +++ b/pkg/op/authrequest.go @@ -9,7 +9,6 @@ import ( "github.com/gorilla/mux" "github.com/caos/oidc/pkg/oidc" - "github.com/caos/oidc/pkg/rp" "github.com/caos/oidc/pkg/utils" ) @@ -18,7 +17,7 @@ type Authorizer interface { Decoder() utils.Decoder Encoder() utils.Encoder Signer() Signer - IDTokenVerifier() rp.Verifier + IDTokenHintVerifier() IDTokenHintVerifier Crypto() Crypto Issuer() string } @@ -27,14 +26,20 @@ type Authorizer interface { //implementing it's own validation mechanism for the auth request type AuthorizeValidator interface { Authorizer - ValidateAuthRequest(context.Context, *oidc.AuthRequest, Storage, rp.Verifier) (string, error) + ValidateAuthRequest(context.Context, *oidc.AuthRequest, Storage, IDTokenHintVerifier) (string, error) } -//ValidationAuthorizer is an extension of Authorizer interface -//implementing it's own validation mechanism for the auth request -// -//Deprecated: ValidationAuthorizer exists for historical compatibility. Use ValidationAuthorizer itself -type ValidationAuthorizer AuthorizeValidator +func authorizeHandler(authorizer Authorizer) func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + Authorize(w, r, authorizer) + } +} + +func authorizeCallbackHandler(authorizer Authorizer) func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + AuthorizeCallback(w, r, authorizer) + } +} //Authorize handles the authorization request, including //parsing, validating, storing and finally redirecting to the login handler @@ -48,7 +53,7 @@ func Authorize(w http.ResponseWriter, r *http.Request, authorizer Authorizer) { if validater, ok := authorizer.(AuthorizeValidator); ok { validation = validater.ValidateAuthRequest } - userID, err := validation(r.Context(), authReq, authorizer.Storage(), authorizer.IDTokenVerifier()) + userID, err := validation(r.Context(), authReq, authorizer.Storage(), authorizer.IDTokenHintVerifier()) if err != nil { AuthRequestError(w, r, authReq, err, authorizer.Encoder()) return @@ -66,6 +71,7 @@ func Authorize(w http.ResponseWriter, r *http.Request, authorizer Authorizer) { RedirectToLogin(req.GetID(), client, w, r) } +//ParseAuthorizeRequest parsed the http request into a AuthRequest func ParseAuthorizeRequest(r *http.Request, decoder utils.Decoder) (*oidc.AuthRequest, error) { err := r.ParseForm() if err != nil { @@ -79,7 +85,8 @@ func ParseAuthorizeRequest(r *http.Request, decoder utils.Decoder) (*oidc.AuthRe return authReq, nil } -func ValidateAuthRequest(ctx context.Context, authReq *oidc.AuthRequest, storage Storage, verifier rp.Verifier) (string, error) { +//ValidateAuthRequest validates the authorize parameters and returns the userID of the id_token_hint if passed +func ValidateAuthRequest(ctx context.Context, authReq *oidc.AuthRequest, storage Storage, verifier IDTokenHintVerifier) (string, error) { client, err := storage.GetClientByClientID(ctx, authReq.ClientID) if err != nil { return "", ErrServerError(err.Error()) @@ -96,6 +103,7 @@ func ValidateAuthRequest(ctx context.Context, authReq *oidc.AuthRequest, storage return ValidateAuthReqIDTokenHint(ctx, authReq.IDTokenHint, verifier) } +//ValidateAuthReqScopes validates the passed scopes func ValidateAuthReqScopes(scopes []string) error { if len(scopes) == 0 { return ErrInvalidRequest("The scope of your request is missing. Please ensure some scopes are requested. If you have any questions, you may contact the administrator of the application.") @@ -106,6 +114,7 @@ func ValidateAuthReqScopes(scopes []string) error { return nil } +//ValidateAuthReqRedirectURI validates the passed redirect_uri and response_type to the registered uris and client type func ValidateAuthReqRedirectURI(client Client, uri string, responseType oidc.ResponseType) error { if uri == "" { return ErrInvalidRequestRedirectURI("The redirect_uri is missing in the request. Please ensure it is added to the request. If you have any questions, you may contact the administrator of the application.") @@ -138,6 +147,7 @@ func ValidateAuthReqRedirectURI(client Client, uri string, responseType oidc.Res return nil } +//ValidateAuthReqResponseType validates the passed response_type to the registered response types func ValidateAuthReqResponseType(client Client, responseType oidc.ResponseType) error { if responseType == "" { return ErrInvalidRequest("The response type is missing in your request. If you have any questions, you may contact the administrator of the application.") @@ -148,22 +158,26 @@ func ValidateAuthReqResponseType(client Client, responseType oidc.ResponseType) return nil } -func ValidateAuthReqIDTokenHint(ctx context.Context, idTokenHint string, verifier rp.Verifier) (string, error) { +//ValidateAuthReqIDTokenHint validates the id_token_hint (if passed as parameter in the request) +//and returns the `sub` claim +func ValidateAuthReqIDTokenHint(ctx context.Context, idTokenHint string, verifier IDTokenHintVerifier) (string, error) { if idTokenHint == "" { return "", nil } - claims, err := verifier.VerifyIDToken(ctx, idTokenHint) + claims, err := VerifyIDTokenHint(ctx, idTokenHint, verifier) if err != nil { return "", ErrInvalidRequest("The id_token_hint is invalid. If you have any questions, you may contact the administrator of the application.") } return claims.Subject, nil } +//RedirectToLogin redirects the end user to the Login UI for authentication func RedirectToLogin(authReqID string, client Client, w http.ResponseWriter, r *http.Request) { login := client.LoginURL(authReqID) http.Redirect(w, r, login, http.StatusFound) } +//AuthorizeCallback handles the callback after authentication in the Login UI func AuthorizeCallback(w http.ResponseWriter, r *http.Request, authorizer Authorizer) { params := mux.Vars(r) id := params["id"] @@ -180,19 +194,21 @@ func AuthorizeCallback(w http.ResponseWriter, r *http.Request, authorizer Author AuthResponse(authReq, authorizer, w, r) } +//AuthResponse creates the successful authentication response (either code or tokens) func AuthResponse(authReq AuthRequest, authorizer Authorizer, w http.ResponseWriter, r *http.Request) { client, err := authorizer.Storage().GetClientByClientID(r.Context(), authReq.GetClientID()) if err != nil { - + AuthRequestError(w, r, authReq, err, authorizer.Encoder()) + return } if authReq.GetResponseType() == oidc.ResponseTypeCode { AuthResponseCode(w, r, authReq, authorizer) return } AuthResponseToken(w, r, authReq, authorizer, client) - return } +//AuthResponseCode creates the successful code authentication response func AuthResponseCode(w http.ResponseWriter, r *http.Request, authReq AuthRequest, authorizer Authorizer) { code, err := CreateAuthRequestCode(r.Context(), authReq, authorizer.Storage(), authorizer.Crypto()) if err != nil { @@ -206,6 +222,7 @@ func AuthResponseCode(w http.ResponseWriter, r *http.Request, authReq AuthReques http.Redirect(w, r, callback, http.StatusFound) } +//AuthResponseToken creates the successful token(s) authentication response func AuthResponseToken(w http.ResponseWriter, r *http.Request, authReq AuthRequest, authorizer Authorizer, client Client) { createAccessToken := authReq.GetResponseType() != oidc.ResponseTypeIDTokenOnly resp, err := CreateTokenResponse(r.Context(), authReq, client, authorizer, createAccessToken, "") @@ -222,6 +239,7 @@ func AuthResponseToken(w http.ResponseWriter, r *http.Request, authReq AuthReque http.Redirect(w, r, callback, http.StatusFound) } +//CreateAuthRequestCode creates and stores a code for the auth code response func CreateAuthRequestCode(ctx context.Context, authReq AuthRequest, storage Storage, crypto Crypto) (string, error) { code, err := BuildAuthRequestCode(authReq, crypto) if err != nil { diff --git a/pkg/op/authrequest_test.go b/pkg/op/authrequest_test.go index 343924f..d74d365 100644 --- a/pkg/op/authrequest_test.go +++ b/pkg/op/authrequest_test.go @@ -13,7 +13,6 @@ import ( "github.com/caos/oidc/pkg/oidc" "github.com/caos/oidc/pkg/op" "github.com/caos/oidc/pkg/op/mock" - "github.com/caos/oidc/pkg/rp" "github.com/caos/oidc/pkg/utils" ) @@ -145,7 +144,7 @@ func TestValidateAuthRequest(t *testing.T) { type args struct { authRequest *oidc.AuthRequest storage op.Storage - verifier rp.Verifier + verifier op.IDTokenHintVerifier } tests := []struct { name string diff --git a/pkg/op/client.go b/pkg/op/client.go index ef9b62e..3184b90 100644 --- a/pkg/op/client.go +++ b/pkg/op/client.go @@ -15,6 +15,12 @@ const ( AccessTokenTypeJWT ) +type ApplicationType int + +type AuthMethod string + +type AccessTokenType int + type Client interface { GetID() string RedirectURIs() []string @@ -28,10 +34,6 @@ type Client interface { DevMode() bool } -func IsConfidentialType(c Client) bool { - return c.ApplicationType() == ApplicationTypeWeb -} - func ContainsResponseType(types []oidc.ResponseType, responseType oidc.ResponseType) bool { for _, t := range types { if t == responseType { @@ -41,8 +43,6 @@ func ContainsResponseType(types []oidc.ResponseType, responseType oidc.ResponseT return false } -type ApplicationType int - -type AuthMethod string - -type AccessTokenType int +func IsConfidentialType(c Client) bool { + return c.ApplicationType() == ApplicationTypeWeb +} diff --git a/pkg/op/default_op.go b/pkg/op/default_op.go deleted file mode 100644 index 9d18dd0..0000000 --- a/pkg/op/default_op.go +++ /dev/null @@ -1,353 +0,0 @@ -package op - -import ( - "context" - "errors" - "net/http" - "time" - - "github.com/gorilla/schema" - "gopkg.in/square/go-jose.v2" - - "github.com/caos/logging" - - "github.com/caos/oidc/pkg/oidc" - "github.com/caos/oidc/pkg/rp" - "github.com/caos/oidc/pkg/utils" -) - -const ( - defaultAuthorizationEndpoint = "authorize" - defaulTokenEndpoint = "oauth/token" - defaultIntrospectEndpoint = "introspect" - defaultUserinfoEndpoint = "userinfo" - defaultEndSessionEndpoint = "end_session" - defaultKeysEndpoint = "keys" - - AuthMethodBasic AuthMethod = "client_secret_basic" - AuthMethodPost = "client_secret_post" - AuthMethodNone = "none" - - CodeMethodS256 = "S256" -) - -var ( - DefaultEndpoints = &endpoints{ - Authorization: NewEndpoint(defaultAuthorizationEndpoint), - Token: NewEndpoint(defaulTokenEndpoint), - Introspection: NewEndpoint(defaultIntrospectEndpoint), - Userinfo: NewEndpoint(defaultUserinfoEndpoint), - EndSession: NewEndpoint(defaultEndSessionEndpoint), - JwksURI: NewEndpoint(defaultKeysEndpoint), - } -) - -type DefaultOP struct { - config *Config - endpoints *endpoints - storage Storage - signer Signer - verifier rp.Verifier - crypto Crypto - http http.Handler - decoder *schema.Decoder - encoder *schema.Encoder - interceptors []HttpInterceptor - retry func(int) (bool, int) - timer <-chan time.Time -} - -type Config struct { - Issuer string - CryptoKey [32]byte - DefaultLogoutRedirectURI string - CodeMethodS256 bool - // ScopesSupported: oidc.SupportedScopes, - // ResponseTypesSupported: responseTypes, - // GrantTypesSupported: oidc.SupportedGrantTypes, - // ClaimsSupported: oidc.SupportedClaims, - // IdTokenSigningAlgValuesSupported: []string{keys.SigningAlgorithm}, - // SubjectTypesSupported: []string{"public"}, - // TokenEndpointAuthMethodsSupported: -} - -type endpoints struct { - Authorization Endpoint - Token Endpoint - Introspection Endpoint - Userinfo Endpoint - EndSession Endpoint - CheckSessionIframe Endpoint - JwksURI Endpoint -} - -type DefaultOPOpts func(o *DefaultOP) error - -func WithCustomAuthEndpoint(endpoint Endpoint) DefaultOPOpts { - return func(o *DefaultOP) error { - if err := endpoint.Validate(); err != nil { - return err - } - o.endpoints.Authorization = endpoint - return nil - } -} - -func WithCustomTokenEndpoint(endpoint Endpoint) DefaultOPOpts { - return func(o *DefaultOP) error { - if err := endpoint.Validate(); err != nil { - return err - } - o.endpoints.Token = endpoint - return nil - } -} - -func WithCustomUserinfoEndpoint(endpoint Endpoint) DefaultOPOpts { - return func(o *DefaultOP) error { - if err := endpoint.Validate(); err != nil { - return err - } - o.endpoints.Userinfo = endpoint - return nil - } -} - -func WithCustomEndSessionEndpoint(endpoint Endpoint) DefaultOPOpts { - return func(o *DefaultOP) error { - if err := endpoint.Validate(); err != nil { - return err - } - o.endpoints.EndSession = endpoint - return nil - } -} - -func WithCustomKeysEndpoint(endpoint Endpoint) DefaultOPOpts { - return func(o *DefaultOP) error { - if err := endpoint.Validate(); err != nil { - return err - } - o.endpoints.JwksURI = endpoint - return nil - } -} - -func WithHttpInterceptors(interceptors ...HttpInterceptor) DefaultOPOpts { - return func(o *DefaultOP) error { - o.interceptors = append(o.interceptors, interceptors...) - return nil - } -} - -func WithRetry(max int, sleep time.Duration) DefaultOPOpts { - return func(o *DefaultOP) error { - o.retry = func(count int) (bool, int) { - count++ - if count == max { - return false, count - } - time.Sleep(sleep) - return true, count - } - return nil - } -} - -func WithTimer(timer <-chan time.Time) DefaultOPOpts { - return func(o *DefaultOP) error { - o.timer = timer - return nil - } -} - -func NewDefaultOP(ctx context.Context, config *Config, storage Storage, opOpts ...DefaultOPOpts) (OpenIDProvider, error) { - err := ValidateIssuer(config.Issuer) - if err != nil { - return nil, err - } - - p := &DefaultOP{ - config: config, - storage: storage, - endpoints: DefaultEndpoints, - timer: make(<-chan time.Time), - } - - for _, optFunc := range opOpts { - if err := optFunc(p); err != nil { - return nil, err - } - } - - keyCh := make(chan jose.SigningKey) - p.signer = NewDefaultSigner(ctx, storage, keyCh) - go p.ensureKey(ctx, storage, keyCh, p.timer) - - p.verifier = rp.NewDefaultVerifier(config.Issuer, "", p, rp.WithIgnoreAudience(), rp.WithIgnoreExpiration()) - - p.http = CreateRouter(p, p.interceptors...) - - p.decoder = schema.NewDecoder() - p.decoder.IgnoreUnknownKeys(true) - - p.encoder = schema.NewEncoder() - - p.crypto = NewAESCrypto(config.CryptoKey) - - return p, nil -} - -func (p *DefaultOP) Issuer() string { - return p.config.Issuer -} - -func (p *DefaultOP) AuthorizationEndpoint() Endpoint { - return p.endpoints.Authorization -} - -func (p *DefaultOP) TokenEndpoint() Endpoint { - return Endpoint(p.endpoints.Token) -} - -func (p *DefaultOP) UserinfoEndpoint() Endpoint { - return Endpoint(p.endpoints.Userinfo) -} - -func (p *DefaultOP) EndSessionEndpoint() Endpoint { - return Endpoint(p.endpoints.EndSession) -} - -func (p *DefaultOP) KeysEndpoint() Endpoint { - return Endpoint(p.endpoints.JwksURI) -} - -func (p *DefaultOP) AuthMethodPostSupported() bool { - return true //TODO: config -} - -func (p *DefaultOP) CodeMethodS256Supported() bool { - return p.config.CodeMethodS256 -} - -func (p *DefaultOP) HttpHandler() http.Handler { - return p.http -} - -func (p *DefaultOP) HandleDiscovery(w http.ResponseWriter, r *http.Request) { - Discover(w, CreateDiscoveryConfig(p, p.Signer())) -} - -func (p *DefaultOP) VerifySignature(ctx context.Context, jws *jose.JSONWebSignature) ([]byte, error) { - keyID := "" - for _, sig := range jws.Signatures { - keyID = sig.Header.KeyID - break - } - keySet, err := p.Storage().GetKeySet(ctx) - if err != nil { - return nil, errors.New("error fetching keys") - } - payload, err, ok := rp.CheckKey(keyID, keySet.Keys, jws) - if !ok { - return nil, errors.New("invalid kid") - } - return payload, err -} - -func (p *DefaultOP) Decoder() utils.Decoder { - return p.decoder -} - -func (p *DefaultOP) Encoder() utils.Encoder { - return p.encoder -} - -func (p *DefaultOP) Storage() Storage { - return p.storage -} - -func (p *DefaultOP) Signer() Signer { - return p.signer -} - -func (p *DefaultOP) Crypto() Crypto { - return p.crypto -} -func (p *DefaultOP) HandleReady(w http.ResponseWriter, r *http.Request) { - probes := []ProbesFn{ - ReadySigner(p.Signer()), - ReadyStorage(p.Storage()), - } - Readiness(w, r, probes...) -} - -func (p *DefaultOP) HandleKeys(w http.ResponseWriter, r *http.Request) { - Keys(w, r, p) -} - -func (p *DefaultOP) HandleAuthorize(w http.ResponseWriter, r *http.Request) { - Authorize(w, r, p) -} - -func (p *DefaultOP) HandleAuthorizeCallback(w http.ResponseWriter, r *http.Request) { - AuthorizeCallback(w, r, p) -} - -func (p *DefaultOP) HandleExchange(w http.ResponseWriter, r *http.Request) { - reqType := r.FormValue("grant_type") - if reqType == "" { - RequestError(w, r, ErrInvalidRequest("grant_type missing")) - return - } - if reqType == string(oidc.GrantTypeCode) { - CodeExchange(w, r, p) - return - } - TokenExchange(w, r, p) -} - -func (p *DefaultOP) HandleUserinfo(w http.ResponseWriter, r *http.Request) { - Userinfo(w, r, p) -} - -func (p *DefaultOP) HandleEndSession(w http.ResponseWriter, r *http.Request) { - EndSession(w, r, p) -} - -func (p *DefaultOP) DefaultLogoutRedirectURI() string { - return p.config.DefaultLogoutRedirectURI -} -func (p *DefaultOP) IDTokenVerifier() rp.Verifier { - return p.verifier -} - -func (p *DefaultOP) ensureKey(ctx context.Context, storage Storage, keyCh chan<- jose.SigningKey, timer <-chan time.Time) { - count := 0 - timer = time.After(0) - errCh := make(chan error) - go storage.GetSigningKey(ctx, keyCh, errCh, timer) - for { - select { - case <-ctx.Done(): - return - case err := <-errCh: - if err == nil { - continue - } - _, ok := err.(StorageNotFoundError) - if ok { - err := storage.SaveNewKeyPair(ctx) - if err == nil { - continue - } - } - ok, count = p.retry(count) - if ok { - timer = time.After(0) - continue - } - logging.Log("OP-n6ynVE").WithError(err).Panic("error in key signer") - } - } -} diff --git a/pkg/op/discovery.go b/pkg/op/discovery.go index 54c473b..7611090 100644 --- a/pkg/op/discovery.go +++ b/pkg/op/discovery.go @@ -7,6 +7,12 @@ import ( "github.com/caos/oidc/pkg/utils" ) +func discoveryHandler(c Configuration, s Signer) func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + Discover(w, CreateDiscoveryConfig(c, s)) + } +} + func Discover(w http.ResponseWriter, config *oidc.DiscoveryConfiguration) { utils.MarshalJSON(w, config) } @@ -32,20 +38,12 @@ func CreateDiscoveryConfig(c Configuration, s Signer) *oidc.DiscoveryConfigurati } } -const ( - ScopeOpenID = "openid" - ScopeProfile = "profile" - ScopeEmail = "email" - ScopePhone = "phone" - ScopeAddress = "address" -) - var DefaultSupportedScopes = []string{ - ScopeOpenID, - ScopeProfile, - ScopeEmail, - ScopePhone, - ScopeAddress, + oidc.ScopeOpenID, + oidc.ScopeProfile, + oidc.ScopeEmail, + oidc.ScopePhone, + oidc.ScopeAddress, } func Scopes(c Configuration) []string { diff --git a/pkg/op/keys.go b/pkg/op/keys.go index 8e2052b..4b8d607 100644 --- a/pkg/op/keys.go +++ b/pkg/op/keys.go @@ -10,10 +10,18 @@ type KeyProvider interface { Storage() Storage } +func keysHandler(k KeyProvider) func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + Keys(w, r, k) + } +} + func Keys(w http.ResponseWriter, r *http.Request, k KeyProvider) { keySet, err := k.Storage().GetKeySet(r.Context()) if err != nil { - + w.WriteHeader(http.StatusInternalServerError) + utils.MarshalJSON(w, err) + return } utils.MarshalJSON(w, keySet) } diff --git a/pkg/op/mock/authorizer.mock.go b/pkg/op/mock/authorizer.mock.go index 5272997..5da2437 100644 --- a/pkg/op/mock/authorizer.mock.go +++ b/pkg/op/mock/authorizer.mock.go @@ -6,7 +6,6 @@ package mock import ( op "github.com/caos/oidc/pkg/op" - rp "github.com/caos/oidc/pkg/rp" utils "github.com/caos/oidc/pkg/utils" gomock "github.com/golang/mock/gomock" reflect "reflect" @@ -77,18 +76,18 @@ func (mr *MockAuthorizerMockRecorder) Encoder() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Encoder", reflect.TypeOf((*MockAuthorizer)(nil).Encoder)) } -// IDTokenVerifier mocks base method -func (m *MockAuthorizer) IDTokenVerifier() rp.Verifier { +// IDTokenHintVerifier mocks base method +func (m *MockAuthorizer) IDTokenHintVerifier() op.IDTokenHintVerifier { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "IDTokenVerifier") - ret0, _ := ret[0].(rp.Verifier) + ret := m.ctrl.Call(m, "IDTokenHintVerifier") + ret0, _ := ret[0].(op.IDTokenHintVerifier) return ret0 } -// IDTokenVerifier indicates an expected call of IDTokenVerifier -func (mr *MockAuthorizerMockRecorder) IDTokenVerifier() *gomock.Call { +// IDTokenHintVerifier indicates an expected call of IDTokenHintVerifier +func (mr *MockAuthorizerMockRecorder) IDTokenHintVerifier() *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IDTokenVerifier", reflect.TypeOf((*MockAuthorizer)(nil).IDTokenVerifier)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IDTokenHintVerifier", reflect.TypeOf((*MockAuthorizer)(nil).IDTokenHintVerifier)) } // Issuer mocks base method diff --git a/pkg/op/mock/authorizer.mock.impl.go b/pkg/op/mock/authorizer.mock.impl.go index 202889c..0a6b6a5 100644 --- a/pkg/op/mock/authorizer.mock.impl.go +++ b/pkg/op/mock/authorizer.mock.impl.go @@ -10,7 +10,6 @@ import ( "github.com/caos/oidc/pkg/oidc" "github.com/caos/oidc/pkg/op" - "github.com/caos/oidc/pkg/rp" ) func NewAuthorizer(t *testing.T) op.Authorizer { @@ -58,9 +57,9 @@ func ExpectSigner(a op.Authorizer, t *testing.T) { func ExpectVerifier(a op.Authorizer, t *testing.T) { mockA := a.(*MockAuthorizer) - mockA.EXPECT().IDTokenVerifier().DoAndReturn( - func() rp.Verifier { - return &Verifier{} + mockA.EXPECT().IDTokenHintVerifier().DoAndReturn( + func() op.IDTokenHintVerifier { + return op.NewIDTokenHintVerifier("", nil) }) } diff --git a/pkg/op/mock/storage.mock.go b/pkg/op/mock/storage.mock.go index 9432616..a7ca4cb 100644 --- a/pkg/op/mock/storage.mock.go +++ b/pkg/op/mock/storage.mock.go @@ -97,7 +97,7 @@ func (mr *MockStorageMockRecorder) CreateAuthRequest(arg0, arg1, arg2 interface{ } // CreateToken mocks base method -func (m *MockStorage) CreateToken(arg0 context.Context, arg1 op.AuthRequest) (string, time.Time, error) { +func (m *MockStorage) CreateToken(arg0 context.Context, arg1 op.TokenRequest) (string, time.Time, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "CreateToken", arg0, arg1) ret0, _ := ret[0].(string) @@ -141,6 +141,21 @@ func (mr *MockStorageMockRecorder) GetClientByClientID(arg0, arg1 interface{}) * return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetClientByClientID", reflect.TypeOf((*MockStorage)(nil).GetClientByClientID), arg0, arg1) } +// GetKeyByIDAndUserID mocks base method +func (m *MockStorage) GetKeyByIDAndUserID(arg0 context.Context, arg1, arg2 string) (*jose.JSONWebKey, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetKeyByIDAndUserID", arg0, arg1, arg2) + ret0, _ := ret[0].(*jose.JSONWebKey) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetKeyByIDAndUserID indicates an expected call of GetKeyByIDAndUserID +func (mr *MockStorageMockRecorder) GetKeyByIDAndUserID(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetKeyByIDAndUserID", reflect.TypeOf((*MockStorage)(nil).GetKeyByIDAndUserID), arg0, arg1, arg2) +} + // GetKeySet mocks base method func (m *MockStorage) GetKeySet(arg0 context.Context) (*jose.JSONWebKeySet, error) { m.ctrl.T.Helper() diff --git a/pkg/op/op.go b/pkg/op/op.go index 624a8a1..d913c7f 100644 --- a/pkg/op/op.go +++ b/pkg/op/op.go @@ -1,29 +1,60 @@ package op import ( + "context" + "errors" "net/http" + "time" + "github.com/caos/logging" "github.com/gorilla/handlers" "github.com/gorilla/mux" + "github.com/gorilla/schema" + "gopkg.in/square/go-jose.v2" "github.com/caos/oidc/pkg/oidc" + "github.com/caos/oidc/pkg/utils" ) const ( - healthzEndpoint = "/healthz" - readinessEndpoint = "/ready" + healthzEndpoint = "/healthz" + readinessEndpoint = "/ready" + defaultAuthorizationEndpoint = "authorize" + defaulTokenEndpoint = "oauth/token" + defaultIntrospectEndpoint = "introspect" + defaultUserinfoEndpoint = "userinfo" + defaultEndSessionEndpoint = "end_session" + defaultKeysEndpoint = "keys" + + AuthMethodBasic AuthMethod = "client_secret_basic" + AuthMethodPost AuthMethod = "client_secret_post" + AuthMethodNone AuthMethod = "none" + + CodeMethodS256 = "S256" +) + +var ( + DefaultEndpoints = &endpoints{ + Authorization: NewEndpoint(defaultAuthorizationEndpoint), + Token: NewEndpoint(defaulTokenEndpoint), + Introspection: NewEndpoint(defaultIntrospectEndpoint), + Userinfo: NewEndpoint(defaultUserinfoEndpoint), + EndSession: NewEndpoint(defaultEndSessionEndpoint), + JwksURI: NewEndpoint(defaultKeysEndpoint), + } ) type OpenIDProvider interface { Configuration - HandleReady(w http.ResponseWriter, r *http.Request) - HandleDiscovery(w http.ResponseWriter, r *http.Request) - HandleAuthorize(w http.ResponseWriter, r *http.Request) - HandleAuthorizeCallback(w http.ResponseWriter, r *http.Request) - HandleExchange(w http.ResponseWriter, r *http.Request) - HandleUserinfo(w http.ResponseWriter, r *http.Request) - HandleEndSession(w http.ResponseWriter, r *http.Request) - HandleKeys(w http.ResponseWriter, r *http.Request) + Storage() Storage + Decoder() utils.Decoder + Encoder() utils.Encoder + IDTokenHintVerifier() IDTokenHintVerifier + JWTProfileVerifier() JWTProfileVerifier + Crypto() Crypto + DefaultLogoutRedirectURI() string + Signer() Signer + Probes() []ProbesFn HttpHandler() http.Handler } @@ -41,18 +72,320 @@ func CreateRouter(o OpenIDProvider, interceptors ...HttpInterceptor) *mux.Router handlers.AllowedHeaders([]string{"authorization", "content-type"}), handlers.AllowedOriginValidator(allowAllOrigins), )) - router.HandleFunc(healthzEndpoint, Healthz) - router.HandleFunc(readinessEndpoint, o.HandleReady) - router.HandleFunc(oidc.DiscoveryEndpoint, o.HandleDiscovery) - router.Handle(o.AuthorizationEndpoint().Relative(), intercept(o.HandleAuthorize)) - router.Handle(o.AuthorizationEndpoint().Relative()+"/{id}", intercept(o.HandleAuthorizeCallback)) - router.Handle(o.TokenEndpoint().Relative(), intercept(o.HandleExchange)) - router.HandleFunc(o.UserinfoEndpoint().Relative(), o.HandleUserinfo) - router.Handle(o.EndSessionEndpoint().Relative(), intercept(o.HandleEndSession)) - router.HandleFunc(o.KeysEndpoint().Relative(), o.HandleKeys) + router.HandleFunc(healthzEndpoint, healthzHandler) + router.HandleFunc(readinessEndpoint, readyHandler(o.Probes())) + router.HandleFunc(oidc.DiscoveryEndpoint, discoveryHandler(o, o.Signer())) + router.Handle(o.AuthorizationEndpoint().Relative(), intercept(authorizeHandler(o))) + router.Handle(o.AuthorizationEndpoint().Relative()+"/{id}", intercept(authorizeCallbackHandler(o))) + router.Handle(o.TokenEndpoint().Relative(), intercept(tokenHandler(o))) + router.HandleFunc(o.UserinfoEndpoint().Relative(), userinfoHandler(o)) + router.Handle(o.EndSessionEndpoint().Relative(), intercept(endSessionHandler(o))) + router.HandleFunc(o.KeysEndpoint().Relative(), keysHandler(o)) return router } +type Config struct { + Issuer string + CryptoKey [32]byte + DefaultLogoutRedirectURI string + CodeMethodS256 bool + + //TODO: add to config after updating Configuration interface for DiscoveryConfig + // ScopesSupported: oidc.SupportedScopes, + // ResponseTypesSupported: responseTypes, + // GrantTypesSupported: oidc.SupportedGrantTypes, + // ClaimsSupported: oidc.SupportedClaims, + // IdTokenSigningAlgValuesSupported: []string{keys.SigningAlgorithm}, + // SubjectTypesSupported: []string{"public"}, + // TokenEndpointAuthMethodsSupported: +} + +type endpoints struct { + Authorization Endpoint + Token Endpoint + Introspection Endpoint + Userinfo Endpoint + EndSession Endpoint + CheckSessionIframe Endpoint + JwksURI Endpoint +} + +func NewOpenIDProvider(ctx context.Context, config *Config, storage Storage, opOpts ...Option) (OpenIDProvider, error) { + err := ValidateIssuer(config.Issuer) + if err != nil { + return nil, err + } + + o := &openidProvider{ + config: config, + storage: storage, + endpoints: DefaultEndpoints, + timer: make(<-chan time.Time), + } + + for _, optFunc := range opOpts { + if err := optFunc(o); err != nil { + return nil, err + } + } + + keyCh := make(chan jose.SigningKey) + o.signer = NewDefaultSigner(ctx, storage, keyCh) + go EnsureKey(ctx, storage, keyCh, o.timer, o.retry) + + o.httpHandler = CreateRouter(o, o.interceptors...) + + o.decoder = schema.NewDecoder() + o.decoder.IgnoreUnknownKeys(true) + + o.encoder = schema.NewEncoder() + + o.crypto = NewAESCrypto(config.CryptoKey) + + return o, nil +} + +type openidProvider struct { + config *Config + endpoints *endpoints + storage Storage + signer Signer + idTokenHintVerifier IDTokenHintVerifier + jwtProfileVerifier JWTProfileVerifier + crypto Crypto + httpHandler http.Handler + decoder *schema.Decoder + encoder *schema.Encoder + interceptors []HttpInterceptor + retry func(int) (bool, int) + timer <-chan time.Time +} + +func (o *openidProvider) Issuer() string { + return o.config.Issuer +} + +func (o *openidProvider) AuthorizationEndpoint() Endpoint { + return o.endpoints.Authorization +} + +func (o *openidProvider) TokenEndpoint() Endpoint { + return o.endpoints.Token +} + +func (o *openidProvider) UserinfoEndpoint() Endpoint { + return o.endpoints.Userinfo +} + +func (o *openidProvider) EndSessionEndpoint() Endpoint { + return o.endpoints.EndSession +} + +func (o *openidProvider) KeysEndpoint() Endpoint { + return o.endpoints.JwksURI +} + +func (o *openidProvider) AuthMethodPostSupported() bool { + return true //todo: config +} + +func (o *openidProvider) CodeMethodS256Supported() bool { + return o.config.CodeMethodS256 +} + +func (o *openidProvider) Storage() Storage { + return o.storage +} + +func (o *openidProvider) Decoder() utils.Decoder { + return o.decoder +} + +func (o *openidProvider) Encoder() utils.Encoder { + return o.encoder +} + +func (o *openidProvider) IDTokenHintVerifier() IDTokenHintVerifier { + if o.idTokenHintVerifier == nil { + o.idTokenHintVerifier = NewIDTokenHintVerifier(o.Issuer(), &openIDKeySet{o.Storage()}) + } + return o.idTokenHintVerifier +} + +func (o *openidProvider) JWTProfileVerifier() JWTProfileVerifier { + if o.jwtProfileVerifier == nil { + o.jwtProfileVerifier = NewJWTProfileVerifier(o.Storage(), o.Issuer(), 1*time.Hour, time.Second) + } + return o.jwtProfileVerifier +} + +func (o *openidProvider) Crypto() Crypto { + return o.crypto +} + +func (o *openidProvider) DefaultLogoutRedirectURI() string { + return o.config.DefaultLogoutRedirectURI +} + +func (o *openidProvider) Signer() Signer { + return o.signer +} + +func (o *openidProvider) Probes() []ProbesFn { + return []ProbesFn{ + ReadySigner(o.Signer()), + ReadyStorage(o.Storage()), + } +} + +func (o *openidProvider) HttpHandler() http.Handler { + return o.httpHandler +} + +type openIDKeySet struct { + Storage +} + +//VerifySignature implements the oidc.KeySet interface +//providing an implementation for the keys stored in the OP Storage interface +func (o *openIDKeySet) VerifySignature(ctx context.Context, jws *jose.JSONWebSignature) ([]byte, error) { + keyID := "" + for _, sig := range jws.Signatures { + keyID = sig.Header.KeyID + break + } + keySet, err := o.Storage.GetKeySet(ctx) + if err != nil { + return nil, errors.New("error fetching keys") + } + payload, err, ok := oidc.CheckKey(keyID, jws, keySet.Keys...) + if !ok { + return nil, errors.New("invalid kid") + } + return payload, err +} + +func EnsureKey(ctx context.Context, storage Storage, keyCh chan<- jose.SigningKey, timer <-chan time.Time, retry func(int) (bool, int)) { + count := 0 + timer = time.After(0) + errCh := make(chan error) + go storage.GetSigningKey(ctx, keyCh, errCh, timer) + for { + select { + case <-ctx.Done(): + return + case err := <-errCh: + if err == nil { + continue + } + _, ok := err.(StorageNotFoundError) + if ok { + err := storage.SaveNewKeyPair(ctx) + if err == nil { + continue + } + } + ok, count = retry(count) + if ok { + timer = time.After(0) + continue + } + logging.Log("OP-n6ynVE").WithError(err).Panic("error in key signer") + } + } +} + +type Option func(o *openidProvider) error + +func WithCustomAuthEndpoint(endpoint Endpoint) Option { + return func(o *openidProvider) error { + if err := endpoint.Validate(); err != nil { + return err + } + o.endpoints.Authorization = endpoint + return nil + } +} + +func WithCustomTokenEndpoint(endpoint Endpoint) Option { + return func(o *openidProvider) error { + if err := endpoint.Validate(); err != nil { + return err + } + o.endpoints.Token = endpoint + return nil + } +} + +func WithCustomUserinfoEndpoint(endpoint Endpoint) Option { + return func(o *openidProvider) error { + if err := endpoint.Validate(); err != nil { + return err + } + o.endpoints.Userinfo = endpoint + return nil + } +} + +func WithCustomEndSessionEndpoint(endpoint Endpoint) Option { + return func(o *openidProvider) error { + if err := endpoint.Validate(); err != nil { + return err + } + o.endpoints.EndSession = endpoint + return nil + } +} + +func WithCustomKeysEndpoint(endpoint Endpoint) Option { + return func(o *openidProvider) error { + if err := endpoint.Validate(); err != nil { + return err + } + o.endpoints.JwksURI = endpoint + return nil + } +} + +func WithCustomEndpoints(auth, token, userInfo, endSession, keys Endpoint) Option { + return func(o *openidProvider) error { + o.endpoints.Authorization = auth + o.endpoints.Token = token + o.endpoints.Userinfo = userInfo + o.endpoints.EndSession = endSession + o.endpoints.JwksURI = keys + return nil + } +} + +func WithHttpInterceptors(interceptors ...HttpInterceptor) Option { + return func(o *openidProvider) error { + o.interceptors = append(o.interceptors, interceptors...) + return nil + } +} + +func WithRetry(max int, sleep time.Duration) Option { + return func(o *openidProvider) error { + o.retry = func(count int) (bool, int) { + count++ + if count == max { + return false, count + } + time.Sleep(sleep) + return true, count + } + return nil + } +} + +func WithTimer(timer <-chan time.Time) Option { + return func(o *openidProvider) error { + o.timer = timer + return nil + } +} + func buildInterceptor(interceptors ...HttpInterceptor) func(http.HandlerFunc) http.Handler { return func(handlerFunc http.HandlerFunc) http.Handler { handler := handlerFuncToHandler(handlerFunc) diff --git a/pkg/op/probes.go b/pkg/op/probes.go index 50e8a0f..7dc00a9 100644 --- a/pkg/op/probes.go +++ b/pkg/op/probes.go @@ -10,10 +10,16 @@ import ( type ProbesFn func(context.Context) error -func Healthz(w http.ResponseWriter, r *http.Request) { +func healthzHandler(w http.ResponseWriter, r *http.Request) { ok(w) } +func readyHandler(probes []ProbesFn) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + Readiness(w, r, probes...) + } +} + func Readiness(w http.ResponseWriter, r *http.Request, probes ...ProbesFn) { ctx := r.Context() for _, probe := range probes { diff --git a/pkg/op/session.go b/pkg/op/session.go index e60f71b..d04e361 100644 --- a/pkg/op/session.go +++ b/pkg/op/session.go @@ -5,17 +5,22 @@ import ( "net/http" "github.com/caos/oidc/pkg/oidc" - "github.com/caos/oidc/pkg/rp" "github.com/caos/oidc/pkg/utils" ) type SessionEnder interface { Decoder() utils.Decoder Storage() Storage - IDTokenVerifier() rp.Verifier + IDTokenHintVerifier() IDTokenHintVerifier DefaultLogoutRedirectURI() string } +func endSessionHandler(ender SessionEnder) func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + EndSession(w, r, ender) + } +} + func EndSession(w http.ResponseWriter, r *http.Request, ender SessionEnder) { req, err := ParseEndSessionRequest(r, ender.Decoder()) if err != nil { @@ -57,7 +62,7 @@ func ValidateEndSessionRequest(ctx context.Context, req *oidc.EndSessionRequest, if req.IdTokenHint == "" { return session, nil } - claims, err := ender.IDTokenVerifier().VerifyIDToken(ctx, req.IdTokenHint) + claims, err := VerifyIDTokenHint(ctx, req.IdTokenHint, ender.IDTokenHintVerifier()) if err != nil { return nil, ErrInvalidRequest("id_token_hint invalid") } diff --git a/pkg/op/signer.go b/pkg/op/signer.go index a313934..e9926cd 100644 --- a/pkg/op/signer.go +++ b/pkg/op/signer.go @@ -1,13 +1,12 @@ package op import ( + "context" "encoding/json" "errors" - "golang.org/x/net/context" - "gopkg.in/square/go-jose.v2" - "github.com/caos/logging" + "gopkg.in/square/go-jose.v2" "github.com/caos/oidc/pkg/oidc" ) diff --git a/pkg/op/storage.go b/pkg/op/storage.go index 17023c1..669b08e 100644 --- a/pkg/op/storage.go +++ b/pkg/op/storage.go @@ -16,7 +16,7 @@ type AuthStorage interface { SaveAuthCode(context.Context, string, string) error DeleteAuthRequest(context.Context, string) error - CreateToken(context.Context, AuthRequest) (string, time.Time, error) + CreateToken(context.Context, TokenRequest) (string, time.Time, error) TerminateSession(context.Context, string, string) error @@ -30,6 +30,7 @@ type OPStorage interface { AuthorizeClientIDSecret(context.Context, string, string) error GetUserinfoFromScopes(context.Context, string, []string) (*oidc.Userinfo, error) GetUserinfoFromToken(context.Context, string, string) (*oidc.Userinfo, error) + GetKeyByIDAndUserID(ctx context.Context, keyID, userID string) (*jose.JSONWebKey, error) } type Storage interface { diff --git a/pkg/op/token.go b/pkg/op/token.go index 9d37788..87494b9 100644 --- a/pkg/op/token.go +++ b/pkg/op/token.go @@ -14,12 +14,18 @@ type TokenCreator interface { Crypto() Crypto } +type TokenRequest interface { + GetSubject() string + GetAudience() []string + GetScopes() []string +} + func CreateTokenResponse(ctx context.Context, authReq AuthRequest, client Client, creator TokenCreator, createAccessToken bool, code string) (*oidc.AccessTokenResponse, error) { var accessToken string var validity time.Duration if createAccessToken { var err error - accessToken, validity, err = CreateAccessToken(ctx, authReq, client, creator) + accessToken, validity, err = CreateAccessToken(ctx, authReq, client.AccessTokenType(), creator) if err != nil { return nil, err } @@ -43,13 +49,27 @@ func CreateTokenResponse(ctx context.Context, authReq AuthRequest, client Client }, nil } -func CreateAccessToken(ctx context.Context, authReq AuthRequest, client Client, creator TokenCreator) (token string, validity time.Duration, err error) { +func CreateJWTTokenResponse(ctx context.Context, tokenRequest TokenRequest, creator TokenCreator) (*oidc.AccessTokenResponse, error) { + accessToken, validity, err := CreateAccessToken(ctx, tokenRequest, AccessTokenTypeBearer, creator) + if err != nil { + return nil, err + } + + exp := uint64(validity.Seconds()) + return &oidc.AccessTokenResponse{ + AccessToken: accessToken, + TokenType: oidc.BearerToken, + ExpiresIn: exp, + }, nil +} + +func CreateAccessToken(ctx context.Context, authReq TokenRequest, accessTokenType AccessTokenType, creator TokenCreator) (token string, validity time.Duration, err error) { id, exp, err := creator.Storage().CreateToken(ctx, authReq) if err != nil { return "", 0, err } validity = exp.Sub(time.Now().UTC()) - if client.AccessTokenType() == AccessTokenTypeJWT { + if accessTokenType == AccessTokenTypeJWT { token, err = CreateJWT(creator.Issuer(), authReq, exp, id, creator.Signer()) return } @@ -61,7 +81,7 @@ func CreateBearerToken(id string, crypto Crypto) (string, error) { return crypto.Encrypt(id) } -func CreateJWT(issuer string, authReq AuthRequest, exp time.Time, id string, signer Signer) (string, error) { +func CreateJWT(issuer string, authReq TokenRequest, exp time.Time, id string, signer Signer) (string, error) { now := time.Now().UTC() nbf := now claims := &oidc.AccessTokenClaims{ @@ -81,7 +101,7 @@ func CreateIDToken(ctx context.Context, issuer string, authReq AuthRequest, vali exp := time.Now().UTC().Add(validity) userinfo, err := storage.GetUserinfoFromScopes(ctx, authReq.GetSubject(), authReq.GetScopes()) if err != nil { - + return "", err } claims := &oidc.IDTokenClaims{ Issuer: issuer, diff --git a/pkg/op/tokenrequest.go b/pkg/op/tokenrequest.go index 71bd1ec..b3613ce 100644 --- a/pkg/op/tokenrequest.go +++ b/pkg/op/tokenrequest.go @@ -6,6 +6,7 @@ import ( "net/http" "github.com/caos/oidc/pkg/oidc" + "github.com/caos/oidc/pkg/oidc/grants/tokenexchange" "github.com/caos/oidc/pkg/utils" ) @@ -16,6 +17,28 @@ type Exchanger interface { Signer() Signer Crypto() Crypto AuthMethodPostSupported() bool + JWTProfileVerifier() JWTProfileVerifier +} + +func tokenHandler(exchanger Exchanger) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + switch r.FormValue("grant_type") { + case string(oidc.GrantTypeCode): + CodeExchange(w, r, exchanger) + return + case string(oidc.GrantTypeBearer): + JWTProfile(w, r, exchanger) + return + case "exchange": + TokenExchange(w, r, exchanger) + case "": + RequestError(w, r, ErrInvalidRequest("grant_type missing")) + return + default: + RequestError(w, r, ErrInvalidRequest("grant_type not supported")) + return + } + } } func CodeExchange(w http.ResponseWriter, r *http.Request, exchanger Exchanger) { @@ -114,6 +137,39 @@ func AuthorizeCodeChallenge(ctx context.Context, tokenReq *oidc.AccessTokenReque return authReq, nil } +func JWTProfile(w http.ResponseWriter, r *http.Request, exchanger Exchanger) { + assertion, err := ParseJWTProfileRequest(r, exchanger.Decoder()) + if err != nil { + RequestError(w, r, err) + } + + claims, err := VerifyJWTAssertion(r.Context(), assertion, exchanger.JWTProfileVerifier()) + if err != nil { + RequestError(w, r, err) + return + } + + resp, err := CreateJWTTokenResponse(r.Context(), claims, exchanger) + if err != nil { + RequestError(w, r, err) + return + } + utils.MarshalJSON(w, resp) +} + +func ParseJWTProfileRequest(r *http.Request, decoder utils.Decoder) (string, error) { + err := r.ParseForm() + if err != nil { + return "", ErrInvalidRequest("error parsing form") + } + tokenReq := new(tokenexchange.JWTProfileRequest) + err = decoder.Decode(tokenReq, r.Form) + if err != nil { + return "", ErrInvalidRequest("error decoding form") + } + return tokenReq.Assertion, nil +} + func TokenExchange(w http.ResponseWriter, r *http.Request, exchanger Exchanger) { tokenRequest, err := ParseTokenExchangeRequest(w, r) if err != nil { diff --git a/pkg/op/userinfo.go b/pkg/op/userinfo.go index 88ba955..36ecd4a 100644 --- a/pkg/op/userinfo.go +++ b/pkg/op/userinfo.go @@ -15,6 +15,12 @@ type UserinfoProvider interface { Storage() Storage } +func userinfoHandler(userinfoProvider UserinfoProvider) func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + Userinfo(w, r, userinfoProvider) + } +} + func Userinfo(w http.ResponseWriter, r *http.Request, userinfoProvider UserinfoProvider) { accessToken, err := getAccessToken(r, userinfoProvider.Decoder()) if err != nil { diff --git a/pkg/op/verifier_id_token_hint.go b/pkg/op/verifier_id_token_hint.go new file mode 100644 index 0000000..3268a5e --- /dev/null +++ b/pkg/op/verifier_id_token_hint.go @@ -0,0 +1,102 @@ +package op + +import ( + "context" + "time" + + "github.com/caos/oidc/pkg/oidc" +) + +type IDTokenHintVerifier interface { + oidc.Verifier + SupportedSignAlgs() []string + KeySet() oidc.KeySet + ACR() oidc.ACRVerifier + MaxAge() time.Duration +} + +type idTokenHintVerifier struct { + issuer string + maxAgeIAT time.Duration + offset time.Duration + supportedSignAlgs []string + maxAge time.Duration + acr oidc.ACRVerifier + keySet oidc.KeySet +} + +func (i *idTokenHintVerifier) Issuer() string { + return i.issuer +} + +func (i *idTokenHintVerifier) MaxAgeIAT() time.Duration { + return i.maxAgeIAT +} + +func (i *idTokenHintVerifier) Offset() time.Duration { + return i.offset +} + +func (i *idTokenHintVerifier) SupportedSignAlgs() []string { + return i.supportedSignAlgs +} + +func (i *idTokenHintVerifier) KeySet() oidc.KeySet { + return i.keySet +} + +func (i *idTokenHintVerifier) ACR() oidc.ACRVerifier { + return i.acr +} + +func (i *idTokenHintVerifier) MaxAge() time.Duration { + return i.maxAge +} + +func NewIDTokenHintVerifier(issuer string, keySet oidc.KeySet) IDTokenHintVerifier { + verifier := &idTokenHintVerifier{ + issuer: issuer, + keySet: keySet, + } + return verifier +} + +//VerifyIDTokenHint validates the id token according to +//https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation +func VerifyIDTokenHint(ctx context.Context, token string, v IDTokenHintVerifier) (*oidc.IDTokenClaims, error) { + claims := new(oidc.IDTokenClaims) + + decrypted, err := oidc.DecryptToken(token) + if err != nil { + return nil, err + } + payload, err := oidc.ParseToken(decrypted, claims) + if err != nil { + return nil, err + } + + if err := oidc.CheckIssuer(claims, v.Issuer()); err != nil { + return nil, err + } + + if err = oidc.CheckSignature(ctx, decrypted, payload, claims, v.SupportedSignAlgs(), v.KeySet()); err != nil { + return nil, err + } + + if err = oidc.CheckExpiration(claims, v.Offset()); err != nil { + return nil, err + } + + if err = oidc.CheckIssuedAt(claims, v.MaxAgeIAT(), v.Offset()); err != nil { + return nil, err + } + + if err = oidc.CheckAuthorizationContextClassReference(claims, v.ACR()); err != nil { + return nil, err + } + + if err = oidc.CheckAuthTime(claims, v.MaxAge()); err != nil { + return nil, err + } + return claims, nil +} diff --git a/pkg/op/verifier_jwt_profile.go b/pkg/op/verifier_jwt_profile.go new file mode 100644 index 0000000..b30bdc5 --- /dev/null +++ b/pkg/op/verifier_jwt_profile.go @@ -0,0 +1,101 @@ +package op + +import ( + "context" + "errors" + "time" + + "gopkg.in/square/go-jose.v2" + + "github.com/caos/oidc/pkg/oidc" +) + +type JWTProfileVerifier interface { + oidc.Verifier + Storage() Storage +} + +type jwtProfileVerifier struct { + storage Storage + issuer string + maxAgeIAT time.Duration + offset time.Duration +} + +func NewJWTProfileVerifier(storage Storage, issuer string, maxAgeIAT, offset time.Duration) JWTProfileVerifier { + return &jwtProfileVerifier{ + storage: storage, + issuer: issuer, + maxAgeIAT: maxAgeIAT, + offset: offset, + } +} + +func (v *jwtProfileVerifier) Issuer() string { + return v.issuer +} + +func (v *jwtProfileVerifier) Storage() Storage { + return v.storage +} + +func (v *jwtProfileVerifier) MaxAgeIAT() time.Duration { + return v.maxAgeIAT +} + +func (v *jwtProfileVerifier) Offset() time.Duration { + return v.offset +} + +func VerifyJWTAssertion(ctx context.Context, assertion string, v JWTProfileVerifier) (*oidc.JWTTokenRequest, error) { + request := new(oidc.JWTTokenRequest) + payload, err := oidc.ParseToken(assertion, request) + if err != nil { + return nil, err + } + + if err = oidc.CheckAudience(request, v.Issuer()); err != nil { + return nil, err + } + + if err = oidc.CheckExpiration(request, v.Offset()); err != nil { + return nil, err + } + + if err = oidc.CheckIssuedAt(request, v.MaxAgeIAT(), v.Offset()); err != nil { + return nil, err + } + + if request.Issuer != request.Subject { + //TODO: implement delegation (openid core / oauth rfc) + } + + keySet := &jwtProfileKeySet{v.Storage(), request.Subject} + + if err = oidc.CheckSignature(ctx, assertion, payload, request, nil, keySet); err != nil { + return nil, err + } + return request, nil +} + +type jwtProfileKeySet struct { + Storage + userID string +} + +func (k *jwtProfileKeySet) VerifySignature(ctx context.Context, jws *jose.JSONWebSignature) (payload []byte, err error) { + keyID := "" + for _, sig := range jws.Signatures { + keyID = sig.Header.KeyID + break + } + key, err := k.Storage.GetKeyByIDAndUserID(ctx, keyID, k.userID) + if err != nil { + return nil, errors.New("error fetching keys") + } + payload, err, ok := oidc.CheckKey(keyID, jws, *key) + if !ok { + return nil, errors.New("invalid kid") + } + return payload, err +} diff --git a/pkg/rp/cli/cli.go b/pkg/rp/cli/cli.go new file mode 100644 index 0000000..4b00ba0 --- /dev/null +++ b/pkg/rp/cli/cli.go @@ -0,0 +1,35 @@ +package cli + +import ( + "context" + "net/http" + + "github.com/caos/oidc/pkg/oidc" + "github.com/caos/oidc/pkg/rp" + "github.com/caos/oidc/pkg/utils" +) + +const ( + loginPath = "/login" +) + +func CodeFlow(relayingParty rp.RelayingParty, callbackPath, port string, stateProvider func() string) *oidc.Tokens { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + var token *oidc.Tokens + callback := func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens, state string) { + token = tokens + msg := "

Success!

" + msg = msg + "

You are authenticated and can now return to the CLI.

" + w.Write([]byte(msg)) + } + http.Handle(loginPath, rp.AuthURLHandler(stateProvider, relayingParty)) + http.Handle(callbackPath, rp.CodeExchangeHandler(callback, relayingParty)) + + utils.StartServer(ctx, port) + + utils.OpenBrowser("http://localhost:" + port + loginPath) + + return token +} diff --git a/pkg/rp/default_rp.go b/pkg/rp/default_rp.go deleted file mode 100644 index 3a830bb..0000000 --- a/pkg/rp/default_rp.go +++ /dev/null @@ -1,318 +0,0 @@ -package rp - -import ( - "context" - "encoding/base64" - "net/http" - "strings" - - "github.com/caos/oidc/pkg/oidc/grants" - - "golang.org/x/oauth2" - - "github.com/caos/oidc/pkg/oidc" - grants_tx "github.com/caos/oidc/pkg/oidc/grants/tokenexchange" - "github.com/caos/oidc/pkg/utils" -) - -const ( - idTokenKey = "id_token" - stateParam = "state" - pkceCode = "pkce" -) - -var ( - DefaultErrorHandler = func(w http.ResponseWriter, r *http.Request, errorType string, errorDesc string, state string) { - http.Error(w, errorType+": "+errorDesc, http.StatusInternalServerError) - } -) - -//DefaultRP impements the `DelegationTokenExchangeRP` interface extending the `RelayingParty` interface -type DefaultRP struct { - endpoints Endpoints - - oauthConfig oauth2.Config - config *Config - pkce bool - - httpClient *http.Client - cookieHandler *utils.CookieHandler - - errorHandler func(http.ResponseWriter, *http.Request, string, string, string) - - verifier Verifier - verifierOpts []ConfFunc - onlyOAuth2 bool -} - -//NewDefaultRP creates `DefaultRP` with the given -//Config and possible configOptions -//it will run discovery on the provided issuer -//if no verifier is provided using the options the `DefaultVerifier` is set -func NewDefaultRP(rpConfig *Config, rpOpts ...DefaultRPOpts) (DelegationTokenExchangeRP, error) { - foundOpenID := false - for _, scope := range rpConfig.Scopes { - if scope == "openid" { - foundOpenID = true - } - } - - p := &DefaultRP{ - config: rpConfig, - httpClient: utils.DefaultHTTPClient, - onlyOAuth2: !foundOpenID, - } - - for _, optFunc := range rpOpts { - optFunc(p) - } - - if rpConfig.Endpoints.TokenURL != "" && rpConfig.Endpoints.AuthURL != "" { - p.oauthConfig = p.getOAuthConfig(rpConfig.Endpoints) - } else { - if err := p.discover(); err != nil { - return nil, err - } - } - - if p.errorHandler == nil { - p.errorHandler = DefaultErrorHandler - } - - if p.verifier == nil { - p.verifier = NewDefaultVerifier(rpConfig.Issuer, rpConfig.ClientID, NewRemoteKeySet(p.httpClient, p.endpoints.JKWsURL), p.verifierOpts...) - } - - return p, nil -} - -//DefaultRPOpts is the type for providing dynamic options to the DefaultRP -type DefaultRPOpts func(p *DefaultRP) - -//WithCookieHandler set a `CookieHandler` for securing the various redirects -func WithCookieHandler(cookieHandler *utils.CookieHandler) DefaultRPOpts { - return func(p *DefaultRP) { - p.cookieHandler = cookieHandler - } -} - -//WithPKCE sets the RP to use PKCE (oauth2 code challenge) -//it also sets a `CookieHandler` for securing the various redirects -//and exchanging the code challenge -func WithPKCE(cookieHandler *utils.CookieHandler) DefaultRPOpts { - return func(p *DefaultRP) { - p.pkce = true - p.cookieHandler = cookieHandler - } -} - -//WithHTTPClient provides the ability to set an http client to be used for the relaying party and verifier -func WithHTTPClient(client *http.Client) DefaultRPOpts { - return func(p *DefaultRP) { - p.httpClient = client - } -} - -func WithVerifierOpts(opts ...ConfFunc) DefaultRPOpts { - return func(p *DefaultRP) { - p.verifierOpts = opts - } -} - -//AuthURL is the `RelayingParty` interface implementation -//wrapping the oauth2 `AuthCodeURL` -//returning the url of the auth request -func (p *DefaultRP) AuthURL(state string, opts ...AuthURLOpt) string { - authOpts := make([]oauth2.AuthCodeOption, 0) - for _, opt := range opts { - authOpts = append(authOpts, opt()...) - } - return p.oauthConfig.AuthCodeURL(state, authOpts...) -} - -//AuthURL is the `RelayingParty` interface implementation -//extending the `AuthURL` method with a http redirect handler -func (p *DefaultRP) AuthURLHandler(state string) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - opts := make([]AuthURLOpt, 0) - if err := p.trySetStateCookie(w, state); err != nil { - http.Error(w, "failed to create state cookie: "+err.Error(), http.StatusUnauthorized) - return - } - if p.pkce { - codeChallenge, err := p.generateAndStoreCodeChallenge(w) - if err != nil { - http.Error(w, "failed to create code challenge: "+err.Error(), http.StatusUnauthorized) - return - } - opts = append(opts, WithCodeChallenge(codeChallenge)) - } - http.Redirect(w, r, p.AuthURL(state, opts...), http.StatusFound) - } -} - -func (p *DefaultRP) generateAndStoreCodeChallenge(w http.ResponseWriter) (string, error) { - var codeVerifier string - codeVerifier = "s" - if err := p.cookieHandler.SetCookie(w, pkceCode, codeVerifier); err != nil { - return "", err - } - return oidc.NewSHACodeChallenge(codeVerifier), nil -} - -//AuthURL is the `RelayingParty` interface implementation -//handling the oauth2 code exchange, extracting and validating the id_token -//returning it paresed together with the oauth2 tokens (access, refresh) -func (p *DefaultRP) CodeExchange(ctx context.Context, code string, opts ...CodeExchangeOpt) (tokens *oidc.Tokens, err error) { - ctx = context.WithValue(ctx, oauth2.HTTPClient, p.httpClient) - codeOpts := make([]oauth2.AuthCodeOption, 0) - for _, opt := range opts { - codeOpts = append(codeOpts, opt()...) - } - - token, err := p.oauthConfig.Exchange(ctx, code, codeOpts...) - if err != nil { - return nil, err //TODO: our error - } - idTokenString, ok := token.Extra(idTokenKey).(string) - if !ok { - //TODO: implement - } - - idToken := new(oidc.IDTokenClaims) - if !p.onlyOAuth2 { - idToken, err = p.verifier.Verify(ctx, token.AccessToken, idTokenString) - if err != nil { - return nil, err //TODO: err - } - } - - return &oidc.Tokens{Token: token, IDTokenClaims: idToken, IDToken: idTokenString}, nil -} - -//AuthURL is the `RelayingParty` interface implementation -//extending the `CodeExchange` method with callback function -func (p *DefaultRP) CodeExchangeHandler(callback func(http.ResponseWriter, *http.Request, *oidc.Tokens, string)) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - state, err := p.tryReadStateCookie(w, r) - if err != nil { - http.Error(w, "failed to get state: "+err.Error(), http.StatusUnauthorized) - return - } - params := r.URL.Query() - if params.Get("error") != "" { - p.errorHandler(w, r, params.Get("error"), params.Get("error_description"), state) - return - } - codeOpts := make([]CodeExchangeOpt, 0) - if p.pkce { - codeVerifier, err := p.cookieHandler.CheckCookie(r, pkceCode) - if err != nil { - http.Error(w, "failed to get code verifier: "+err.Error(), http.StatusUnauthorized) - return - } - codeOpts = append(codeOpts, WithCodeVerifier(codeVerifier)) - } - tokens, err := p.CodeExchange(r.Context(), params.Get("code"), codeOpts...) - if err != nil { - http.Error(w, "failed to exchange token: "+err.Error(), http.StatusUnauthorized) - return - } - callback(w, r, tokens, state) - } -} - -// func (p *DefaultRP) Introspect(ctx context.Context, accessToken string) (oidc.TokenIntrospectResponse, error) { -// // req := &http.Request{} -// // resp, err := p.httpClient.Do(req) -// // if err != nil { - -// // } -// // p.endpoints.IntrospectURL -// return nil, nil -// } - -func (p *DefaultRP) Userinfo() {} - -//ClientCredentials is the `RelayingParty` interface implementation -//handling the oauth2 client credentials grant -func (p *DefaultRP) ClientCredentials(ctx context.Context, scopes ...string) (newToken *oauth2.Token, err error) { - return p.callTokenEndpoint(grants.ClientCredentialsGrantBasic(scopes...)) -} - -//TokenExchange is the `TokenExchangeRP` interface implementation -//handling the oauth2 token exchange (draft) -func (p *DefaultRP) TokenExchange(ctx context.Context, request *grants_tx.TokenExchangeRequest) (newToken *oauth2.Token, err error) { - return p.callTokenEndpoint(request) -} - -//DelegationTokenExchange is the `TokenExchangeRP` interface implementation -//handling the oauth2 token exchange for a delegation token (draft) -func (p *DefaultRP) DelegationTokenExchange(ctx context.Context, subjectToken string, reqOpts ...grants_tx.TokenExchangeOption) (newToken *oauth2.Token, err error) { - return p.TokenExchange(ctx, DelegationTokenRequest(subjectToken, reqOpts...)) -} - -func (p *DefaultRP) discover() error { - wellKnown := strings.TrimSuffix(p.config.Issuer, "/") + oidc.DiscoveryEndpoint - req, err := http.NewRequest("GET", wellKnown, nil) - if err != nil { - return err - } - discoveryConfig := new(oidc.DiscoveryConfiguration) - err = utils.HttpRequest(p.httpClient, req, &discoveryConfig) - if err != nil { - return err - } - p.endpoints = GetEndpoints(discoveryConfig) - p.oauthConfig = p.getOAuthConfig(p.endpoints.Endpoint) - return nil -} - -func (p *DefaultRP) getOAuthConfig(endpoint oauth2.Endpoint) oauth2.Config { - return oauth2.Config{ - ClientID: p.config.ClientID, - ClientSecret: p.config.ClientSecret, - Endpoint: endpoint, - RedirectURL: p.config.CallbackURL, - Scopes: p.config.Scopes, - } -} - -func (p *DefaultRP) callTokenEndpoint(request interface{}) (newToken *oauth2.Token, err error) { - req, err := utils.FormRequest(p.endpoints.TokenURL, request) - if err != nil { - return nil, err - } - auth := base64.StdEncoding.EncodeToString([]byte(p.config.ClientID + ":" + p.config.ClientSecret)) - req.Header.Set("Authorization", "Basic "+auth) - token := new(oauth2.Token) - if err := utils.HttpRequest(p.httpClient, req, token); err != nil { - return nil, err - } - return token, nil -} - -func (p *DefaultRP) trySetStateCookie(w http.ResponseWriter, state string) error { - if p.cookieHandler != nil { - if err := p.cookieHandler.SetCookie(w, stateParam, state); err != nil { - return err - } - } - return nil -} - -func (p *DefaultRP) tryReadStateCookie(w http.ResponseWriter, r *http.Request) (state string, err error) { - if p.cookieHandler == nil { - return r.FormValue(stateParam), nil - } - state, err = p.cookieHandler.CheckQueryCookie(r, stateParam) - if err != nil { - return "", err - } - p.cookieHandler.DeleteCookie(w, stateParam) - return state, nil -} - -func (p *DefaultRP) Client(ctx context.Context, token *oauth2.Token) *http.Client { - return p.oauthConfig.Client(ctx, token) -} diff --git a/pkg/rp/default_verifier.go b/pkg/rp/default_verifier.go deleted file mode 100644 index dfdf134..0000000 --- a/pkg/rp/default_verifier.go +++ /dev/null @@ -1,388 +0,0 @@ -package rp - -import ( - "bytes" - "context" - "encoding/base64" - "encoding/json" - "fmt" - "strings" - "time" - - "gopkg.in/square/go-jose.v2" - - "github.com/caos/oidc/pkg/oidc" - "github.com/caos/oidc/pkg/utils" -) - -//DefaultVerifier implements the `Verifier` interface -type DefaultVerifier struct { - config *verifierConfig - keySet oidc.KeySet -} - -//ConfFunc is the type for providing dynamic options to the DefaultVerfifier -type ConfFunc func(*verifierConfig) - -//ACRVerifier specifies the function to be used by the `DefaultVerifier` for validating the acr claim -type ACRVerifier func(string) error - -//NewDefaultVerifier creates `DefaultVerifier` with the given -//issuer, clientID, keyset and possible configOptions -func NewDefaultVerifier(issuer, clientID string, keySet oidc.KeySet, confOpts ...ConfFunc) Verifier { - conf := &verifierConfig{ - issuer: issuer, - clientID: clientID, - iat: &iatConfig{ - // offset: time.Duration(500 * time.Millisecond), - }, - } - - for _, opt := range confOpts { - if opt != nil { - opt(conf) - } - } - return &DefaultVerifier{config: conf, keySet: keySet} -} - -//WithIgnoreAudience will turn off validation for audience claim (should only be used for id_token_hints) -func WithIgnoreAudience() func(*verifierConfig) { - return func(conf *verifierConfig) { - conf.ignoreAudience = true - } -} - -//WithIgnoreExpiration will turn off validation for expiration claim (should only be used for id_token_hints) -func WithIgnoreExpiration() func(*verifierConfig) { - return func(conf *verifierConfig) { - conf.ignoreExpiration = true - } -} - -//WithIgnoreIssuedAt will turn off iat claim verification -func WithIgnoreIssuedAt() func(*verifierConfig) { - return func(conf *verifierConfig) { - conf.iat.ignore = true - } -} - -//WithIssuedAtOffset mitigates the risk of iat to be in the future -//because of clock skews with the ability to add an offset to the current time -func WithIssuedAtOffset(offset time.Duration) func(*verifierConfig) { - return func(conf *verifierConfig) { - conf.iat.offset = offset - } -} - -//WithIssuedAtMaxAge provides the ability to define the maximum duration between iat and now -func WithIssuedAtMaxAge(maxAge time.Duration) func(*verifierConfig) { - return func(conf *verifierConfig) { - conf.iat.maxAge = maxAge - } -} - -//WithNonce TODO: ? -func WithNonce(nonce string) func(*verifierConfig) { - return func(conf *verifierConfig) { - conf.nonce = nonce - } -} - -//WithACRVerifier sets the verifier for the acr claim -func WithACRVerifier(verifier ACRVerifier) func(*verifierConfig) { - return func(conf *verifierConfig) { - conf.acr = verifier - } -} - -//WithAuthTimeMaxAge provides the ability to define the maximum duration between auth_time and now -func WithAuthTimeMaxAge(maxAge time.Duration) func(*verifierConfig) { - return func(conf *verifierConfig) { - conf.maxAge = maxAge - } -} - -//WithSupportedSigningAlgorithms overwrites the default RS256 signing algorithm -func WithSupportedSigningAlgorithms(algs ...string) func(*verifierConfig) { - return func(conf *verifierConfig) { - conf.supportedSignAlgs = algs - } -} - -type verifierConfig struct { - issuer string - clientID string - nonce string - ignoreAudience bool - ignoreExpiration bool - iat *iatConfig - acr ACRVerifier - maxAge time.Duration - supportedSignAlgs []string - - // httpClient *http.Client - - now time.Time -} - -type iatConfig struct { - ignore bool - offset time.Duration - maxAge time.Duration -} - -//DefaultACRVerifier implements `ACRVerifier` returning an error -//if non of the provided values matches the acr claim -func DefaultACRVerifier(possibleValues []string) ACRVerifier { - return func(acr string) error { - if !utils.Contains(possibleValues, acr) { - return ErrAcrInvalid(possibleValues, acr) - } - return nil - } -} - -//Verify implements the `Verify` method of the `Verifier` interface -//according to https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation -//and https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowTokenValidation -func (v *DefaultVerifier) Verify(ctx context.Context, accessToken, idTokenString string) (*oidc.IDTokenClaims, error) { - v.config.now = time.Now().UTC() - idToken, err := v.VerifyIDToken(ctx, idTokenString) - if err != nil { - return nil, err - } - if err := v.verifyAccessToken(accessToken, idToken.AccessTokenHash, idToken.Signature); err != nil { //TODO: sig from token - return nil, err - } - return idToken, nil -} - -//Verify implements the `VerifyIDToken` method of the `Verifier` interface -//according to https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation -func (v *DefaultVerifier) VerifyIDToken(ctx context.Context, idTokenString string) (*oidc.IDTokenClaims, error) { - //1. if encrypted --> decrypt - decrypted, err := v.decryptToken(idTokenString) - if err != nil { - return nil, err - } - claims, payload, err := v.parseToken(decrypted) - if err != nil { - return nil, err - } - // token, err := jwt.ParseWithClaims(decrypted, claims, func(token *jwt.Token) (interface{}, error) { - //2, check issuer (exact match) - if err := v.checkIssuer(claims.Issuer); err != nil { - return nil, err - } - - //3. check aud (aud must contain client_id, all aud strings must be allowed) - if err = v.checkAudience(claims.Audiences); err != nil { - return nil, err - } - - if err = v.checkAuthorizedParty(claims.Audiences, claims.AuthorizedParty); err != nil { - return nil, err - } - - //6. check signature by keys - //7. check alg default is rs256 - //8. check if alg is mac based (hs...) -> audience contains client_id. for validation use utf-8 representation of your client_secret - claims.Signature, err = v.checkSignature(ctx, decrypted, payload) - if err != nil { - return nil, err - } - - //9. check exp before now - if err = v.checkExpiration(claims.Expiration); err != nil { - return nil, err - } - - //10. check iat duration is optional (can be checked) - if err = v.checkIssuedAt(claims.IssuedAt); err != nil { - return nil, err - } - - //11. check nonce (check if optional possible) id_token.nonce == sentNonce - if err = v.checkNonce(claims.Nonce); err != nil { - return nil, err - } - - //12. if acr requested check acr - if err = v.checkAuthorizationContextClassReference(claims.AuthenticationContextClassReference); err != nil { - return nil, err - } - - //13. if auth_time requested check if auth_time is less than max age - if err = v.checkAuthTime(claims.AuthTime); err != nil { - return nil, err - } - - return claims, nil -} - -func (v *DefaultVerifier) now() time.Time { - if v.config.now.IsZero() { - v.config.now = time.Now().UTC().Round(time.Second) - } - return v.config.now -} - -func (v *DefaultVerifier) parseToken(tokenString string) (*oidc.IDTokenClaims, []byte, error) { - parts := strings.Split(tokenString, ".") - if len(parts) != 3 { - return nil, nil, ValidationError("token contains an invalid number of segments") //TODO: err NewValidationError("token contains an invalid number of segments", ValidationErrorMalformed) - } - payload, err := base64.RawURLEncoding.DecodeString(parts[1]) - if err != nil { - return nil, nil, fmt.Errorf("oidc: malformed jwt payload: %v", err) - } - idToken := new(oidc.IDTokenClaims) - err = json.Unmarshal(payload, idToken) - return idToken, payload, err -} - -func (v *DefaultVerifier) checkIssuer(issuer string) error { - if v.config.issuer != issuer { - return ErrIssuerInvalid(v.config.issuer, issuer) - } - return nil -} - -func (v *DefaultVerifier) checkAudience(audiences []string) error { - if v.config.ignoreAudience { - return nil - } - if !utils.Contains(audiences, v.config.clientID) { - return ErrAudienceMissingClientID(v.config.clientID) - } - - //TODO: check aud trusted - return nil -} - -//4. if multiple aud strings --> check if azp -//5. if azp --> check azp == client_id -func (v *DefaultVerifier) checkAuthorizedParty(audiences []string, authorizedParty string) error { - if v.config.ignoreAudience { - return nil - } - if len(audiences) > 1 { - if authorizedParty == "" { - return ErrAzpMissing() - } - } - if authorizedParty != "" && authorizedParty != v.config.clientID { - return ErrAzpInvalid(authorizedParty, v.config.clientID) - } - return nil -} - -func (v *DefaultVerifier) checkSignature(ctx context.Context, idTokenString string, payload []byte) (jose.SignatureAlgorithm, error) { - jws, err := jose.ParseSigned(idTokenString) - if err != nil { - return "", err - } - if len(jws.Signatures) == 0 { - return "", ErrSignatureMissing() - } - if len(jws.Signatures) > 1 { - return "", ErrSignatureMultiple() - } - sig := jws.Signatures[0] - supportedSigAlgs := v.config.supportedSignAlgs - if len(supportedSigAlgs) == 0 { - supportedSigAlgs = []string{"RS256"} - } - if !utils.Contains(supportedSigAlgs, sig.Header.Algorithm) { - return "", fmt.Errorf("oidc: id token signed with unsupported algorithm, expected %q got %q", supportedSigAlgs, sig.Header.Algorithm) - } - - signedPayload, err := v.keySet.VerifySignature(ctx, jws) - if err != nil { - return "", err - } - - if !bytes.Equal(signedPayload, payload) { - return "", ErrSignatureInvalidPayload() - } - return jose.SignatureAlgorithm(sig.Header.Algorithm), nil -} - -func (v *DefaultVerifier) checkExpiration(expiration time.Time) error { - if v.config.ignoreExpiration { - return nil - } - expiration = expiration.Round(time.Second) - if !v.now().Before(expiration) { - return ErrExpInvalid(expiration) - } - return nil -} - -func (v *DefaultVerifier) checkIssuedAt(issuedAt time.Time) error { - if v.config.iat.ignore { - return nil - } - issuedAt = issuedAt.Round(time.Second) - offset := v.now().Add(v.config.iat.offset).Round(time.Second) - if issuedAt.After(offset) { - return ErrIatInFuture(issuedAt, offset) - } - if v.config.iat.maxAge == 0 { - return nil - } - maxAge := v.now().Add(-v.config.iat.maxAge).Round(time.Second) - if issuedAt.Before(maxAge) { - return ErrIatToOld(maxAge, issuedAt) - } - return nil -} -func (v *DefaultVerifier) checkNonce(nonce string) error { - if v.config.nonce == "" { - return nil - } - if v.config.nonce != nonce { - return ErrNonceInvalid(v.config.nonce, nonce) - } - return nil -} -func (v *DefaultVerifier) checkAuthorizationContextClassReference(acr string) error { - if v.config.acr != nil { - return v.config.acr(acr) - } - return nil -} -func (v *DefaultVerifier) checkAuthTime(authTime time.Time) error { - if v.config.maxAge == 0 { - return nil - } - if authTime.IsZero() { - return ErrAuthTimeNotPresent() - } - authTime = authTime.Round(time.Second) - maxAge := v.now().Add(-v.config.maxAge).Round(time.Second) - if authTime.Before(maxAge) { - return ErrAuthTimeToOld(maxAge, authTime) - } - return nil -} - -func (v *DefaultVerifier) decryptToken(tokenString string) (string, error) { - return tokenString, nil //TODO: impl -} - -func (v *DefaultVerifier) verifyAccessToken(accessToken, atHash string, sigAlgorithm jose.SignatureAlgorithm) error { - if atHash == "" { - return nil - } - - actual, err := oidc.ClaimHash(accessToken, sigAlgorithm) - if err != nil { - return err - } - if actual != atHash { - return ErrAtHash() - } - return nil -} diff --git a/pkg/rp/error.go b/pkg/rp/error.go deleted file mode 100644 index fa0ece9..0000000 --- a/pkg/rp/error.go +++ /dev/null @@ -1,67 +0,0 @@ -package rp - -import ( - "fmt" - "time" -) - -var ( - ErrIssuerInvalid = func(expected, actual string) *validationError { - return ValidationError("Issuer does not match. Expected: %s, got: %s", expected, actual) - } - ErrAudienceMissingClientID = func(clientID string) *validationError { - return ValidationError("Audience is not valid. Audience must contain client_id (%s)", clientID) - } - ErrAzpMissing = func() *validationError { - return ValidationError("Authorized Party is not set. If Token is valid for multiple audiences, azp must not be empty") - } - ErrAzpInvalid = func(azp, clientID string) *validationError { - return ValidationError("Authorized Party is not valid. azp (%s) must be equal to client_id (%s)", azp, clientID) - } - ErrExpInvalid = func(exp time.Time) *validationError { - return ValidationError("Token has expired %v", exp) - } - ErrIatInFuture = func(exp, now time.Time) *validationError { - return ValidationError("IssuedAt of token is in the future (%v, now with offset: %v)", exp, now) - } - ErrIatToOld = func(maxAge, iat time.Time) *validationError { - return ValidationError("IssuedAt of token must not be older than %v, but was %v (%v to old)", maxAge, iat, maxAge.Sub(iat)) - } - ErrNonceInvalid = func(expected, actual string) *validationError { - return ValidationError("nonce does not match. Expected: %s, got: %s", expected, actual) - } - ErrAcrInvalid = func(expected []string, actual string) *validationError { - return ValidationError("acr is invalid. Expected one of: %v, got: %s", expected, actual) - } - - ErrAuthTimeNotPresent = func() *validationError { - return ValidationError("claim `auth_time` of token is missing") - } - ErrAuthTimeToOld = func(maxAge, authTime time.Time) *validationError { - return ValidationError("Auth Time of token must not be older than %v, but was %v (%v to old)", maxAge, authTime, maxAge.Sub(authTime)) - } - ErrSignatureMissing = func() *validationError { - return ValidationError("id_token does not contain a signature") - } - ErrSignatureMultiple = func() *validationError { - return ValidationError("id_token contains multiple signatures") - } - ErrSignatureInvalidPayload = func() *validationError { - return ValidationError("Signature does not match Payload") - } - ErrAtHash = func() *validationError { - return ValidationError("at_hash does not correspond to access token") - } -) - -func ValidationError(message string, args ...interface{}) *validationError { - return &validationError{fmt.Sprintf(message, args...)} //TODO: impl -} - -type validationError struct { - message string -} - -func (v *validationError) Error() string { - return v.message -} diff --git a/pkg/rp/jwks.go b/pkg/rp/jwks.go index 97b1e6f..339fc93 100644 --- a/pkg/rp/jwks.go +++ b/pkg/rp/jwks.go @@ -74,7 +74,7 @@ func (r *remoteKeySet) VerifySignature(ctx context.Context, jws *jose.JSONWebSig } keys := r.keysFromCache() - payload, err, ok := CheckKey(keyID, keys, jws) + payload, err, ok := oidc.CheckKey(keyID, jws, keys...) if ok { return payload, err } @@ -84,7 +84,7 @@ func (r *remoteKeySet) VerifySignature(ctx context.Context, jws *jose.JSONWebSig return nil, fmt.Errorf("fetching keys %v", err) } - payload, err, ok = CheckKey(keyID, keys, jws) + payload, err, ok = oidc.CheckKey(keyID, jws, keys...) if !ok { return nil, errors.New("invalid kid") } diff --git a/pkg/rp/jws.go b/pkg/rp/jws.go deleted file mode 100644 index 20ab896..0000000 --- a/pkg/rp/jws.go +++ /dev/null @@ -1,15 +0,0 @@ -package rp - -import ( - "gopkg.in/square/go-jose.v2" -) - -func CheckKey(keyID string, keys []jose.JSONWebKey, jws *jose.JSONWebSignature) ([]byte, error, bool) { - for _, key := range keys { - if keyID == "" || key.KeyID == keyID { - payload, err := jws.Verify(&key) - return payload, err, true - } - } - return nil, nil, false -} diff --git a/pkg/rp/relaying_party.go b/pkg/rp/relaying_party.go index 8aba443..109a3ef 100644 --- a/pkg/rp/relaying_party.go +++ b/pkg/rp/relaying_party.go @@ -2,66 +2,369 @@ package rp import ( "context" + "errors" "net/http" + "strings" + + "github.com/google/uuid" "github.com/caos/oidc/pkg/oidc" + "github.com/caos/oidc/pkg/oidc/grants" + "github.com/caos/oidc/pkg/utils" "golang.org/x/oauth2" ) +const ( + idTokenKey = "id_token" + stateParam = "state" + pkceCode = "pkce" + jwtProfileKey = "urn:ietf:params:oauth:grant-type:jwt-bearer" +) + //RelayingParty declares the minimal interface for oidc clients type RelayingParty interface { - //Client return a standard http client where the token can be used - Client(ctx context.Context, token *oauth2.Token) *http.Client + //OAuthConfig returns the oauth2 Config + OAuthConfig() *oauth2.Config - //AuthURL returns the authorization endpoint with a given state - AuthURL(state string, opts ...AuthURLOpt) string + //IsPKCE returns if authorization is done using `Authorization Code Flow with Proof Key for Code Exchange (PKCE)` + IsPKCE() bool - //AuthURLHandler should implement the AuthURL func as http.HandlerFunc - //(redirecting to the auth endpoint) - AuthURLHandler(state string) http.HandlerFunc + //CookieHandler returns a http cookie handler used for various state transfer cookies + CookieHandler() *utils.CookieHandler - //CodeExchange implements the OIDC Token Request (oauth2 Authorization Code Grant) - //returning an `Access Token` and `ID Token Claims` - CodeExchange(ctx context.Context, code string, opts ...CodeExchangeOpt) (*oidc.Tokens, error) + //HttpClient returns a http client used for calls to the openid provider, e.g. calling token endpoint + HttpClient() *http.Client - //CodeExchangeHandler extends the CodeExchange func, - //calling the provided callback func on success with additional returned `state` - CodeExchangeHandler(callback func(http.ResponseWriter, *http.Request, *oidc.Tokens, string)) http.HandlerFunc + //IsOAuth2Only specifies whether relaying party handles only oauth2 or oidc calls + IsOAuth2Only() bool - //ClientCredentials implements the oauth2 Client Credentials Grant - //requesting an `Access Token` for the client itself, without user context - ClientCredentials(ctx context.Context, scopes ...string) (*oauth2.Token, error) + //IDTokenVerifier returns the verifier interface used for oidc id_token verification + IDTokenVerifier() IDTokenVerifier - //Introspects calls the Introspect Endpoint - //for validating an (access) token - // Introspect(ctx context.Context, token string) (TokenIntrospectResponse, error) - - //Userinfo implements the OIDC Userinfo call - //returning the info of the user for the requested scopes of an access token - Userinfo() + //ErrorHandler returns the handler used for callback errors + ErrorHandler() func(http.ResponseWriter, *http.Request, string, string, string) } -//PasswortGrantRP extends the `RelayingParty` interface with the oauth2 `Password Grant` -// -//This interface is separated from the standard `RelayingParty` interface as the `password grant` -//is part of the oauth2 and therefore OIDC specification, but should only be used when there's no -//other possibility, so IMHO never ever. Ever. -type PasswortGrantRP interface { - RelayingParty +type ErrorHandler func(w http.ResponseWriter, r *http.Request, errorType string, errorDesc string, state string) - //PasswordGrant implements the oauth2 `Password Grant`, - //requesting an access token with the users `username` and `password` - PasswordGrant(context.Context, string, string) (*oauth2.Token, error) +var ( + DefaultErrorHandler ErrorHandler = func(w http.ResponseWriter, r *http.Request, errorType string, errorDesc string, state string) { + http.Error(w, errorType+": "+errorDesc, http.StatusInternalServerError) + } +) + +type relayingParty struct { + issuer string + endpoints Endpoints + oauthConfig *oauth2.Config + oauth2Only bool + pkce bool + + httpClient *http.Client + cookieHandler *utils.CookieHandler + errorHandler func(http.ResponseWriter, *http.Request, string, string, string) + + idTokenVerifier IDTokenVerifier + verifierOpts []VerifierOption } -type Config struct { - ClientID string - ClientSecret string - CallbackURL string - Issuer string - Scopes []string - Endpoints oauth2.Endpoint +func (rp *relayingParty) OAuthConfig() *oauth2.Config { + return rp.oauthConfig +} + +func (rp *relayingParty) IsPKCE() bool { + return rp.pkce +} + +func (rp *relayingParty) CookieHandler() *utils.CookieHandler { + return rp.cookieHandler +} + +func (rp *relayingParty) HttpClient() *http.Client { + return rp.httpClient +} + +func (rp *relayingParty) IsOAuth2Only() bool { + return rp.oauth2Only +} + +func (rp *relayingParty) IDTokenVerifier() IDTokenVerifier { + if rp.idTokenVerifier == nil { + rp.idTokenVerifier = NewIDTokenVerifier(rp.issuer, rp.oauthConfig.ClientID, NewRemoteKeySet(rp.httpClient, rp.endpoints.JKWsURL), rp.verifierOpts...) + } + return rp.idTokenVerifier +} + +func (rp *relayingParty) ErrorHandler() func(http.ResponseWriter, *http.Request, string, string, string) { + if rp.errorHandler == nil { + rp.errorHandler = DefaultErrorHandler + } + return rp.errorHandler +} + +//NewRelayingPartyOAuth creates an (OAuth2) RelayingParty with the given +//OAuth2 Config and possible configOptions +//it will use the AuthURL and TokenURL set in config +func NewRelayingPartyOAuth(config *oauth2.Config, options ...Option) (RelayingParty, error) { + rp := &relayingParty{ + oauthConfig: config, + httpClient: utils.DefaultHTTPClient, + oauth2Only: true, + } + + for _, optFunc := range options { + optFunc(rp) + } + + return rp, nil +} + +//NewRelayingPartyOIDC creates an (OIDC) RelayingParty with the given +//issuer, clientID, clientSecret, redirectURI, scopes and possible configOptions +//it will run discovery on the provided issuer and use the found endpoints +func NewRelayingPartyOIDC(issuer, clientID, clientSecret, redirectURI string, scopes []string, options ...Option) (RelayingParty, error) { + rp := &relayingParty{ + issuer: issuer, + oauthConfig: &oauth2.Config{ + ClientID: clientID, + ClientSecret: clientSecret, + RedirectURL: redirectURI, + Scopes: scopes, + }, + httpClient: utils.DefaultHTTPClient, + oauth2Only: false, + } + + for _, optFunc := range options { + optFunc(rp) + } + + endpoints, err := Discover(rp.issuer, rp.httpClient) + if err != nil { + return nil, err + } + rp.oauthConfig.Endpoint = endpoints.Endpoint + rp.endpoints = endpoints + + return rp, nil +} + +//DefaultRPOpts is the type for providing dynamic options to the DefaultRP +type Option func(*relayingParty) + +//WithCookieHandler set a `CookieHandler` for securing the various redirects +func WithCookieHandler(cookieHandler *utils.CookieHandler) Option { + return func(rp *relayingParty) { + rp.cookieHandler = cookieHandler + } +} + +//WithPKCE sets the RP to use PKCE (oauth2 code challenge) +//it also sets a `CookieHandler` for securing the various redirects +//and exchanging the code challenge +func WithPKCE(cookieHandler *utils.CookieHandler) Option { + return func(rp *relayingParty) { + rp.pkce = true + rp.cookieHandler = cookieHandler + } +} + +//WithHTTPClient provides the ability to set an http client to be used for the relaying party and verifier +func WithHTTPClient(client *http.Client) Option { + return func(rp *relayingParty) { + rp.httpClient = client + } +} + +func WithErrorHandler(errorHandler ErrorHandler) Option { + return func(rp *relayingParty) { + rp.errorHandler = errorHandler + } +} + +func WithVerifierOpts(opts ...VerifierOption) Option { + return func(rp *relayingParty) { + rp.verifierOpts = opts + } +} + +//Discover calls the discovery endpoint of the provided issuer and returns the found endpoints +func Discover(issuer string, httpClient *http.Client) (Endpoints, error) { + wellKnown := strings.TrimSuffix(issuer, "/") + oidc.DiscoveryEndpoint + req, err := http.NewRequest("GET", wellKnown, nil) + if err != nil { + return Endpoints{}, err + } + discoveryConfig := new(oidc.DiscoveryConfiguration) + err = utils.HttpRequest(httpClient, req, &discoveryConfig) + if err != nil { + return Endpoints{}, err + } + return GetEndpoints(discoveryConfig), nil +} + +//AuthURL returns the auth request url +//(wrapping the oauth2 `AuthCodeURL`) +func AuthURL(state string, rp RelayingParty, opts ...AuthURLOpt) string { + authOpts := make([]oauth2.AuthCodeOption, 0) + for _, opt := range opts { + authOpts = append(authOpts, opt()...) + } + return rp.OAuthConfig().AuthCodeURL(state, authOpts...) +} + +//AuthURLHandler extends the `AuthURL` method with a http redirect handler +//including handling setting cookie for secure `state` transfer +func AuthURLHandler(stateFn func() string, rp RelayingParty) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + opts := make([]AuthURLOpt, 0) + state := stateFn() + if err := trySetStateCookie(w, state, rp); err != nil { + http.Error(w, "failed to create state cookie: "+err.Error(), http.StatusUnauthorized) + return + } + if rp.IsPKCE() { + codeChallenge, err := GenerateAndStoreCodeChallenge(w, rp) + if err != nil { + http.Error(w, "failed to create code challenge: "+err.Error(), http.StatusUnauthorized) + return + } + opts = append(opts, WithCodeChallenge(codeChallenge)) + } + http.Redirect(w, r, AuthURL(state, rp, opts...), http.StatusFound) + } +} + +//GenerateAndStoreCodeChallenge generates a PKCE code challenge and stores its verifier into a secure cookie +func GenerateAndStoreCodeChallenge(w http.ResponseWriter, rp RelayingParty) (string, error) { + codeVerifier := uuid.New().String() + if err := rp.CookieHandler().SetCookie(w, pkceCode, codeVerifier); err != nil { + return "", err + } + return oidc.NewSHACodeChallenge(codeVerifier), nil +} + +//CodeExchange handles the oauth2 code exchange, extracting and validating the id_token +//returning it parsed together with the oauth2 tokens (access, refresh) +func CodeExchange(ctx context.Context, code string, rp RelayingParty, opts ...CodeExchangeOpt) (tokens *oidc.Tokens, err error) { + ctx = context.WithValue(ctx, oauth2.HTTPClient, rp.HttpClient()) + codeOpts := make([]oauth2.AuthCodeOption, 0) + for _, opt := range opts { + codeOpts = append(codeOpts, opt()...) + } + + token, err := rp.OAuthConfig().Exchange(ctx, code, codeOpts...) + if err != nil { + return nil, err + } + + if rp.IsOAuth2Only() { + return &oidc.Tokens{Token: token}, nil + } + + idTokenString, ok := token.Extra(idTokenKey).(string) + if !ok { + return nil, errors.New("id_token missing") + } + + idToken, err := VerifyTokens(ctx, token.AccessToken, idTokenString, rp.IDTokenVerifier()) + if err != nil { + return nil, err + } + + return &oidc.Tokens{Token: token, IDTokenClaims: idToken, IDToken: idTokenString}, nil +} + +//CodeExchangeHandler extends the `CodeExchange` method with a http handler +//including cookie handling for secure `state` transfer +//and optional PKCE code verifier checking +func CodeExchangeHandler(callback func(http.ResponseWriter, *http.Request, *oidc.Tokens, string), rp RelayingParty) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + state, err := tryReadStateCookie(w, r, rp) + if err != nil { + http.Error(w, "failed to get state: "+err.Error(), http.StatusUnauthorized) + return + } + params := r.URL.Query() + if params.Get("error") != "" { + rp.ErrorHandler()(w, r, params.Get("error"), params.Get("error_description"), state) + return + } + codeOpts := make([]CodeExchangeOpt, 0) + if rp.IsPKCE() { + codeVerifier, err := rp.CookieHandler().CheckCookie(r, pkceCode) + if err != nil { + http.Error(w, "failed to get code verifier: "+err.Error(), http.StatusUnauthorized) + return + } + codeOpts = append(codeOpts, WithCodeVerifier(codeVerifier)) + } + tokens, err := CodeExchange(r.Context(), params.Get("code"), rp, codeOpts...) + if err != nil { + http.Error(w, "failed to exchange token: "+err.Error(), http.StatusUnauthorized) + return + } + callback(w, r, tokens, state) + } +} + +//ClientCredentials is the `RelayingParty` interface implementation +//handling the oauth2 client credentials grant +func ClientCredentials(ctx context.Context, rp RelayingParty, scopes ...string) (newToken *oauth2.Token, err error) { + return CallTokenEndpoint(grants.ClientCredentialsGrantBasic(scopes...), rp) +} + +func CallTokenEndpoint(request interface{}, rp RelayingParty) (newToken *oauth2.Token, err error) { + config := rp.OAuthConfig() + req, err := utils.FormRequest(rp.OAuthConfig().Endpoint.TokenURL, request, config.ClientID, config.ClientSecret, config.Endpoint.AuthStyle != oauth2.AuthStyleInParams) + if err != nil { + return nil, err + } + token := new(oauth2.Token) + if err := utils.HttpRequest(rp.HttpClient(), req, token); err != nil { + return nil, err + } + return token, nil +} + +func CallJWTProfileEndpoint(assertion string, rp RelayingParty) (*oauth2.Token, error) { + form := make(map[string][]string) + form["assertion"] = []string{assertion} + form["grant_type"] = []string{jwtProfileKey} + req, err := http.NewRequest("POST", rp.OAuthConfig().Endpoint.TokenURL, nil) + if err != nil { + return nil, err + } + + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + token := new(oauth2.Token) + if err := utils.HttpRequest(rp.HttpClient(), req, token); err != nil { + return nil, err + } + return token, nil +} + +func trySetStateCookie(w http.ResponseWriter, state string, rp RelayingParty) error { + if rp.CookieHandler() != nil { + if err := rp.CookieHandler().SetCookie(w, stateParam, state); err != nil { + return err + } + } + return nil +} + +func tryReadStateCookie(w http.ResponseWriter, r *http.Request, rp RelayingParty) (state string, err error) { + if rp.CookieHandler() == nil { + return r.FormValue(stateParam), nil + } + state, err = rp.CookieHandler().CheckQueryCookie(r, stateParam) + if err != nil { + return "", err + } + rp.CookieHandler().DeleteCookie(w, stateParam) + return state, nil } type OptionFunc func(RelayingParty) diff --git a/pkg/rp/tockenexchange.go b/pkg/rp/tockenexchange.go index d84b38e..24b588a 100644 --- a/pkg/rp/tockenexchange.go +++ b/pkg/rp/tockenexchange.go @@ -2,9 +2,15 @@ package rp import ( "context" + "crypto/rsa" + "crypto/x509" + "encoding/json" + "encoding/pem" "golang.org/x/oauth2" + "gopkg.in/square/go-jose.v2" + "github.com/caos/oidc/pkg/oidc" "github.com/caos/oidc/pkg/oidc/grants/tokenexchange" ) @@ -12,7 +18,7 @@ import ( type TokenExchangeRP interface { RelayingParty - //TokenExchange implement the `Token Echange Grant` exchanging some token for an other + //TokenExchange implement the `Token Exchange Grant` exchanging some token for an other TokenExchange(context.Context, *tokenexchange.TokenExchangeRequest) (*oauth2.Token, error) } @@ -25,3 +31,65 @@ type DelegationTokenExchangeRP interface { //providing an access token in request for a `delegation` token for a given resource / audience DelegationTokenExchange(context.Context, string, ...tokenexchange.TokenExchangeOption) (*oauth2.Token, error) } + +//TokenExchange handles the oauth2 token exchange +func TokenExchange(ctx context.Context, request *tokenexchange.TokenExchangeRequest, rp RelayingParty) (newToken *oauth2.Token, err error) { + return CallTokenEndpoint(request, rp) +} + +//DelegationTokenExchange handles the oauth2 token exchange for a delegation token +func DelegationTokenExchange(ctx context.Context, subjectToken string, rp RelayingParty, reqOpts ...tokenexchange.TokenExchangeOption) (newToken *oauth2.Token, err error) { + return TokenExchange(ctx, DelegationTokenRequest(subjectToken, reqOpts...), rp) +} + +//JWTProfileExchange handles the oauth2 jwt profile exchange +func JWTProfileExchange(ctx context.Context, assertion *oidc.JWTProfileAssertion, rp RelayingParty) (*oauth2.Token, error) { + token, err := generateJWTProfileToken(assertion) + if err != nil { + return nil, err + } + return CallJWTProfileEndpoint(token, rp) +} + +func generateJWTProfileToken(assertion *oidc.JWTProfileAssertion) (string, error) { + privateKey, err := bytesToPrivateKey(assertion.PrivateKey) + if err != nil { + return "", err + } + key := jose.SigningKey{ + Algorithm: jose.RS256, + Key: &jose.JSONWebKey{Key: privateKey, KeyID: assertion.PrivateKeyID}, + } + signer, err := jose.NewSigner(key, &jose.SignerOptions{}) + if err != nil { + return "", err + } + + marshalledAssertion, err := json.Marshal(assertion) + if err != nil { + return "", err + } + signedAssertion, err := signer.Sign(marshalledAssertion) + if err != nil { + return "", err + } + return signedAssertion.CompactSerialize() +} + +func bytesToPrivateKey(priv []byte) (*rsa.PrivateKey, error) { + block, _ := pem.Decode(priv) + enc := x509.IsEncryptedPEMBlock(block) + b := block.Bytes + var err error + if enc { + b, err = x509.DecryptPEMBlock(block, nil) + if err != nil { + return nil, err + } + } + key, err := x509.ParsePKCS1PrivateKey(b) + if err != nil { + return nil, err + } + return key, nil +} diff --git a/pkg/rp/verifier.go b/pkg/rp/verifier.go index 5add60f..ef2cf87 100644 --- a/pkg/rp/verifier.go +++ b/pkg/rp/verifier.go @@ -2,12 +2,220 @@ package rp import ( "context" + "time" + + "gopkg.in/square/go-jose.v2" "github.com/caos/oidc/pkg/oidc" ) -//Verifier implement the Token Response Validation as defined in OIDC specification +type IDTokenVerifier interface { + oidc.Verifier + ClientID() string + SupportedSignAlgs() []string + KeySet() oidc.KeySet + Nonce(context.Context) string + ACR() oidc.ACRVerifier + MaxAge() time.Duration +} + +//VerifyTokens implement the Token Response Validation as defined in OIDC specification //https://openid.net/specs/openid-connect-core-1_0.html#TokenResponseValidation +func VerifyTokens(ctx context.Context, accessToken, idTokenString string, v IDTokenVerifier) (*oidc.IDTokenClaims, error) { + idToken, err := VerifyIDToken(ctx, idTokenString, v) + if err != nil { + return nil, err + } + if err := VerifyAccessToken(accessToken, idToken.AccessTokenHash, idToken.Signature); err != nil { + return nil, err + } + return idToken, nil +} + +//VerifyIDToken validates the id token according to +//https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation +func VerifyIDToken(ctx context.Context, token string, v IDTokenVerifier) (*oidc.IDTokenClaims, error) { + claims := new(oidc.IDTokenClaims) + + decrypted, err := oidc.DecryptToken(token) + if err != nil { + return nil, err + } + payload, err := oidc.ParseToken(decrypted, claims) + if err != nil { + return nil, err + } + + if err := oidc.CheckIssuer(claims, v.Issuer()); err != nil { + return nil, err + } + + if err = oidc.CheckAudience(claims, v.ClientID()); err != nil { + return nil, err + } + + if err = oidc.CheckAuthorizedParty(claims, v.ClientID()); err != nil { + return nil, err + } + + if err = oidc.CheckSignature(ctx, decrypted, payload, claims, v.SupportedSignAlgs(), v.KeySet()); err != nil { + return nil, err + } + + if err = oidc.CheckExpiration(claims, v.Offset()); err != nil { + return nil, err + } + + if err = oidc.CheckIssuedAt(claims, v.MaxAgeIAT(), v.Offset()); err != nil { + return nil, err + } + + if err = oidc.CheckNonce(claims, v.Nonce(ctx)); err != nil { + return nil, err + } + + if err = oidc.CheckAuthorizationContextClassReference(claims, v.ACR()); err != nil { + return nil, err + } + + if err = oidc.CheckAuthTime(claims, v.MaxAge()); err != nil { + return nil, err + } + return claims, nil +} + +//VerifyAccessToken validates the access token according to +//https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowTokenValidation +func VerifyAccessToken(accessToken, atHash string, sigAlgorithm jose.SignatureAlgorithm) error { + if atHash == "" { + return nil + } + + actual, err := oidc.ClaimHash(accessToken, sigAlgorithm) + if err != nil { + return err + } + if actual != atHash { + return oidc.ErrAtHash + } + return nil +} + +//NewIDTokenVerifier returns an implementation of `IDTokenVerifier` +//for `VerifyTokens` and `VerifyIDToken` +func NewIDTokenVerifier(issuer, clientID string, keySet oidc.KeySet, options ...VerifierOption) IDTokenVerifier { + v := &idTokenVerifier{ + issuer: issuer, + clientID: clientID, + keySet: keySet, + offset: 1 * time.Second, + nonce: func(_ context.Context) string { + return "" + }, + } + + for _, opts := range options { + opts(v) + } + + return v +} + +//VerifierOption is the type for providing dynamic options to the IDTokenVerifier +type VerifierOption func(*idTokenVerifier) + +//WithIssuedAtOffset mitigates the risk of iat to be in the future +//because of clock skews with the ability to add an offset to the current time +func WithIssuedAtOffset(offset time.Duration) func(*idTokenVerifier) { + return func(v *idTokenVerifier) { + v.offset = offset + } +} + +//WithIssuedAtMaxAge provides the ability to define the maximum duration between iat and now +func WithIssuedAtMaxAge(maxAge time.Duration) func(*idTokenVerifier) { + return func(v *idTokenVerifier) { + v.maxAge = maxAge + } +} + +//WithNonce sets the function to check the nonce +func WithNonce(nonce func(context.Context) string) VerifierOption { + return func(v *idTokenVerifier) { + v.nonce = nonce + } +} + +//WithACRVerifier sets the verifier for the acr claim +func WithACRVerifier(verifier oidc.ACRVerifier) VerifierOption { + return func(v *idTokenVerifier) { + v.acr = verifier + } +} + +//WithAuthTimeMaxAge provides the ability to define the maximum duration between auth_time and now +func WithAuthTimeMaxAge(maxAge time.Duration) VerifierOption { + return func(v *idTokenVerifier) { + v.maxAge = maxAge + } +} + +//WithSupportedSigningAlgorithms overwrites the default RS256 signing algorithm +func WithSupportedSigningAlgorithms(algs ...string) VerifierOption { + return func(v *idTokenVerifier) { + v.supportedSignAlgs = algs + } +} + +type idTokenVerifier struct { + issuer string + maxAgeIAT time.Duration + offset time.Duration + clientID string + supportedSignAlgs []string + keySet oidc.KeySet + acr oidc.ACRVerifier + maxAge time.Duration + nonce func(ctx context.Context) string +} + +func (i *idTokenVerifier) Issuer() string { + return i.issuer +} + +func (i *idTokenVerifier) MaxAgeIAT() time.Duration { + return i.maxAgeIAT +} + +func (i *idTokenVerifier) Offset() time.Duration { + return i.offset +} + +func (i *idTokenVerifier) ClientID() string { + return i.clientID +} + +func (i *idTokenVerifier) SupportedSignAlgs() []string { + return i.supportedSignAlgs +} + +func (i *idTokenVerifier) KeySet() oidc.KeySet { + return i.keySet +} + +func (i *idTokenVerifier) Nonce(ctx context.Context) string { + return i.nonce(ctx) +} + +func (i *idTokenVerifier) ACR() oidc.ACRVerifier { + return i.acr +} + +func (i *idTokenVerifier) MaxAge() time.Duration { + return i.maxAge +} + +//deprecated: Use IDTokenVerifier (or oidc.Verifier) type Verifier interface { //Verify checks the access_token and id_token and returns the `id token claims` diff --git a/pkg/utils/crypto.go b/pkg/utils/crypto.go index 05acb75..15de1f9 100644 --- a/pkg/utils/crypto.go +++ b/pkg/utils/crypto.go @@ -19,7 +19,6 @@ func EncryptAES(data string, key string) (string, error) { } func EncryptBytesAES(plainText []byte, key string) ([]byte, error) { - block, err := aes.NewCipher([]byte(key)) if err != nil { return nil, err @@ -50,7 +49,6 @@ func DecryptAES(data string, key string) (string, error) { } func DecryptBytesAES(cipherText []byte, key string) ([]byte, error) { - block, err := aes.NewCipher([]byte(key)) if err != nil { return nil, err diff --git a/pkg/utils/hash.go b/pkg/utils/hash.go index 78c007f..b7dfd9c 100644 --- a/pkg/utils/hash.go +++ b/pkg/utils/hash.go @@ -24,7 +24,8 @@ func GetHashAlgorithm(sigAlgorithm jose.SignatureAlgorithm) (hash.Hash, error) { } func HashString(hash hash.Hash, s string, firstHalf bool) string { - hash.Write([]byte(s)) // hash documents that Write will never return an error + //nolint:errcheck + hash.Write([]byte(s)) size := hash.Size() if firstHalf { size = size / 2 diff --git a/pkg/utils/http.go b/pkg/utils/http.go index b3ed631..993febb 100644 --- a/pkg/utils/http.go +++ b/pkg/utils/http.go @@ -1,9 +1,11 @@ package utils import ( + "context" "encoding/json" "fmt" "io/ioutil" + "log" "net/http" "net/url" "strings" @@ -25,17 +27,24 @@ type Encoder interface { Encode(src interface{}, dst map[string][]string) error } -func FormRequest(endpoint string, request interface{}) (*http.Request, error) { +func FormRequest(endpoint string, request interface{}, clientID, clientSecret string, header bool) (*http.Request, error) { form := make(map[string][]string) encoder := schema.NewEncoder() if err := encoder.Encode(request, form); err != nil { return nil, err } + if !header { + form["client_id"] = []string{clientID} + form["client_secret"] = []string{clientSecret} + } body := strings.NewReader(url.Values(form).Encode()) req, err := http.NewRequest("POST", endpoint, body) if err != nil { return nil, err } + if header { + req.SetBasicAuth(clientID, clientSecret) + } req.Header.Set("Content-Type", "application/x-www-form-urlencoded") return req, nil } @@ -72,3 +81,18 @@ func URLEncodeResponse(resp interface{}, encoder Encoder) (string, error) { v := url.Values(values) return v.Encode(), nil } + +func StartServer(ctx context.Context, port string) { + server := &http.Server{Addr: port} + go func() { + if err := server.ListenAndServe(); err != http.ErrServerClosed { + log.Fatalf("ListenAndServe(): %v", err) + } + }() + + go func() { + <-ctx.Done() + err := server.Shutdown(ctx) + log.Fatalf("Shutdown(): %v", err) + }() +}