Merge pull request #56 from caos/service-accounts

feat: jwt profile grant
This commit is contained in:
Fabi 2020-09-16 16:58:01 +02:00 committed by GitHub
commit e96815fddc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
44 changed files with 2244 additions and 1465 deletions

View file

@ -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))
}

View file

@ -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.Printf("error creating relaying party: %v", err)
return
}
state := func() string {
return uuid.New().String()
}
token := cli.CodeFlow(relayingParty, callbackPath, port, state)
fmt.Println("OAuth flow success")
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")
}

View file

@ -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 {

View file

@ -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)
}

16
go.mod
View file

@ -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
)

354
go.sum
View file

@ -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=

View file

@ -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 := "<p><strong>Success!</strong></p>"
msg = msg + "<p>You are authenticated and can now return to the CLI.</p>"
fmt.Fprintf(w, msg)
}
return getToken, setToken
}

View file

@ -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"`

View file

@ -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,

View file

@ -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
}

View file

@ -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{

View file

@ -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 {

203
pkg/oidc/verifier.go Normal file
View file

@ -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
}

View file

@ -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 {

View file

@ -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

View file

@ -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
}

View file

@ -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")
}
}
}

View file

@ -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 {

View file

@ -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)
}

View file

@ -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

View file

@ -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)
})
}

View file

@ -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()

View file

@ -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"
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)

View file

@ -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 {

View file

@ -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")
}

View file

@ -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"
)

View file

@ -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 {

View file

@ -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,

View file

@ -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 {

View file

@ -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 {

View file

@ -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
}

View file

@ -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
}

35
pkg/rp/cli/cli.go Normal file
View file

@ -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 := "<p><strong>Success!</strong></p>"
msg = msg + "<p>You are authenticated and can now return to the CLI.</p>"
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
}

View file

@ -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)
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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")
}

View file

@ -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
}

View file

@ -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)

View file

@ -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
}

View file

@ -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`

View file

@ -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

View file

@ -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

View file

@ -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)
}()
}