Merge pull request #56 from caos/service-accounts
feat: jwt profile grant
This commit is contained in:
commit
e96815fddc
44 changed files with 2244 additions and 1465 deletions
|
@ -6,10 +6,10 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"time"
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
|
||||||
"github.com/caos/oidc/pkg/oidc"
|
"github.com/caos/oidc/pkg/oidc"
|
||||||
"github.com/caos/oidc/pkg/rp"
|
"github.com/caos/oidc/pkg/rp"
|
||||||
|
@ -29,41 +29,30 @@ func main() {
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
rpConfig := &rp.Config{
|
redirectURI := fmt.Sprintf("http://localhost:%v%v", port, callbackPath)
|
||||||
ClientID: clientID,
|
scopes := []string{oidc.ScopeOpenID, oidc.ScopeProfile, oidc.ScopeEmail}
|
||||||
ClientSecret: clientSecret,
|
|
||||||
Issuer: issuer,
|
|
||||||
CallbackURL: fmt.Sprintf("http://localhost:%v%v", port, callbackPath),
|
|
||||||
Scopes: []string{"openid", "profile", "email"},
|
|
||||||
}
|
|
||||||
cookieHandler := utils.NewCookieHandler(key, key, utils.WithUnsecure())
|
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 {
|
if err != nil {
|
||||||
logrus.Fatalf("error creating provider %s", err.Error())
|
logrus.Fatalf("error creating provider %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
// state := "foobar"
|
//generate some state (representing the state of the user in your application,
|
||||||
state := uuid.New().String()
|
//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))
|
//register the AuthURLHandler at your preferred path
|
||||||
// http.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) {
|
//the AuthURLHandler creates the auth request and redirects the user to the auth server
|
||||||
// http.Redirect(w, r, provider.AuthURL(state), http.StatusFound)
|
//including state handling with secure cookie and the possibility to use PKCE
|
||||||
// })
|
http.Handle("/login", rp.AuthURLHandler(state, provider))
|
||||||
|
|
||||||
// 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)
|
|
||||||
// })
|
|
||||||
|
|
||||||
|
//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) {
|
marshal := func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens, state string) {
|
||||||
_ = state
|
_ = state
|
||||||
data, err := json.Marshal(tokens)
|
data, err := json.Marshal(tokens)
|
||||||
|
@ -74,10 +63,13 @@ func main() {
|
||||||
w.Write(data)
|
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) {
|
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 {
|
if err != nil {
|
||||||
http.Error(w, "failed to exchange token: "+err.Error(), http.StatusUnauthorized)
|
http.Error(w, "failed to exchange token: "+err.Error(), http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
|
@ -92,5 +84,5 @@ func main() {
|
||||||
})
|
})
|
||||||
lis := fmt.Sprintf("127.0.0.1:%s", port)
|
lis := fmt.Sprintf("127.0.0.1:%s", port)
|
||||||
logrus.Infof("listening on http://%s/", lis)
|
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))
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,11 +3,16 @@ package main
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"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"
|
"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 (
|
var (
|
||||||
|
@ -20,24 +25,32 @@ func main() {
|
||||||
clientSecret := os.Getenv("CLIENT_SECRET")
|
clientSecret := os.Getenv("CLIENT_SECRET")
|
||||||
port := os.Getenv("PORT")
|
port := os.Getenv("PORT")
|
||||||
|
|
||||||
rpConfig := &rp.Config{
|
rpConfig := &oauth2.Config{
|
||||||
ClientID: clientID,
|
ClientID: clientID,
|
||||||
ClientSecret: clientSecret,
|
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"},
|
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()
|
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 {
|
if err != nil {
|
||||||
fmt.Println("OAuth flow failed")
|
fmt.Printf("error creating relaying party: %v", err)
|
||||||
} else {
|
return
|
||||||
|
|
||||||
fmt.Println("OAuth flow success")
|
|
||||||
}
|
}
|
||||||
|
state := func() string {
|
||||||
|
return uuid.New().String()
|
||||||
|
}
|
||||||
|
token := cli.CodeFlow(relayingParty, callbackPath, port, state)
|
||||||
|
|
||||||
|
client := github.NewClient(relayingParty.Client(ctx, token.Token))
|
||||||
|
|
||||||
|
_, _, err = client.Users.Get(ctx, "")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("error %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Println("call succeeded")
|
||||||
}
|
}
|
||||||
|
|
|
@ -151,8 +151,8 @@ func (s *AuthStorage) AuthRequestByID(_ context.Context, id string) (op.AuthRequ
|
||||||
}
|
}
|
||||||
return a, nil
|
return a, nil
|
||||||
}
|
}
|
||||||
func (s *AuthStorage) CreateToken(_ context.Context, authReq op.AuthRequest) (string, time.Time, error) {
|
func (s *AuthStorage) CreateToken(_ context.Context, authReq op.TokenRequest) (string, time.Time, error) {
|
||||||
return authReq.GetID(), time.Now().UTC().Add(5 * time.Minute), nil
|
return "id", time.Now().UTC().Add(5 * time.Minute), nil
|
||||||
}
|
}
|
||||||
func (s *AuthStorage) TerminateSession(_ context.Context, userID, clientID string) error {
|
func (s *AuthStorage) TerminateSession(_ context.Context, userID, clientID string) error {
|
||||||
return nil
|
return nil
|
||||||
|
@ -174,6 +174,10 @@ func (s *AuthStorage) GetKeySet(_ context.Context) (*jose.JSONWebKeySet, error)
|
||||||
},
|
},
|
||||||
}, nil
|
}, 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) {
|
func (s *AuthStorage) GetClientByClientID(_ context.Context, id string) (op.Client, error) {
|
||||||
if id == "none" {
|
if id == "none" {
|
||||||
|
@ -182,20 +186,24 @@ func (s *AuthStorage) GetClientByClientID(_ context.Context, id string) (op.Clie
|
||||||
var appType op.ApplicationType
|
var appType op.ApplicationType
|
||||||
var authMethod op.AuthMethod
|
var authMethod op.AuthMethod
|
||||||
var accessTokenType op.AccessTokenType
|
var accessTokenType op.AccessTokenType
|
||||||
|
var responseTypes []oidc.ResponseType
|
||||||
if id == "web" {
|
if id == "web" {
|
||||||
appType = op.ApplicationTypeWeb
|
appType = op.ApplicationTypeWeb
|
||||||
authMethod = op.AuthMethodBasic
|
authMethod = op.AuthMethodBasic
|
||||||
accessTokenType = op.AccessTokenTypeBearer
|
accessTokenType = op.AccessTokenTypeBearer
|
||||||
|
responseTypes = []oidc.ResponseType{oidc.ResponseTypeCode}
|
||||||
} else if id == "native" {
|
} else if id == "native" {
|
||||||
appType = op.ApplicationTypeNative
|
appType = op.ApplicationTypeNative
|
||||||
authMethod = op.AuthMethodNone
|
authMethod = op.AuthMethodNone
|
||||||
accessTokenType = op.AccessTokenTypeBearer
|
accessTokenType = op.AccessTokenTypeBearer
|
||||||
|
responseTypes = []oidc.ResponseType{oidc.ResponseTypeCode}
|
||||||
} else {
|
} else {
|
||||||
appType = op.ApplicationTypeUserAgent
|
appType = op.ApplicationTypeUserAgent
|
||||||
authMethod = op.AuthMethodNone
|
authMethod = op.AuthMethodNone
|
||||||
accessTokenType = op.AccessTokenTypeJWT
|
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 {
|
func (s *AuthStorage) AuthorizeClientIDSecret(_ context.Context, id string, _ string) error {
|
||||||
|
|
|
@ -21,7 +21,7 @@ func main() {
|
||||||
CryptoKey: sha256.Sum256([]byte("test")),
|
CryptoKey: sha256.Sum256([]byte("test")),
|
||||||
}
|
}
|
||||||
storage := mock.NewAuthStorage()
|
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 {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
16
go.mod
16
go.mod
|
@ -3,23 +3,21 @@ module github.com/caos/oidc
|
||||||
go 1.15
|
go 1.15
|
||||||
|
|
||||||
require (
|
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/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/go-github/v31 v31.0.0
|
||||||
github.com/google/uuid v1.1.2
|
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/mux v1.8.0
|
||||||
github.com/gorilla/schema v1.2.0
|
github.com/gorilla/schema v1.2.0
|
||||||
github.com/gorilla/securecookie v1.1.1
|
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/sirupsen/logrus v1.6.0
|
||||||
github.com/stretchr/testify v1.6.1
|
github.com/stretchr/testify v1.6.1
|
||||||
golang.org/x/crypto v0.0.0-20191128160524-b544559bb6d1 // indirect
|
golang.org/x/net v0.0.0-20200904194848-62affa334b73
|
||||||
golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933
|
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43
|
||||||
golang.org/x/oauth2 v0.0.0-20191122200657-5d9234df094c
|
|
||||||
golang.org/x/text v0.3.3
|
golang.org/x/text v0.3.3
|
||||||
google.golang.org/appengine v1.6.5 // indirect
|
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b // indirect
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
|
|
||||||
gopkg.in/square/go-jose.v2 v2.5.1
|
gopkg.in/square/go-jose.v2 v2.5.1
|
||||||
)
|
)
|
||||||
|
|
354
go.sum
354
go.sum
|
@ -1,33 +1,128 @@
|
||||||
|
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
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=
|
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
|
||||||
github.com/caos/logging v0.0.0-20191210002624-b3260f690a6a/go.mod h1:9LKiDE2ChuGv6CHYif/kiugrfEXu9AwDiFWSreX7Wp0=
|
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.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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
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 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ=
|
||||||
github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
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 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc=
|
||||||
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
|
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.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.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
|
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/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.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 h1:JJUxlP9lFK+ziXKimTCprajMApV1ecWD4NB6CCb0plo=
|
||||||
github.com/google/go-github/v31 v31.0.0/go.mod h1:NQPZol8/1sMoWYGN2yaALIBytu17gAWfhbweiEed3pM=
|
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 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
|
||||||
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
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 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=
|
||||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
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/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||||
github.com/gorilla/handlers v1.5.0/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=
|
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 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
||||||
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
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 h1:YufUaxZYCKGFuAq3c96BOhjgd5nmXiOY9NGzF247Tsc=
|
||||||
github.com/gorilla/schema v1.2.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU=
|
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 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
|
||||||
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
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.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 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
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/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 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
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 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
|
||||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||||
github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I=
|
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 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
|
||||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
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.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 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
||||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
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 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
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-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20191128160524-b544559bb6d1/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
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-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-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 h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628=
|
||||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
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-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-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-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933/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-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-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
golang.org/x/oauth2 v0.0.0-20191122200657-5d9234df094c/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-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-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-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-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-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 h1:FvshnhkKW+LO3HWHodML8kuVX8rnJTxKm9dFPuI68UM=
|
||||||
golang.org/x/sys v0.0.0-20191206220618-eeba5f6aabab/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
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.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 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
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 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
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-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-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 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
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.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 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
|
||||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
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 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM=
|
||||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
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 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-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 h1:7odma5RETjNHWJnR32wx8t+Io4djHE1PqxCFx3iiZ2w=
|
||||||
gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
|
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 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo=
|
||||||
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
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 h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
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=
|
||||||
|
|
117
pkg/cli/cli.go
117
pkg/cli/cli.go
|
@ -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
|
|
||||||
}
|
|
|
@ -1,10 +1,13 @@
|
||||||
package oidc
|
package oidc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"golang.org/x/text/language"
|
"golang.org/x/text/language"
|
||||||
|
"gopkg.in/square/go-jose.v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -64,6 +67,8 @@ const (
|
||||||
|
|
||||||
//GrantTypeCode defines the grant_type `authorization_code` used for the Token Request in the Authorization Code Flow
|
//GrantTypeCode defines the grant_type `authorization_code` used for the Token Request in the Authorization Code Flow
|
||||||
GrantTypeCode GrantType = "authorization_code"
|
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 defines the token_type `Bearer`, which is returned in a successful token response
|
||||||
BearerToken = "Bearer"
|
BearerToken = "Bearer"
|
||||||
|
@ -144,6 +149,72 @@ type AccessTokenResponse struct {
|
||||||
IDToken string `json:"id_token,omitempty" schema:"id_token,omitempty"`
|
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 {
|
type TokenExchangeRequest struct {
|
||||||
subjectToken string `schema:"subject_token"`
|
subjectToken string `schema:"subject_token"`
|
||||||
subjectTokenType string `schema:"subject_token_type"`
|
subjectTokenType string `schema:"subject_token_type"`
|
||||||
|
|
|
@ -22,6 +22,10 @@ type TokenExchangeRequest struct {
|
||||||
requestedTokenType string `schema:"requested_token_type"`
|
requestedTokenType string `schema:"requested_token_type"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type JWTProfileRequest struct {
|
||||||
|
Assertion string `schema:"assertion"`
|
||||||
|
}
|
||||||
|
|
||||||
func NewTokenExchangeRequest(subjectToken, subjectTokenType string, opts ...TokenExchangeOption) *TokenExchangeRequest {
|
func NewTokenExchangeRequest(subjectToken, subjectTokenType string, opts ...TokenExchangeOption) *TokenExchangeRequest {
|
||||||
t := &TokenExchangeRequest{
|
t := &TokenExchangeRequest{
|
||||||
grantType: TokenExchangeGrantType,
|
grantType: TokenExchangeGrantType,
|
||||||
|
|
|
@ -20,3 +20,13 @@ type KeySet interface {
|
||||||
// use any HTTP client associated with the context through ClientContext.
|
// use any HTTP client associated with the context through ClientContext.
|
||||||
VerifySignature(ctx context.Context, jws *jose.JSONWebSignature) (payload []byte, err error)
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ package oidc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"io/ioutil"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -59,6 +60,47 @@ type IDTokenClaims struct {
|
||||||
Signature jose.SignatureAlgorithm //TODO: ???
|
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 {
|
type jsonToken struct {
|
||||||
Issuer string `json:"iss,omitempty"`
|
Issuer string `json:"iss,omitempty"`
|
||||||
Subject string `json:"sub,omitempty"`
|
Subject string `json:"sub,omitempty"`
|
||||||
|
@ -177,6 +219,70 @@ func (t *IDTokenClaims) UnmarshalJSON(b []byte) error {
|
||||||
return nil
|
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 {
|
func (j *jsonToken) UnmarshalUserinfoProfile() UserinfoProfile {
|
||||||
locale, _ := language.Parse(j.Locale)
|
locale, _ := language.Parse(j.Locale)
|
||||||
return UserinfoProfile{
|
return UserinfoProfile{
|
||||||
|
|
|
@ -105,7 +105,7 @@ func (i *Userinfo) UnmmarshalJSON(data []byte) error {
|
||||||
if err := json.Unmarshal(data, i); err != nil {
|
if err := json.Unmarshal(data, i); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return json.Unmarshal(data, i.claims)
|
return json.Unmarshal(data, &i.claims)
|
||||||
}
|
}
|
||||||
|
|
||||||
type jsonUserinfo struct {
|
type jsonUserinfo struct {
|
||||||
|
|
203
pkg/oidc/verifier.go
Normal file
203
pkg/oidc/verifier.go
Normal 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
|
||||||
|
}
|
|
@ -9,7 +9,6 @@ import (
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
|
|
||||||
"github.com/caos/oidc/pkg/oidc"
|
"github.com/caos/oidc/pkg/oidc"
|
||||||
"github.com/caos/oidc/pkg/rp"
|
|
||||||
"github.com/caos/oidc/pkg/utils"
|
"github.com/caos/oidc/pkg/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -18,7 +17,7 @@ type Authorizer interface {
|
||||||
Decoder() utils.Decoder
|
Decoder() utils.Decoder
|
||||||
Encoder() utils.Encoder
|
Encoder() utils.Encoder
|
||||||
Signer() Signer
|
Signer() Signer
|
||||||
IDTokenVerifier() rp.Verifier
|
IDTokenHintVerifier() IDTokenHintVerifier
|
||||||
Crypto() Crypto
|
Crypto() Crypto
|
||||||
Issuer() string
|
Issuer() string
|
||||||
}
|
}
|
||||||
|
@ -27,14 +26,20 @@ type Authorizer interface {
|
||||||
//implementing it's own validation mechanism for the auth request
|
//implementing it's own validation mechanism for the auth request
|
||||||
type AuthorizeValidator interface {
|
type AuthorizeValidator interface {
|
||||||
Authorizer
|
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
|
func authorizeHandler(authorizer Authorizer) func(http.ResponseWriter, *http.Request) {
|
||||||
//implementing it's own validation mechanism for the auth request
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
//
|
Authorize(w, r, authorizer)
|
||||||
//Deprecated: ValidationAuthorizer exists for historical compatibility. Use ValidationAuthorizer itself
|
}
|
||||||
type ValidationAuthorizer AuthorizeValidator
|
}
|
||||||
|
|
||||||
|
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
|
//Authorize handles the authorization request, including
|
||||||
//parsing, validating, storing and finally redirecting to the login handler
|
//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 {
|
if validater, ok := authorizer.(AuthorizeValidator); ok {
|
||||||
validation = validater.ValidateAuthRequest
|
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 {
|
if err != nil {
|
||||||
AuthRequestError(w, r, authReq, err, authorizer.Encoder())
|
AuthRequestError(w, r, authReq, err, authorizer.Encoder())
|
||||||
return
|
return
|
||||||
|
@ -66,6 +71,7 @@ func Authorize(w http.ResponseWriter, r *http.Request, authorizer Authorizer) {
|
||||||
RedirectToLogin(req.GetID(), client, w, r)
|
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) {
|
func ParseAuthorizeRequest(r *http.Request, decoder utils.Decoder) (*oidc.AuthRequest, error) {
|
||||||
err := r.ParseForm()
|
err := r.ParseForm()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -79,7 +85,8 @@ func ParseAuthorizeRequest(r *http.Request, decoder utils.Decoder) (*oidc.AuthRe
|
||||||
return authReq, nil
|
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)
|
client, err := storage.GetClientByClientID(ctx, authReq.ClientID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", ErrServerError(err.Error())
|
return "", ErrServerError(err.Error())
|
||||||
|
@ -96,6 +103,7 @@ func ValidateAuthRequest(ctx context.Context, authReq *oidc.AuthRequest, storage
|
||||||
return ValidateAuthReqIDTokenHint(ctx, authReq.IDTokenHint, verifier)
|
return ValidateAuthReqIDTokenHint(ctx, authReq.IDTokenHint, verifier)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//ValidateAuthReqScopes validates the passed scopes
|
||||||
func ValidateAuthReqScopes(scopes []string) error {
|
func ValidateAuthReqScopes(scopes []string) error {
|
||||||
if len(scopes) == 0 {
|
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.")
|
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
|
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 {
|
func ValidateAuthReqRedirectURI(client Client, uri string, responseType oidc.ResponseType) error {
|
||||||
if uri == "" {
|
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.")
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//ValidateAuthReqResponseType validates the passed response_type to the registered response types
|
||||||
func ValidateAuthReqResponseType(client Client, responseType oidc.ResponseType) error {
|
func ValidateAuthReqResponseType(client Client, responseType oidc.ResponseType) error {
|
||||||
if responseType == "" {
|
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.")
|
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
|
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 == "" {
|
if idTokenHint == "" {
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
claims, err := verifier.VerifyIDToken(ctx, idTokenHint)
|
claims, err := VerifyIDTokenHint(ctx, idTokenHint, verifier)
|
||||||
if err != nil {
|
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 "", ErrInvalidRequest("The id_token_hint is invalid. If you have any questions, you may contact the administrator of the application.")
|
||||||
}
|
}
|
||||||
return claims.Subject, nil
|
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) {
|
func RedirectToLogin(authReqID string, client Client, w http.ResponseWriter, r *http.Request) {
|
||||||
login := client.LoginURL(authReqID)
|
login := client.LoginURL(authReqID)
|
||||||
http.Redirect(w, r, login, http.StatusFound)
|
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) {
|
func AuthorizeCallback(w http.ResponseWriter, r *http.Request, authorizer Authorizer) {
|
||||||
params := mux.Vars(r)
|
params := mux.Vars(r)
|
||||||
id := params["id"]
|
id := params["id"]
|
||||||
|
@ -180,19 +194,21 @@ func AuthorizeCallback(w http.ResponseWriter, r *http.Request, authorizer Author
|
||||||
AuthResponse(authReq, authorizer, w, r)
|
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) {
|
func AuthResponse(authReq AuthRequest, authorizer Authorizer, w http.ResponseWriter, r *http.Request) {
|
||||||
client, err := authorizer.Storage().GetClientByClientID(r.Context(), authReq.GetClientID())
|
client, err := authorizer.Storage().GetClientByClientID(r.Context(), authReq.GetClientID())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
AuthRequestError(w, r, authReq, err, authorizer.Encoder())
|
||||||
|
return
|
||||||
}
|
}
|
||||||
if authReq.GetResponseType() == oidc.ResponseTypeCode {
|
if authReq.GetResponseType() == oidc.ResponseTypeCode {
|
||||||
AuthResponseCode(w, r, authReq, authorizer)
|
AuthResponseCode(w, r, authReq, authorizer)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
AuthResponseToken(w, r, authReq, authorizer, client)
|
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) {
|
func AuthResponseCode(w http.ResponseWriter, r *http.Request, authReq AuthRequest, authorizer Authorizer) {
|
||||||
code, err := CreateAuthRequestCode(r.Context(), authReq, authorizer.Storage(), authorizer.Crypto())
|
code, err := CreateAuthRequestCode(r.Context(), authReq, authorizer.Storage(), authorizer.Crypto())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -206,6 +222,7 @@ func AuthResponseCode(w http.ResponseWriter, r *http.Request, authReq AuthReques
|
||||||
http.Redirect(w, r, callback, http.StatusFound)
|
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) {
|
func AuthResponseToken(w http.ResponseWriter, r *http.Request, authReq AuthRequest, authorizer Authorizer, client Client) {
|
||||||
createAccessToken := authReq.GetResponseType() != oidc.ResponseTypeIDTokenOnly
|
createAccessToken := authReq.GetResponseType() != oidc.ResponseTypeIDTokenOnly
|
||||||
resp, err := CreateTokenResponse(r.Context(), authReq, client, authorizer, createAccessToken, "")
|
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)
|
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) {
|
func CreateAuthRequestCode(ctx context.Context, authReq AuthRequest, storage Storage, crypto Crypto) (string, error) {
|
||||||
code, err := BuildAuthRequestCode(authReq, crypto)
|
code, err := BuildAuthRequestCode(authReq, crypto)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -13,7 +13,6 @@ import (
|
||||||
"github.com/caos/oidc/pkg/oidc"
|
"github.com/caos/oidc/pkg/oidc"
|
||||||
"github.com/caos/oidc/pkg/op"
|
"github.com/caos/oidc/pkg/op"
|
||||||
"github.com/caos/oidc/pkg/op/mock"
|
"github.com/caos/oidc/pkg/op/mock"
|
||||||
"github.com/caos/oidc/pkg/rp"
|
|
||||||
"github.com/caos/oidc/pkg/utils"
|
"github.com/caos/oidc/pkg/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -145,7 +144,7 @@ func TestValidateAuthRequest(t *testing.T) {
|
||||||
type args struct {
|
type args struct {
|
||||||
authRequest *oidc.AuthRequest
|
authRequest *oidc.AuthRequest
|
||||||
storage op.Storage
|
storage op.Storage
|
||||||
verifier rp.Verifier
|
verifier op.IDTokenHintVerifier
|
||||||
}
|
}
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
|
|
@ -15,6 +15,12 @@ const (
|
||||||
AccessTokenTypeJWT
|
AccessTokenTypeJWT
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type ApplicationType int
|
||||||
|
|
||||||
|
type AuthMethod string
|
||||||
|
|
||||||
|
type AccessTokenType int
|
||||||
|
|
||||||
type Client interface {
|
type Client interface {
|
||||||
GetID() string
|
GetID() string
|
||||||
RedirectURIs() []string
|
RedirectURIs() []string
|
||||||
|
@ -28,10 +34,6 @@ type Client interface {
|
||||||
DevMode() bool
|
DevMode() bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func IsConfidentialType(c Client) bool {
|
|
||||||
return c.ApplicationType() == ApplicationTypeWeb
|
|
||||||
}
|
|
||||||
|
|
||||||
func ContainsResponseType(types []oidc.ResponseType, responseType oidc.ResponseType) bool {
|
func ContainsResponseType(types []oidc.ResponseType, responseType oidc.ResponseType) bool {
|
||||||
for _, t := range types {
|
for _, t := range types {
|
||||||
if t == responseType {
|
if t == responseType {
|
||||||
|
@ -41,8 +43,6 @@ func ContainsResponseType(types []oidc.ResponseType, responseType oidc.ResponseT
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
type ApplicationType int
|
func IsConfidentialType(c Client) bool {
|
||||||
|
return c.ApplicationType() == ApplicationTypeWeb
|
||||||
type AuthMethod string
|
}
|
||||||
|
|
||||||
type AccessTokenType int
|
|
||||||
|
|
|
@ -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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -7,6 +7,12 @@ import (
|
||||||
"github.com/caos/oidc/pkg/utils"
|
"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) {
|
func Discover(w http.ResponseWriter, config *oidc.DiscoveryConfiguration) {
|
||||||
utils.MarshalJSON(w, config)
|
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{
|
var DefaultSupportedScopes = []string{
|
||||||
ScopeOpenID,
|
oidc.ScopeOpenID,
|
||||||
ScopeProfile,
|
oidc.ScopeProfile,
|
||||||
ScopeEmail,
|
oidc.ScopeEmail,
|
||||||
ScopePhone,
|
oidc.ScopePhone,
|
||||||
ScopeAddress,
|
oidc.ScopeAddress,
|
||||||
}
|
}
|
||||||
|
|
||||||
func Scopes(c Configuration) []string {
|
func Scopes(c Configuration) []string {
|
||||||
|
|
|
@ -10,10 +10,18 @@ type KeyProvider interface {
|
||||||
Storage() Storage
|
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) {
|
func Keys(w http.ResponseWriter, r *http.Request, k KeyProvider) {
|
||||||
keySet, err := k.Storage().GetKeySet(r.Context())
|
keySet, err := k.Storage().GetKeySet(r.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
utils.MarshalJSON(w, err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
utils.MarshalJSON(w, keySet)
|
utils.MarshalJSON(w, keySet)
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,6 @@ package mock
|
||||||
|
|
||||||
import (
|
import (
|
||||||
op "github.com/caos/oidc/pkg/op"
|
op "github.com/caos/oidc/pkg/op"
|
||||||
rp "github.com/caos/oidc/pkg/rp"
|
|
||||||
utils "github.com/caos/oidc/pkg/utils"
|
utils "github.com/caos/oidc/pkg/utils"
|
||||||
gomock "github.com/golang/mock/gomock"
|
gomock "github.com/golang/mock/gomock"
|
||||||
reflect "reflect"
|
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))
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Encoder", reflect.TypeOf((*MockAuthorizer)(nil).Encoder))
|
||||||
}
|
}
|
||||||
|
|
||||||
// IDTokenVerifier mocks base method
|
// IDTokenHintVerifier mocks base method
|
||||||
func (m *MockAuthorizer) IDTokenVerifier() rp.Verifier {
|
func (m *MockAuthorizer) IDTokenHintVerifier() op.IDTokenHintVerifier {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
ret := m.ctrl.Call(m, "IDTokenVerifier")
|
ret := m.ctrl.Call(m, "IDTokenHintVerifier")
|
||||||
ret0, _ := ret[0].(rp.Verifier)
|
ret0, _ := ret[0].(op.IDTokenHintVerifier)
|
||||||
return ret0
|
return ret0
|
||||||
}
|
}
|
||||||
|
|
||||||
// IDTokenVerifier indicates an expected call of IDTokenVerifier
|
// IDTokenHintVerifier indicates an expected call of IDTokenHintVerifier
|
||||||
func (mr *MockAuthorizerMockRecorder) IDTokenVerifier() *gomock.Call {
|
func (mr *MockAuthorizerMockRecorder) IDTokenHintVerifier() *gomock.Call {
|
||||||
mr.mock.ctrl.T.Helper()
|
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
|
// Issuer mocks base method
|
||||||
|
|
|
@ -10,7 +10,6 @@ import (
|
||||||
|
|
||||||
"github.com/caos/oidc/pkg/oidc"
|
"github.com/caos/oidc/pkg/oidc"
|
||||||
"github.com/caos/oidc/pkg/op"
|
"github.com/caos/oidc/pkg/op"
|
||||||
"github.com/caos/oidc/pkg/rp"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewAuthorizer(t *testing.T) op.Authorizer {
|
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) {
|
func ExpectVerifier(a op.Authorizer, t *testing.T) {
|
||||||
mockA := a.(*MockAuthorizer)
|
mockA := a.(*MockAuthorizer)
|
||||||
mockA.EXPECT().IDTokenVerifier().DoAndReturn(
|
mockA.EXPECT().IDTokenHintVerifier().DoAndReturn(
|
||||||
func() rp.Verifier {
|
func() op.IDTokenHintVerifier {
|
||||||
return &Verifier{}
|
return op.NewIDTokenHintVerifier("", nil)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -97,7 +97,7 @@ func (mr *MockStorageMockRecorder) CreateAuthRequest(arg0, arg1, arg2 interface{
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateToken mocks base method
|
// 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()
|
m.ctrl.T.Helper()
|
||||||
ret := m.ctrl.Call(m, "CreateToken", arg0, arg1)
|
ret := m.ctrl.Call(m, "CreateToken", arg0, arg1)
|
||||||
ret0, _ := ret[0].(string)
|
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)
|
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
|
// GetKeySet mocks base method
|
||||||
func (m *MockStorage) GetKeySet(arg0 context.Context) (*jose.JSONWebKeySet, error) {
|
func (m *MockStorage) GetKeySet(arg0 context.Context) (*jose.JSONWebKeySet, error) {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
|
|
371
pkg/op/op.go
371
pkg/op/op.go
|
@ -1,29 +1,60 @@
|
||||||
package op
|
package op
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/caos/logging"
|
||||||
"github.com/gorilla/handlers"
|
"github.com/gorilla/handlers"
|
||||||
"github.com/gorilla/mux"
|
"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/oidc"
|
||||||
|
"github.com/caos/oidc/pkg/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
healthzEndpoint = "/healthz"
|
healthzEndpoint = "/healthz"
|
||||||
readinessEndpoint = "/ready"
|
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 {
|
type OpenIDProvider interface {
|
||||||
Configuration
|
Configuration
|
||||||
HandleReady(w http.ResponseWriter, r *http.Request)
|
Storage() Storage
|
||||||
HandleDiscovery(w http.ResponseWriter, r *http.Request)
|
Decoder() utils.Decoder
|
||||||
HandleAuthorize(w http.ResponseWriter, r *http.Request)
|
Encoder() utils.Encoder
|
||||||
HandleAuthorizeCallback(w http.ResponseWriter, r *http.Request)
|
IDTokenHintVerifier() IDTokenHintVerifier
|
||||||
HandleExchange(w http.ResponseWriter, r *http.Request)
|
JWTProfileVerifier() JWTProfileVerifier
|
||||||
HandleUserinfo(w http.ResponseWriter, r *http.Request)
|
Crypto() Crypto
|
||||||
HandleEndSession(w http.ResponseWriter, r *http.Request)
|
DefaultLogoutRedirectURI() string
|
||||||
HandleKeys(w http.ResponseWriter, r *http.Request)
|
Signer() Signer
|
||||||
|
Probes() []ProbesFn
|
||||||
HttpHandler() http.Handler
|
HttpHandler() http.Handler
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,18 +72,320 @@ func CreateRouter(o OpenIDProvider, interceptors ...HttpInterceptor) *mux.Router
|
||||||
handlers.AllowedHeaders([]string{"authorization", "content-type"}),
|
handlers.AllowedHeaders([]string{"authorization", "content-type"}),
|
||||||
handlers.AllowedOriginValidator(allowAllOrigins),
|
handlers.AllowedOriginValidator(allowAllOrigins),
|
||||||
))
|
))
|
||||||
router.HandleFunc(healthzEndpoint, Healthz)
|
router.HandleFunc(healthzEndpoint, healthzHandler)
|
||||||
router.HandleFunc(readinessEndpoint, o.HandleReady)
|
router.HandleFunc(readinessEndpoint, readyHandler(o.Probes()))
|
||||||
router.HandleFunc(oidc.DiscoveryEndpoint, o.HandleDiscovery)
|
router.HandleFunc(oidc.DiscoveryEndpoint, discoveryHandler(o, o.Signer()))
|
||||||
router.Handle(o.AuthorizationEndpoint().Relative(), intercept(o.HandleAuthorize))
|
router.Handle(o.AuthorizationEndpoint().Relative(), intercept(authorizeHandler(o)))
|
||||||
router.Handle(o.AuthorizationEndpoint().Relative()+"/{id}", intercept(o.HandleAuthorizeCallback))
|
router.Handle(o.AuthorizationEndpoint().Relative()+"/{id}", intercept(authorizeCallbackHandler(o)))
|
||||||
router.Handle(o.TokenEndpoint().Relative(), intercept(o.HandleExchange))
|
router.Handle(o.TokenEndpoint().Relative(), intercept(tokenHandler(o)))
|
||||||
router.HandleFunc(o.UserinfoEndpoint().Relative(), o.HandleUserinfo)
|
router.HandleFunc(o.UserinfoEndpoint().Relative(), userinfoHandler(o))
|
||||||
router.Handle(o.EndSessionEndpoint().Relative(), intercept(o.HandleEndSession))
|
router.Handle(o.EndSessionEndpoint().Relative(), intercept(endSessionHandler(o)))
|
||||||
router.HandleFunc(o.KeysEndpoint().Relative(), o.HandleKeys)
|
router.HandleFunc(o.KeysEndpoint().Relative(), keysHandler(o))
|
||||||
return router
|
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 {
|
func buildInterceptor(interceptors ...HttpInterceptor) func(http.HandlerFunc) http.Handler {
|
||||||
return func(handlerFunc http.HandlerFunc) http.Handler {
|
return func(handlerFunc http.HandlerFunc) http.Handler {
|
||||||
handler := handlerFuncToHandler(handlerFunc)
|
handler := handlerFuncToHandler(handlerFunc)
|
||||||
|
|
|
@ -10,10 +10,16 @@ import (
|
||||||
|
|
||||||
type ProbesFn func(context.Context) error
|
type ProbesFn func(context.Context) error
|
||||||
|
|
||||||
func Healthz(w http.ResponseWriter, r *http.Request) {
|
func healthzHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
ok(w)
|
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) {
|
func Readiness(w http.ResponseWriter, r *http.Request, probes ...ProbesFn) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
for _, probe := range probes {
|
for _, probe := range probes {
|
||||||
|
|
|
@ -5,17 +5,22 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/caos/oidc/pkg/oidc"
|
"github.com/caos/oidc/pkg/oidc"
|
||||||
"github.com/caos/oidc/pkg/rp"
|
|
||||||
"github.com/caos/oidc/pkg/utils"
|
"github.com/caos/oidc/pkg/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SessionEnder interface {
|
type SessionEnder interface {
|
||||||
Decoder() utils.Decoder
|
Decoder() utils.Decoder
|
||||||
Storage() Storage
|
Storage() Storage
|
||||||
IDTokenVerifier() rp.Verifier
|
IDTokenHintVerifier() IDTokenHintVerifier
|
||||||
DefaultLogoutRedirectURI() string
|
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) {
|
func EndSession(w http.ResponseWriter, r *http.Request, ender SessionEnder) {
|
||||||
req, err := ParseEndSessionRequest(r, ender.Decoder())
|
req, err := ParseEndSessionRequest(r, ender.Decoder())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -57,7 +62,7 @@ func ValidateEndSessionRequest(ctx context.Context, req *oidc.EndSessionRequest,
|
||||||
if req.IdTokenHint == "" {
|
if req.IdTokenHint == "" {
|
||||||
return session, nil
|
return session, nil
|
||||||
}
|
}
|
||||||
claims, err := ender.IDTokenVerifier().VerifyIDToken(ctx, req.IdTokenHint)
|
claims, err := VerifyIDTokenHint(ctx, req.IdTokenHint, ender.IDTokenHintVerifier())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, ErrInvalidRequest("id_token_hint invalid")
|
return nil, ErrInvalidRequest("id_token_hint invalid")
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,12 @@
|
||||||
package op
|
package op
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
"golang.org/x/net/context"
|
|
||||||
"gopkg.in/square/go-jose.v2"
|
|
||||||
|
|
||||||
"github.com/caos/logging"
|
"github.com/caos/logging"
|
||||||
|
"gopkg.in/square/go-jose.v2"
|
||||||
|
|
||||||
"github.com/caos/oidc/pkg/oidc"
|
"github.com/caos/oidc/pkg/oidc"
|
||||||
)
|
)
|
||||||
|
|
|
@ -16,7 +16,7 @@ type AuthStorage interface {
|
||||||
SaveAuthCode(context.Context, string, string) error
|
SaveAuthCode(context.Context, string, string) error
|
||||||
DeleteAuthRequest(context.Context, 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
|
TerminateSession(context.Context, string, string) error
|
||||||
|
|
||||||
|
@ -30,6 +30,7 @@ type OPStorage interface {
|
||||||
AuthorizeClientIDSecret(context.Context, string, string) error
|
AuthorizeClientIDSecret(context.Context, string, string) error
|
||||||
GetUserinfoFromScopes(context.Context, string, []string) (*oidc.Userinfo, error)
|
GetUserinfoFromScopes(context.Context, string, []string) (*oidc.Userinfo, error)
|
||||||
GetUserinfoFromToken(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 {
|
type Storage interface {
|
||||||
|
|
|
@ -14,12 +14,18 @@ type TokenCreator interface {
|
||||||
Crypto() Crypto
|
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) {
|
func CreateTokenResponse(ctx context.Context, authReq AuthRequest, client Client, creator TokenCreator, createAccessToken bool, code string) (*oidc.AccessTokenResponse, error) {
|
||||||
var accessToken string
|
var accessToken string
|
||||||
var validity time.Duration
|
var validity time.Duration
|
||||||
if createAccessToken {
|
if createAccessToken {
|
||||||
var err error
|
var err error
|
||||||
accessToken, validity, err = CreateAccessToken(ctx, authReq, client, creator)
|
accessToken, validity, err = CreateAccessToken(ctx, authReq, client.AccessTokenType(), creator)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -43,13 +49,27 @@ func CreateTokenResponse(ctx context.Context, authReq AuthRequest, client Client
|
||||||
}, nil
|
}, 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)
|
id, exp, err := creator.Storage().CreateToken(ctx, authReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", 0, err
|
return "", 0, err
|
||||||
}
|
}
|
||||||
validity = exp.Sub(time.Now().UTC())
|
validity = exp.Sub(time.Now().UTC())
|
||||||
if client.AccessTokenType() == AccessTokenTypeJWT {
|
if accessTokenType == AccessTokenTypeJWT {
|
||||||
token, err = CreateJWT(creator.Issuer(), authReq, exp, id, creator.Signer())
|
token, err = CreateJWT(creator.Issuer(), authReq, exp, id, creator.Signer())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -61,7 +81,7 @@ func CreateBearerToken(id string, crypto Crypto) (string, error) {
|
||||||
return crypto.Encrypt(id)
|
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()
|
now := time.Now().UTC()
|
||||||
nbf := now
|
nbf := now
|
||||||
claims := &oidc.AccessTokenClaims{
|
claims := &oidc.AccessTokenClaims{
|
||||||
|
@ -81,7 +101,7 @@ func CreateIDToken(ctx context.Context, issuer string, authReq AuthRequest, vali
|
||||||
exp := time.Now().UTC().Add(validity)
|
exp := time.Now().UTC().Add(validity)
|
||||||
userinfo, err := storage.GetUserinfoFromScopes(ctx, authReq.GetSubject(), authReq.GetScopes())
|
userinfo, err := storage.GetUserinfoFromScopes(ctx, authReq.GetSubject(), authReq.GetScopes())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
return "", err
|
||||||
}
|
}
|
||||||
claims := &oidc.IDTokenClaims{
|
claims := &oidc.IDTokenClaims{
|
||||||
Issuer: issuer,
|
Issuer: issuer,
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/caos/oidc/pkg/oidc"
|
"github.com/caos/oidc/pkg/oidc"
|
||||||
|
"github.com/caos/oidc/pkg/oidc/grants/tokenexchange"
|
||||||
"github.com/caos/oidc/pkg/utils"
|
"github.com/caos/oidc/pkg/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -16,6 +17,28 @@ type Exchanger interface {
|
||||||
Signer() Signer
|
Signer() Signer
|
||||||
Crypto() Crypto
|
Crypto() Crypto
|
||||||
AuthMethodPostSupported() bool
|
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) {
|
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
|
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) {
|
func TokenExchange(w http.ResponseWriter, r *http.Request, exchanger Exchanger) {
|
||||||
tokenRequest, err := ParseTokenExchangeRequest(w, r)
|
tokenRequest, err := ParseTokenExchangeRequest(w, r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -15,6 +15,12 @@ type UserinfoProvider interface {
|
||||||
Storage() Storage
|
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) {
|
func Userinfo(w http.ResponseWriter, r *http.Request, userinfoProvider UserinfoProvider) {
|
||||||
accessToken, err := getAccessToken(r, userinfoProvider.Decoder())
|
accessToken, err := getAccessToken(r, userinfoProvider.Decoder())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
102
pkg/op/verifier_id_token_hint.go
Normal file
102
pkg/op/verifier_id_token_hint.go
Normal 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
|
||||||
|
}
|
101
pkg/op/verifier_jwt_profile.go
Normal file
101
pkg/op/verifier_jwt_profile.go
Normal 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
35
pkg/rp/cli/cli.go
Normal 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
|
||||||
|
}
|
|
@ -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)
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -74,7 +74,7 @@ func (r *remoteKeySet) VerifySignature(ctx context.Context, jws *jose.JSONWebSig
|
||||||
}
|
}
|
||||||
|
|
||||||
keys := r.keysFromCache()
|
keys := r.keysFromCache()
|
||||||
payload, err, ok := CheckKey(keyID, keys, jws)
|
payload, err, ok := oidc.CheckKey(keyID, jws, keys...)
|
||||||
if ok {
|
if ok {
|
||||||
return payload, err
|
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)
|
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 {
|
if !ok {
|
||||||
return nil, errors.New("invalid kid")
|
return nil, errors.New("invalid kid")
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -2,66 +2,369 @@ package rp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
|
||||||
"github.com/caos/oidc/pkg/oidc"
|
"github.com/caos/oidc/pkg/oidc"
|
||||||
|
"github.com/caos/oidc/pkg/oidc/grants"
|
||||||
|
"github.com/caos/oidc/pkg/utils"
|
||||||
|
|
||||||
"golang.org/x/oauth2"
|
"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
|
//RelayingParty declares the minimal interface for oidc clients
|
||||||
type RelayingParty interface {
|
type RelayingParty interface {
|
||||||
//Client return a standard http client where the token can be used
|
//OAuthConfig returns the oauth2 Config
|
||||||
Client(ctx context.Context, token *oauth2.Token) *http.Client
|
OAuthConfig() *oauth2.Config
|
||||||
|
|
||||||
//AuthURL returns the authorization endpoint with a given state
|
//IsPKCE returns if authorization is done using `Authorization Code Flow with Proof Key for Code Exchange (PKCE)`
|
||||||
AuthURL(state string, opts ...AuthURLOpt) string
|
IsPKCE() bool
|
||||||
|
|
||||||
//AuthURLHandler should implement the AuthURL func as http.HandlerFunc
|
//CookieHandler returns a http cookie handler used for various state transfer cookies
|
||||||
//(redirecting to the auth endpoint)
|
CookieHandler() *utils.CookieHandler
|
||||||
AuthURLHandler(state string) http.HandlerFunc
|
|
||||||
|
|
||||||
//CodeExchange implements the OIDC Token Request (oauth2 Authorization Code Grant)
|
//HttpClient returns a http client used for calls to the openid provider, e.g. calling token endpoint
|
||||||
//returning an `Access Token` and `ID Token Claims`
|
HttpClient() *http.Client
|
||||||
CodeExchange(ctx context.Context, code string, opts ...CodeExchangeOpt) (*oidc.Tokens, error)
|
|
||||||
|
|
||||||
//CodeExchangeHandler extends the CodeExchange func,
|
//IsOAuth2Only specifies whether relaying party handles only oauth2 or oidc calls
|
||||||
//calling the provided callback func on success with additional returned `state`
|
IsOAuth2Only() bool
|
||||||
CodeExchangeHandler(callback func(http.ResponseWriter, *http.Request, *oidc.Tokens, string)) http.HandlerFunc
|
|
||||||
|
|
||||||
//ClientCredentials implements the oauth2 Client Credentials Grant
|
//IDTokenVerifier returns the verifier interface used for oidc id_token verification
|
||||||
//requesting an `Access Token` for the client itself, without user context
|
IDTokenVerifier() IDTokenVerifier
|
||||||
ClientCredentials(ctx context.Context, scopes ...string) (*oauth2.Token, error)
|
|
||||||
|
|
||||||
//Introspects calls the Introspect Endpoint
|
//ErrorHandler returns the handler used for callback errors
|
||||||
//for validating an (access) token
|
ErrorHandler() func(http.ResponseWriter, *http.Request, string, string, string)
|
||||||
// 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()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//PasswortGrantRP extends the `RelayingParty` interface with the oauth2 `Password Grant`
|
type ErrorHandler func(w http.ResponseWriter, r *http.Request, errorType string, errorDesc string, state string)
|
||||||
//
|
|
||||||
//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
|
|
||||||
|
|
||||||
//PasswordGrant implements the oauth2 `Password Grant`,
|
var (
|
||||||
//requesting an access token with the users `username` and `password`
|
DefaultErrorHandler ErrorHandler = func(w http.ResponseWriter, r *http.Request, errorType string, errorDesc string, state string) {
|
||||||
PasswordGrant(context.Context, string, string) (*oauth2.Token, error)
|
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 {
|
func (rp *relayingParty) OAuthConfig() *oauth2.Config {
|
||||||
ClientID string
|
return rp.oauthConfig
|
||||||
ClientSecret string
|
}
|
||||||
CallbackURL string
|
|
||||||
Issuer string
|
func (rp *relayingParty) IsPKCE() bool {
|
||||||
Scopes []string
|
return rp.pkce
|
||||||
Endpoints oauth2.Endpoint
|
}
|
||||||
|
|
||||||
|
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)
|
type OptionFunc func(RelayingParty)
|
||||||
|
|
|
@ -2,9 +2,15 @@ package rp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/json"
|
||||||
|
"encoding/pem"
|
||||||
|
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
|
"gopkg.in/square/go-jose.v2"
|
||||||
|
|
||||||
|
"github.com/caos/oidc/pkg/oidc"
|
||||||
"github.com/caos/oidc/pkg/oidc/grants/tokenexchange"
|
"github.com/caos/oidc/pkg/oidc/grants/tokenexchange"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -12,7 +18,7 @@ import (
|
||||||
type TokenExchangeRP interface {
|
type TokenExchangeRP interface {
|
||||||
RelayingParty
|
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)
|
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
|
//providing an access token in request for a `delegation` token for a given resource / audience
|
||||||
DelegationTokenExchange(context.Context, string, ...tokenexchange.TokenExchangeOption) (*oauth2.Token, error)
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -2,12 +2,220 @@ package rp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gopkg.in/square/go-jose.v2"
|
||||||
|
|
||||||
"github.com/caos/oidc/pkg/oidc"
|
"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
|
//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 {
|
type Verifier interface {
|
||||||
|
|
||||||
//Verify checks the access_token and id_token and returns the `id token claims`
|
//Verify checks the access_token and id_token and returns the `id token claims`
|
||||||
|
|
|
@ -19,7 +19,6 @@ func EncryptAES(data string, key string) (string, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func EncryptBytesAES(plainText []byte, key string) ([]byte, error) {
|
func EncryptBytesAES(plainText []byte, key string) ([]byte, error) {
|
||||||
|
|
||||||
block, err := aes.NewCipher([]byte(key))
|
block, err := aes.NewCipher([]byte(key))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -50,7 +49,6 @@ func DecryptAES(data string, key string) (string, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func DecryptBytesAES(cipherText []byte, key string) ([]byte, error) {
|
func DecryptBytesAES(cipherText []byte, key string) ([]byte, error) {
|
||||||
|
|
||||||
block, err := aes.NewCipher([]byte(key))
|
block, err := aes.NewCipher([]byte(key))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
@ -24,7 +24,8 @@ func GetHashAlgorithm(sigAlgorithm jose.SignatureAlgorithm) (hash.Hash, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func HashString(hash hash.Hash, s string, firstHalf bool) string {
|
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()
|
size := hash.Size()
|
||||||
if firstHalf {
|
if firstHalf {
|
||||||
size = size / 2
|
size = size / 2
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
package utils
|
package utils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -25,17 +27,24 @@ type Encoder interface {
|
||||||
Encode(src interface{}, dst map[string][]string) error
|
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)
|
form := make(map[string][]string)
|
||||||
encoder := schema.NewEncoder()
|
encoder := schema.NewEncoder()
|
||||||
if err := encoder.Encode(request, form); err != nil {
|
if err := encoder.Encode(request, form); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if !header {
|
||||||
|
form["client_id"] = []string{clientID}
|
||||||
|
form["client_secret"] = []string{clientSecret}
|
||||||
|
}
|
||||||
body := strings.NewReader(url.Values(form).Encode())
|
body := strings.NewReader(url.Values(form).Encode())
|
||||||
req, err := http.NewRequest("POST", endpoint, body)
|
req, err := http.NewRequest("POST", endpoint, body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if header {
|
||||||
|
req.SetBasicAuth(clientID, clientSecret)
|
||||||
|
}
|
||||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
return req, nil
|
return req, nil
|
||||||
}
|
}
|
||||||
|
@ -72,3 +81,18 @@ func URLEncodeResponse(resp interface{}, encoder Encoder) (string, error) {
|
||||||
v := url.Values(values)
|
v := url.Values(values)
|
||||||
return v.Encode(), nil
|
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)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue