Merge pull request #1473 from alindeman/add-user-endpoint
Add UserInfo endpoint
This commit is contained in:
commit
8b4dbb9fe7
2
go.mod
2
go.mod
@ -6,7 +6,7 @@ require (
|
|||||||
github.com/boltdb/bolt v1.3.1 // indirect
|
github.com/boltdb/bolt v1.3.1 // indirect
|
||||||
github.com/cockroachdb/cmux v0.0.0-20170110192607-30d10be49292 // indirect
|
github.com/cockroachdb/cmux v0.0.0-20170110192607-30d10be49292 // indirect
|
||||||
github.com/coreos/etcd v3.2.9+incompatible
|
github.com/coreos/etcd v3.2.9+incompatible
|
||||||
github.com/coreos/go-oidc v0.0.0-20170307191026-be73733bb8cc
|
github.com/coreos/go-oidc v2.0.0+incompatible
|
||||||
github.com/coreos/go-semver v0.2.0 // indirect
|
github.com/coreos/go-semver v0.2.0 // indirect
|
||||||
github.com/coreos/go-systemd v0.0.0-20181031085051-9002847aa142 // indirect
|
github.com/coreos/go-systemd v0.0.0-20181031085051-9002847aa142 // indirect
|
||||||
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f // indirect
|
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f // indirect
|
||||||
|
4
go.sum
4
go.sum
@ -8,8 +8,8 @@ github.com/cockroachdb/cmux v0.0.0-20170110192607-30d10be49292 h1:dzj1/xcivGjNPw
|
|||||||
github.com/cockroachdb/cmux v0.0.0-20170110192607-30d10be49292/go.mod h1:qRiX68mZX1lGBkTWyp3CLcenw9I94W2dLeRvMzcn9N4=
|
github.com/cockroachdb/cmux v0.0.0-20170110192607-30d10be49292/go.mod h1:qRiX68mZX1lGBkTWyp3CLcenw9I94W2dLeRvMzcn9N4=
|
||||||
github.com/coreos/etcd v3.2.9+incompatible h1:3TbjfK5+aSRLTU/KgBC1xlgA2dn2ddYQngRqX6HFwlQ=
|
github.com/coreos/etcd v3.2.9+incompatible h1:3TbjfK5+aSRLTU/KgBC1xlgA2dn2ddYQngRqX6HFwlQ=
|
||||||
github.com/coreos/etcd v3.2.9+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
github.com/coreos/etcd v3.2.9+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||||
github.com/coreos/go-oidc v0.0.0-20170307191026-be73733bb8cc h1:9yuvA19Q5WFkLwJcMDoYm8m89ilzqZ5zEHqdvU+Zbds=
|
github.com/coreos/go-oidc v2.0.0+incompatible h1:+RStIopZ8wooMx+Vs5Bt8zMXxV1ABl5LbakNExNmZIg=
|
||||||
github.com/coreos/go-oidc v0.0.0-20170307191026-be73733bb8cc/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=
|
github.com/coreos/go-oidc v2.0.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=
|
||||||
github.com/coreos/go-semver v0.2.0 h1:3Jm3tLmsgAYcjC+4Up7hJrFBPr+n7rAqYeSw/SZazuY=
|
github.com/coreos/go-semver v0.2.0 h1:3Jm3tLmsgAYcjC+4Up7hJrFBPr+n7rAqYeSw/SZazuY=
|
||||||
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||||
github.com/coreos/go-systemd v0.0.0-20181031085051-9002847aa142 h1:3jFq2xL4ZajGK4aZY8jz+DAF0FHjI51BXjjSwCzS1Dk=
|
github.com/coreos/go-systemd v0.0.0-20181031085051-9002847aa142 h1:3jFq2xL4ZajGK4aZY8jz+DAF0FHjI51BXjjSwCzS1Dk=
|
||||||
|
@ -14,6 +14,7 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
oidc "github.com/coreos/go-oidc"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
jose "gopkg.in/square/go-jose.v2"
|
jose "gopkg.in/square/go-jose.v2"
|
||||||
|
|
||||||
@ -151,6 +152,7 @@ type discovery struct {
|
|||||||
Auth string `json:"authorization_endpoint"`
|
Auth string `json:"authorization_endpoint"`
|
||||||
Token string `json:"token_endpoint"`
|
Token string `json:"token_endpoint"`
|
||||||
Keys string `json:"jwks_uri"`
|
Keys string `json:"jwks_uri"`
|
||||||
|
UserInfo string `json:"userinfo_endpoint"`
|
||||||
ResponseTypes []string `json:"response_types_supported"`
|
ResponseTypes []string `json:"response_types_supported"`
|
||||||
Subjects []string `json:"subject_types_supported"`
|
Subjects []string `json:"subject_types_supported"`
|
||||||
IDTokenAlgs []string `json:"id_token_signing_alg_values_supported"`
|
IDTokenAlgs []string `json:"id_token_signing_alg_values_supported"`
|
||||||
@ -165,6 +167,7 @@ func (s *Server) discoveryHandler() (http.HandlerFunc, error) {
|
|||||||
Auth: s.absURL("/auth"),
|
Auth: s.absURL("/auth"),
|
||||||
Token: s.absURL("/token"),
|
Token: s.absURL("/token"),
|
||||||
Keys: s.absURL("/keys"),
|
Keys: s.absURL("/keys"),
|
||||||
|
UserInfo: s.absURL("/userinfo"),
|
||||||
Subjects: []string{"public"},
|
Subjects: []string{"public"},
|
||||||
IDTokenAlgs: []string{string(jose.RS256)},
|
IDTokenAlgs: []string{string(jose.RS256)},
|
||||||
Scopes: []string{"openid", "email", "groups", "profile", "offline_access"},
|
Scopes: []string{"openid", "email", "groups", "profile", "offline_access"},
|
||||||
@ -559,7 +562,8 @@ func (s *Server) sendCodeResponse(w http.ResponseWriter, r *http.Request, authRe
|
|||||||
idToken string
|
idToken string
|
||||||
idTokenExpiry time.Time
|
idTokenExpiry time.Time
|
||||||
|
|
||||||
accessToken = storage.NewID()
|
// Access token
|
||||||
|
accessToken string
|
||||||
)
|
)
|
||||||
|
|
||||||
for _, responseType := range authReq.ResponseTypes {
|
for _, responseType := range authReq.ResponseTypes {
|
||||||
@ -595,6 +599,14 @@ func (s *Server) sendCodeResponse(w http.ResponseWriter, r *http.Request, authRe
|
|||||||
case responseTypeIDToken:
|
case responseTypeIDToken:
|
||||||
implicitOrHybrid = true
|
implicitOrHybrid = true
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
|
accessToken, err = s.newAccessToken(authReq.ClientID, authReq.Claims, authReq.Scopes, authReq.Nonce, authReq.ConnectorID)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Errorf("failed to create new access token: %v", err)
|
||||||
|
s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
idToken, idTokenExpiry, err = s.newIDToken(authReq.ClientID, authReq.Claims, authReq.Scopes, authReq.Nonce, accessToken, authReq.ConnectorID)
|
idToken, idTokenExpiry, err = s.newIDToken(authReq.ClientID, authReq.Claims, authReq.Scopes, authReq.Nonce, accessToken, authReq.ConnectorID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.Errorf("failed to create ID token: %v", err)
|
s.logger.Errorf("failed to create ID token: %v", err)
|
||||||
@ -716,7 +728,13 @@ func (s *Server) handleAuthCode(w http.ResponseWriter, r *http.Request, client s
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
accessToken := storage.NewID()
|
accessToken, err := s.newAccessToken(client.ID, authCode.Claims, authCode.Scopes, authCode.Nonce, authCode.ConnectorID)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Errorf("failed to create new access token: %v", err)
|
||||||
|
s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
idToken, expiry, err := s.newIDToken(client.ID, authCode.Claims, authCode.Scopes, authCode.Nonce, accessToken, authCode.ConnectorID)
|
idToken, expiry, err := s.newIDToken(client.ID, authCode.Claims, authCode.Scopes, authCode.Nonce, accessToken, authCode.ConnectorID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.Errorf("failed to create ID token: %v", err)
|
s.logger.Errorf("failed to create ID token: %v", err)
|
||||||
@ -965,7 +983,13 @@ func (s *Server) handleRefreshToken(w http.ResponseWriter, r *http.Request, clie
|
|||||||
Groups: ident.Groups,
|
Groups: ident.Groups,
|
||||||
}
|
}
|
||||||
|
|
||||||
accessToken := storage.NewID()
|
accessToken, err := s.newAccessToken(client.ID, claims, scopes, refresh.Nonce, refresh.ConnectorID)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Errorf("failed to create new access token: %v", err)
|
||||||
|
s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
idToken, expiry, err := s.newIDToken(client.ID, claims, scopes, refresh.Nonce, accessToken, refresh.ConnectorID)
|
idToken, expiry, err := s.newIDToken(client.ID, claims, scopes, refresh.Nonce, accessToken, refresh.ConnectorID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.Errorf("failed to create ID token: %v", err)
|
s.logger.Errorf("failed to create ID token: %v", err)
|
||||||
@ -1026,10 +1050,35 @@ func (s *Server) handleRefreshToken(w http.ResponseWriter, r *http.Request, clie
|
|||||||
s.writeAccessToken(w, idToken, accessToken, rawNewToken, expiry)
|
s.writeAccessToken(w, idToken, accessToken, rawNewToken, expiry)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleUserInfo(w http.ResponseWriter, r *http.Request) {
|
||||||
|
const prefix = "Bearer "
|
||||||
|
|
||||||
|
auth := r.Header.Get("authorization")
|
||||||
|
if len(auth) < len(prefix) || !strings.EqualFold(prefix, auth[:len(prefix)]) {
|
||||||
|
w.Header().Set("WWW-Authenticate", "Bearer")
|
||||||
|
s.tokenErrHelper(w, errAccessDenied, "Invalid bearer token.", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rawIDToken := auth[len(prefix):]
|
||||||
|
|
||||||
|
verifier := oidc.NewVerifier(s.issuerURL.String(), &storageKeySet{s.storage}, &oidc.Config{SkipClientIDCheck: true})
|
||||||
|
idToken, err := verifier.Verify(r.Context(), rawIDToken)
|
||||||
|
if err != nil {
|
||||||
|
s.tokenErrHelper(w, errAccessDenied, err.Error(), http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var claims json.RawMessage
|
||||||
|
if err := idToken.Claims(&claims); err != nil {
|
||||||
|
s.tokenErrHelper(w, errServerError, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.Write(claims)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Server) writeAccessToken(w http.ResponseWriter, idToken, accessToken, refreshToken string, expiry time.Time) {
|
func (s *Server) writeAccessToken(w http.ResponseWriter, idToken, accessToken, refreshToken string, expiry time.Time) {
|
||||||
// TODO(ericchiang): figure out an access token story and support the user info
|
|
||||||
// endpoint. For now use a random value so no one depends on the access_token
|
|
||||||
// holding a specific structure.
|
|
||||||
resp := struct {
|
resp := struct {
|
||||||
AccessToken string `json:"access_token"`
|
AccessToken string `json:"access_token"`
|
||||||
TokenType string `json:"token_type"`
|
TokenType string `json:"token_type"`
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"crypto/ecdsa"
|
"crypto/ecdsa"
|
||||||
"crypto/elliptic"
|
"crypto/elliptic"
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
@ -265,6 +266,11 @@ type federatedIDClaims struct {
|
|||||||
UserID string `json:"user_id,omitempty"`
|
UserID string `json:"user_id,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Server) newAccessToken(clientID string, claims storage.Claims, scopes []string, nonce, connID string) (accessToken string, err error) {
|
||||||
|
idToken, _, err := s.newIDToken(clientID, claims, scopes, nonce, storage.NewID(), connID)
|
||||||
|
return idToken, err
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Server) newIDToken(clientID string, claims storage.Claims, scopes []string, nonce, accessToken, connID string) (idToken string, expiry time.Time, err error) {
|
func (s *Server) newIDToken(clientID string, claims storage.Claims, scopes []string, nonce, accessToken, connID string) (idToken string, expiry time.Time, err error) {
|
||||||
keys, err := s.storage.GetKeys()
|
keys, err := s.storage.GetKeys()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -561,3 +567,41 @@ func validateRedirectURI(client storage.Client, redirectURI string) bool {
|
|||||||
host, _, err := net.SplitHostPort(u.Host)
|
host, _, err := net.SplitHostPort(u.Host)
|
||||||
return err == nil && host == "localhost"
|
return err == nil && host == "localhost"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// storageKeySet implements the oidc.KeySet interface backed by Dex storage
|
||||||
|
type storageKeySet struct {
|
||||||
|
storage.Storage
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *storageKeySet) VerifySignature(_ context.Context, jwt string) (payload []byte, err error) {
|
||||||
|
jws, err := jose.ParseSigned(jwt)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
keyID := ""
|
||||||
|
for _, sig := range jws.Signatures {
|
||||||
|
keyID = sig.Header.KeyID
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
skeys, err := s.Storage.GetKeys()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
keys := []*jose.JSONWebKey{skeys.SigningKeyPub}
|
||||||
|
for _, vk := range skeys.VerificationKeys {
|
||||||
|
keys = append(keys, vk.PublicKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, key := range keys {
|
||||||
|
if keyID == "" || key.KeyID == keyID {
|
||||||
|
if payload, err := jws.Verify(key); err == nil {
|
||||||
|
return payload, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errors.New("failed to verify id token signature")
|
||||||
|
}
|
||||||
|
@ -2,6 +2,8 @@ package server
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"net/url"
|
"net/url"
|
||||||
@ -11,6 +13,7 @@ import (
|
|||||||
jose "gopkg.in/square/go-jose.v2"
|
jose "gopkg.in/square/go-jose.v2"
|
||||||
|
|
||||||
"github.com/dexidp/dex/storage"
|
"github.com/dexidp/dex/storage"
|
||||||
|
"github.com/dexidp/dex/storage/memory"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestParseAuthorizationRequest(t *testing.T) {
|
func TestParseAuthorizationRequest(t *testing.T) {
|
||||||
@ -259,3 +262,87 @@ func TestValidRedirectURI(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestStorageKeySet(t *testing.T) {
|
||||||
|
s := memory.New(logger)
|
||||||
|
if err := s.UpdateKeys(func(keys storage.Keys) (storage.Keys, error) {
|
||||||
|
keys.SigningKey = &jose.JSONWebKey{
|
||||||
|
Key: testKey,
|
||||||
|
KeyID: "testkey",
|
||||||
|
Algorithm: "RS256",
|
||||||
|
Use: "sig",
|
||||||
|
}
|
||||||
|
keys.SigningKeyPub = &jose.JSONWebKey{
|
||||||
|
Key: testKey.Public(),
|
||||||
|
KeyID: "testkey",
|
||||||
|
Algorithm: "RS256",
|
||||||
|
Use: "sig",
|
||||||
|
}
|
||||||
|
return keys, nil
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
tokenGenerator func() (jwt string, err error)
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "valid token",
|
||||||
|
tokenGenerator: func() (string, error) {
|
||||||
|
signer, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.RS256, Key: testKey}, nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
jws, err := signer.Sign([]byte("payload"))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return jws.CompactSerialize()
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "token signed by different key",
|
||||||
|
tokenGenerator: func() (string, error) {
|
||||||
|
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
signer, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.RS256, Key: key}, nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
jws, err := signer.Sign([]byte("payload"))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return jws.CompactSerialize()
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
tc := tc
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
jwt, err := tc.tokenGenerator()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
keySet := &storageKeySet{s}
|
||||||
|
|
||||||
|
_, err = keySet.VerifySignature(context.Background(), jwt)
|
||||||
|
if (err != nil && !tc.wantErr) || (err == nil && tc.wantErr) {
|
||||||
|
t.Fatalf("wantErr = %v, but got err = %v", tc.wantErr, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -270,6 +270,7 @@ func newServer(ctx context.Context, c Config, rotationStrategy rotationStrategy)
|
|||||||
// TODO(ericchiang): rate limit certain paths based on IP.
|
// TODO(ericchiang): rate limit certain paths based on IP.
|
||||||
handleWithCORS("/token", s.handleToken)
|
handleWithCORS("/token", s.handleToken)
|
||||||
handleWithCORS("/keys", s.handlePublicKeys)
|
handleWithCORS("/keys", s.handlePublicKeys)
|
||||||
|
handleWithCORS("/userinfo", s.handleUserInfo)
|
||||||
handleFunc("/auth", s.handleAuthorization)
|
handleFunc("/auth", s.handleAuthorization)
|
||||||
handleFunc("/auth/{connector}", s.handleConnectorLogin)
|
handleFunc("/auth/{connector}", s.handleConnectorLogin)
|
||||||
r.HandleFunc(path.Join(issuerURL.Path, "/callback"), func(w http.ResponseWriter, r *http.Request) {
|
r.HandleFunc(path.Join(issuerURL.Path, "/callback"), func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
@ -16,7 +16,6 @@ import (
|
|||||||
"reflect"
|
"reflect"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -148,6 +147,7 @@ func TestDiscovery(t *testing.T) {
|
|||||||
"authorization_endpoint",
|
"authorization_endpoint",
|
||||||
"token_endpoint",
|
"token_endpoint",
|
||||||
"jwks_uri",
|
"jwks_uri",
|
||||||
|
"userinfo_endpoint",
|
||||||
}
|
}
|
||||||
for _, field := range required {
|
for _, field := range required {
|
||||||
if _, ok := got[field]; !ok {
|
if _, ok := got[field]; !ok {
|
||||||
@ -201,6 +201,19 @@ func TestOAuth2CodeFlow(t *testing.T) {
|
|||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "fetch userinfo",
|
||||||
|
handleToken: func(ctx context.Context, p *oidc.Provider, config *oauth2.Config, token *oauth2.Token) error {
|
||||||
|
ui, err := p.UserInfo(ctx, config.TokenSource(ctx, token))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to fetch userinfo: %v", err)
|
||||||
|
}
|
||||||
|
if conn.Identity.Email != ui.Email {
|
||||||
|
return fmt.Errorf("expected email to be %v, got %v", conn.Identity.Email, ui.Email)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "verify id token and oauth2 token expiry",
|
name: "verify id token and oauth2 token expiry",
|
||||||
handleToken: func(ctx context.Context, p *oidc.Provider, config *oauth2.Config, token *oauth2.Token) error {
|
handleToken: func(ctx context.Context, p *oidc.Provider, config *oauth2.Config, token *oauth2.Token) error {
|
||||||
@ -541,23 +554,6 @@ func TestOAuth2CodeFlow(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type nonceSource struct {
|
|
||||||
nonce string
|
|
||||||
once sync.Once
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *nonceSource) ClaimNonce(nonce string) error {
|
|
||||||
if n.nonce != nonce {
|
|
||||||
return errors.New("invalid nonce")
|
|
||||||
}
|
|
||||||
ok := false
|
|
||||||
n.once.Do(func() { ok = true })
|
|
||||||
if !ok {
|
|
||||||
return errors.New("invalid nonce")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestOAuth2ImplicitFlow(t *testing.T) {
|
func TestOAuth2ImplicitFlow(t *testing.T) {
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
@ -623,11 +619,8 @@ func TestOAuth2ImplicitFlow(t *testing.T) {
|
|||||||
t.Fatalf("failed to create client: %v", err)
|
t.Fatalf("failed to create client: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
src := &nonceSource{nonce: nonce}
|
|
||||||
|
|
||||||
idTokenVerifier := p.Verifier(&oidc.Config{
|
idTokenVerifier := p.Verifier(&oidc.Config{
|
||||||
ClientID: client.ID,
|
ClientID: client.ID,
|
||||||
ClaimNonce: src.ClaimNonce,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
oauth2Config = &oauth2.Config{
|
oauth2Config = &oauth2.Config{
|
||||||
@ -646,13 +639,17 @@ func TestOAuth2ImplicitFlow(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to parse fragment: %v", err)
|
return fmt.Errorf("failed to parse fragment: %v", err)
|
||||||
}
|
}
|
||||||
idToken := v.Get("id_token")
|
rawIDToken := v.Get("id_token")
|
||||||
if idToken == "" {
|
if rawIDToken == "" {
|
||||||
return errors.New("no id_token in fragment")
|
return errors.New("no id_token in fragment")
|
||||||
}
|
}
|
||||||
if _, err := idTokenVerifier.Verify(ctx, idToken); err != nil {
|
idToken, err := idTokenVerifier.Verify(ctx, rawIDToken)
|
||||||
|
if err != nil {
|
||||||
return fmt.Errorf("failed to verify id_token: %v", err)
|
return fmt.Errorf("failed to verify id_token: %v", err)
|
||||||
}
|
}
|
||||||
|
if idToken.Nonce != nonce {
|
||||||
|
return fmt.Errorf("failed to verify id_token: nonce was %v, but want %v", idToken.Nonce, nonce)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
5
vendor/github.com/coreos/go-oidc/MAINTAINERS
generated
vendored
5
vendor/github.com/coreos/go-oidc/MAINTAINERS
generated
vendored
@ -1,3 +1,2 @@
|
|||||||
Bobby Rullo <bobby.rullo@coreos.com> (@bobbyrullo)
|
Eric Chiang <echiang@redhat.com> (@ericchiang)
|
||||||
Ed Rooth <ed.rooth@coreos.com> (@sym3tri)
|
Rithu Leena John <rjohn@redhat.com> (@rithujohn191)
|
||||||
Eric Chiang <eric.chiang@coreos.com> (@ericchiang)
|
|
||||||
|
2
vendor/github.com/coreos/go-oidc/README.md
generated
vendored
2
vendor/github.com/coreos/go-oidc/README.md
generated
vendored
@ -38,7 +38,7 @@ func handleRedirect(w http.ResponseWriter, r *http.Request) {
|
|||||||
The on responses, the provider can be used to verify ID Tokens.
|
The on responses, the provider can be used to verify ID Tokens.
|
||||||
|
|
||||||
```go
|
```go
|
||||||
var verifier = provider.Verifier()
|
var verifier = provider.Verifier(&oidc.Config{ClientID: clientID})
|
||||||
|
|
||||||
func handleOAuth2Callback(w http.ResponseWriter, r *http.Request) {
|
func handleOAuth2Callback(w http.ResponseWriter, r *http.Request) {
|
||||||
// Verify state and errors.
|
// Verify state and errors.
|
||||||
|
61
vendor/github.com/coreos/go-oidc/code-of-conduct.md
generated
vendored
Normal file
61
vendor/github.com/coreos/go-oidc/code-of-conduct.md
generated
vendored
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
## CoreOS Community Code of Conduct
|
||||||
|
|
||||||
|
### Contributor Code of Conduct
|
||||||
|
|
||||||
|
As contributors and maintainers of this project, and in the interest of
|
||||||
|
fostering an open and welcoming community, we pledge to respect all people who
|
||||||
|
contribute through reporting issues, posting feature requests, updating
|
||||||
|
documentation, submitting pull requests or patches, and other activities.
|
||||||
|
|
||||||
|
We are committed to making participation in this project a harassment-free
|
||||||
|
experience for everyone, regardless of level of experience, gender, gender
|
||||||
|
identity and expression, sexual orientation, disability, personal appearance,
|
||||||
|
body size, race, ethnicity, age, religion, or nationality.
|
||||||
|
|
||||||
|
Examples of unacceptable behavior by participants include:
|
||||||
|
|
||||||
|
* The use of sexualized language or imagery
|
||||||
|
* Personal attacks
|
||||||
|
* Trolling or insulting/derogatory comments
|
||||||
|
* Public or private harassment
|
||||||
|
* Publishing others' private information, such as physical or electronic addresses, without explicit permission
|
||||||
|
* Other unethical or unprofessional conduct.
|
||||||
|
|
||||||
|
Project maintainers have the right and responsibility to remove, edit, or
|
||||||
|
reject comments, commits, code, wiki edits, issues, and other contributions
|
||||||
|
that are not aligned to this Code of Conduct. By adopting this Code of Conduct,
|
||||||
|
project maintainers commit themselves to fairly and consistently applying these
|
||||||
|
principles to every aspect of managing this project. Project maintainers who do
|
||||||
|
not follow or enforce the Code of Conduct may be permanently removed from the
|
||||||
|
project team.
|
||||||
|
|
||||||
|
This code of conduct applies both within project spaces and in public spaces
|
||||||
|
when an individual is representing the project or its community.
|
||||||
|
|
||||||
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||||
|
reported by contacting a project maintainer, Brandon Philips
|
||||||
|
<brandon.philips@coreos.com>, and/or Rithu John <rithu.john@coreos.com>.
|
||||||
|
|
||||||
|
This Code of Conduct is adapted from the Contributor Covenant
|
||||||
|
(http://contributor-covenant.org), version 1.2.0, available at
|
||||||
|
http://contributor-covenant.org/version/1/2/0/
|
||||||
|
|
||||||
|
### CoreOS Events Code of Conduct
|
||||||
|
|
||||||
|
CoreOS events are working conferences intended for professional networking and
|
||||||
|
collaboration in the CoreOS community. Attendees are expected to behave
|
||||||
|
according to professional standards and in accordance with their employer’s
|
||||||
|
policies on appropriate workplace behavior.
|
||||||
|
|
||||||
|
While at CoreOS events or related social networking opportunities, attendees
|
||||||
|
should not engage in discriminatory or offensive speech or actions including
|
||||||
|
but not limited to gender, sexuality, race, age, disability, or religion.
|
||||||
|
Speakers should be especially aware of these concerns.
|
||||||
|
|
||||||
|
CoreOS does not condone any statements by speakers contrary to these standards.
|
||||||
|
CoreOS reserves the right to deny entrance and/or eject from an event (without
|
||||||
|
refund) any individual found to be engaging in discriminatory or offensive
|
||||||
|
speech or actions.
|
||||||
|
|
||||||
|
Please bring any concerns to the immediate attention of designated on-site
|
||||||
|
staff, Brandon Philips <brandon.philips@coreos.com>, and/or Rithu John <rithu.john@coreos.com>.
|
150
vendor/github.com/coreos/go-oidc/gen.go
generated
vendored
150
vendor/github.com/coreos/go-oidc/gen.go
generated
vendored
@ -1,150 +0,0 @@
|
|||||||
// +build ignore
|
|
||||||
|
|
||||||
// This file is used to generate keys for tests.
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"crypto"
|
|
||||||
"crypto/ecdsa"
|
|
||||||
"crypto/elliptic"
|
|
||||||
"crypto/rand"
|
|
||||||
"crypto/rsa"
|
|
||||||
"encoding/hex"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"log"
|
|
||||||
"text/template"
|
|
||||||
|
|
||||||
jose "gopkg.in/square/go-jose.v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
type key struct {
|
|
||||||
name string
|
|
||||||
new func() (crypto.Signer, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
var keys = []key{
|
|
||||||
{
|
|
||||||
"ECDSA_256", func() (crypto.Signer, error) {
|
|
||||||
return ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ECDSA_384", func() (crypto.Signer, error) {
|
|
||||||
return ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ECDSA_521", func() (crypto.Signer, error) {
|
|
||||||
return ecdsa.GenerateKey(elliptic.P521(), rand.Reader)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"RSA_1024", func() (crypto.Signer, error) {
|
|
||||||
return rsa.GenerateKey(rand.Reader, 1024)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"RSA_2048", func() (crypto.Signer, error) {
|
|
||||||
return rsa.GenerateKey(rand.Reader, 2048)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"RSA_4096", func() (crypto.Signer, error) {
|
|
||||||
return rsa.GenerateKey(rand.Reader, 4096)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func newJWK(k key, prefix, ident string) (privBytes, pubBytes []byte, err error) {
|
|
||||||
priv, err := k.new()
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, fmt.Errorf("generate %s: %v", k.name, err)
|
|
||||||
}
|
|
||||||
pub := priv.Public()
|
|
||||||
|
|
||||||
privKey := &jose.JSONWebKey{Key: priv}
|
|
||||||
thumbprint, err := privKey.Thumbprint(crypto.SHA256)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, fmt.Errorf("computing thumbprint: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
keyID := hex.EncodeToString(thumbprint)
|
|
||||||
privKey.KeyID = keyID
|
|
||||||
pubKey := &jose.JSONWebKey{Key: pub, KeyID: keyID}
|
|
||||||
|
|
||||||
privBytes, err = json.MarshalIndent(privKey, prefix, ident)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
pubBytes, err = json.MarshalIndent(pubKey, prefix, ident)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
type keyData struct {
|
|
||||||
Name string
|
|
||||||
Priv string
|
|
||||||
Pub string
|
|
||||||
}
|
|
||||||
|
|
||||||
var tmpl = template.Must(template.New("").Parse(`// +build !golint
|
|
||||||
|
|
||||||
// This file contains statically created JWKs for tests created by gen.go
|
|
||||||
|
|
||||||
package oidc
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
|
|
||||||
jose "gopkg.in/square/go-jose.v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
func mustLoadJWK(s string) jose.JSONWebKey {
|
|
||||||
var jwk jose.JSONWebKey
|
|
||||||
if err := json.Unmarshal([]byte(s), &jwk); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return jwk
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
{{- range $i, $key := .Keys }}
|
|
||||||
testKey{{ $key.Name }} = mustLoadJWK(` + "`" + `{{ $key.Pub }}` + "`" + `)
|
|
||||||
testKey{{ $key.Name }}_Priv = mustLoadJWK(` + "`" + `{{ $key.Priv }}` + "`" + `)
|
|
||||||
{{ end -}}
|
|
||||||
)
|
|
||||||
`))
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
var tmplData struct {
|
|
||||||
Keys []keyData
|
|
||||||
}
|
|
||||||
for _, k := range keys {
|
|
||||||
for i := 0; i < 4; i++ {
|
|
||||||
log.Printf("generating %s", k.name)
|
|
||||||
priv, pub, err := newJWK(k, "\t", "\t")
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
name := fmt.Sprintf("%s_%d", k.name, i)
|
|
||||||
|
|
||||||
tmplData.Keys = append(tmplData.Keys, keyData{
|
|
||||||
Name: name,
|
|
||||||
Priv: string(priv),
|
|
||||||
Pub: string(pub),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
buff := new(bytes.Buffer)
|
|
||||||
if err := tmpl.Execute(buff, tmplData); err != nil {
|
|
||||||
log.Fatalf("excuting template: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := ioutil.WriteFile("jose_test.go", buff.Bytes(), 0644); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
201
vendor/github.com/coreos/go-oidc/jwks.go
generated
vendored
201
vendor/github.com/coreos/go-oidc/jwks.go
generated
vendored
@ -2,7 +2,7 @@ package oidc
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
@ -23,6 +23,20 @@ import (
|
|||||||
// updated.
|
// updated.
|
||||||
const keysExpiryDelta = 30 * time.Second
|
const keysExpiryDelta = 30 * time.Second
|
||||||
|
|
||||||
|
// NewRemoteKeySet returns a KeySet that can validate JSON web tokens by using HTTP
|
||||||
|
// GETs to fetch JSON web token sets hosted at a remote URL. This is automatically
|
||||||
|
// used by NewProvider using the URLs returned by OpenID Connect discovery, but is
|
||||||
|
// exposed for providers that don't support discovery or to prevent round trips to the
|
||||||
|
// discovery URL.
|
||||||
|
//
|
||||||
|
// The returned KeySet is a long lived verifier that caches keys based on cache-control
|
||||||
|
// headers. Reuse a common remote key set instead of creating new ones as needed.
|
||||||
|
//
|
||||||
|
// The behavior of the returned KeySet is undefined once the context is canceled.
|
||||||
|
func NewRemoteKeySet(ctx context.Context, jwksURL string) KeySet {
|
||||||
|
return newRemoteKeySet(ctx, jwksURL, time.Now)
|
||||||
|
}
|
||||||
|
|
||||||
func newRemoteKeySet(ctx context.Context, jwksURL string, now func() time.Time) *remoteKeySet {
|
func newRemoteKeySet(ctx context.Context, jwksURL string, now func() time.Time) *remoteKeySet {
|
||||||
if now == nil {
|
if now == nil {
|
||||||
now = time.Now
|
now = time.Now
|
||||||
@ -38,147 +52,168 @@ type remoteKeySet struct {
|
|||||||
// guard all other fields
|
// guard all other fields
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
|
|
||||||
// inflightCtx suppresses parallel execution of updateKeys and allows
|
// inflight suppresses parallel execution of updateKeys and allows
|
||||||
// multiple goroutines to wait for its result.
|
// multiple goroutines to wait for its result.
|
||||||
// Its Err() method returns any errors encountered during updateKeys.
|
inflight *inflight
|
||||||
//
|
|
||||||
// If nil, there is no inflight updateKeys request.
|
|
||||||
inflightCtx *inflight
|
|
||||||
|
|
||||||
// A set of cached keys and their expiry.
|
// A set of cached keys and their expiry.
|
||||||
cachedKeys []jose.JSONWebKey
|
cachedKeys []jose.JSONWebKey
|
||||||
expiry time.Time
|
expiry time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
// inflight is used to wait on some in-flight request from multiple goroutines
|
// inflight is used to wait on some in-flight request from multiple goroutines.
|
||||||
type inflight struct {
|
type inflight struct {
|
||||||
done chan struct{}
|
doneCh chan struct{}
|
||||||
|
|
||||||
|
keys []jose.JSONWebKey
|
||||||
err error
|
err error
|
||||||
}
|
}
|
||||||
|
|
||||||
// Done returns a channel that is closed when the inflight request finishes.
|
func newInflight() *inflight {
|
||||||
func (i *inflight) Done() <-chan struct{} {
|
return &inflight{doneCh: make(chan struct{})}
|
||||||
return i.done
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Err returns any error encountered during request execution. May be nil.
|
// wait returns a channel that multiple goroutines can receive on. Once it returns
|
||||||
func (i *inflight) Err() error {
|
// a value, the inflight request is done and result() can be inspected.
|
||||||
return i.err
|
func (i *inflight) wait() <-chan struct{} {
|
||||||
|
return i.doneCh
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cancel signals completion of the inflight request with error err.
|
// done can only be called by a single goroutine. It records the result of the
|
||||||
// Must be called only once for particular inflight instance.
|
// inflight request and signals other goroutines that the result is safe to
|
||||||
func (i *inflight) Cancel(err error) {
|
// inspect.
|
||||||
|
func (i *inflight) done(keys []jose.JSONWebKey, err error) {
|
||||||
|
i.keys = keys
|
||||||
i.err = err
|
i.err = err
|
||||||
close(i.done)
|
close(i.doneCh)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *remoteKeySet) keysWithIDFromCache(keyIDs []string) ([]jose.JSONWebKey, bool) {
|
// result cannot be called until the wait() channel has returned a value.
|
||||||
r.mu.Lock()
|
func (i *inflight) result() ([]jose.JSONWebKey, error) {
|
||||||
keys, expiry := r.cachedKeys, r.expiry
|
return i.keys, i.err
|
||||||
r.mu.Unlock()
|
|
||||||
|
|
||||||
// Have the keys expired?
|
|
||||||
if expiry.Add(keysExpiryDelta).Before(r.now()) {
|
|
||||||
return nil, false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var signingKeys []jose.JSONWebKey
|
func (r *remoteKeySet) VerifySignature(ctx context.Context, jwt string) ([]byte, error) {
|
||||||
|
jws, err := jose.ParseSigned(jwt)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("oidc: malformed jwt: %v", err)
|
||||||
|
}
|
||||||
|
return r.verify(ctx, jws)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *remoteKeySet) verify(ctx context.Context, jws *jose.JSONWebSignature) ([]byte, error) {
|
||||||
|
// We don't support JWTs signed with multiple signatures.
|
||||||
|
keyID := ""
|
||||||
|
for _, sig := range jws.Signatures {
|
||||||
|
keyID = sig.Header.KeyID
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
keys, expiry := r.keysFromCache()
|
||||||
|
|
||||||
|
// Don't check expiry yet. This optimizes for when the provider is unavailable.
|
||||||
for _, key := range keys {
|
for _, key := range keys {
|
||||||
if contains(keyIDs, key.KeyID) {
|
if keyID == "" || key.KeyID == keyID {
|
||||||
signingKeys = append(signingKeys, key)
|
if payload, err := jws.Verify(&key); err == nil {
|
||||||
|
return payload, nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(signingKeys) == 0 {
|
if !r.now().Add(keysExpiryDelta).After(expiry) {
|
||||||
// Are the keys about to expire?
|
// Keys haven't expired, don't refresh.
|
||||||
if r.now().Add(keysExpiryDelta).After(expiry) {
|
return nil, errors.New("failed to verify id token signature")
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return signingKeys, true
|
keys, err := r.keysFromRemote(ctx)
|
||||||
}
|
if err != nil {
|
||||||
func (r *remoteKeySet) keysWithID(ctx context.Context, keyIDs []string) ([]jose.JSONWebKey, error) {
|
return nil, fmt.Errorf("fetching keys %v", err)
|
||||||
keys, ok := r.keysWithIDFromCache(keyIDs)
|
|
||||||
if ok {
|
|
||||||
return keys, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var inflightCtx *inflight
|
for _, key := range keys {
|
||||||
func() {
|
if keyID == "" || key.KeyID == keyID {
|
||||||
|
if payload, err := jws.Verify(&key); err == nil {
|
||||||
|
return payload, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, errors.New("failed to verify id token signature")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *remoteKeySet) keysFromCache() (keys []jose.JSONWebKey, expiry time.Time) {
|
||||||
r.mu.Lock()
|
r.mu.Lock()
|
||||||
defer r.mu.Unlock()
|
defer r.mu.Unlock()
|
||||||
|
return r.cachedKeys, r.expiry
|
||||||
|
}
|
||||||
|
|
||||||
|
// keysFromRemote syncs the key set from the remote set, records the values in the
|
||||||
|
// cache, and returns the key set.
|
||||||
|
func (r *remoteKeySet) keysFromRemote(ctx context.Context) ([]jose.JSONWebKey, error) {
|
||||||
|
// Need to lock to inspect the inflight request field.
|
||||||
|
r.mu.Lock()
|
||||||
// If there's not a current inflight request, create one.
|
// If there's not a current inflight request, create one.
|
||||||
if r.inflightCtx == nil {
|
if r.inflight == nil {
|
||||||
inflightCtx := &inflight{make(chan struct{}), nil}
|
r.inflight = newInflight()
|
||||||
r.inflightCtx = inflightCtx
|
|
||||||
|
|
||||||
|
// This goroutine has exclusive ownership over the current inflight
|
||||||
|
// request. It releases the resource by nil'ing the inflight field
|
||||||
|
// once the goroutine is done.
|
||||||
go func() {
|
go func() {
|
||||||
// TODO(ericchiang): Upstream Kubernetes request that we recover every time
|
// Sync keys and finish inflight when that's done.
|
||||||
// we spawn a goroutine, because panics in a goroutine will bring down the
|
keys, expiry, err := r.updateKeys()
|
||||||
// entire program. There's no way to recover from another goroutine's panic.
|
|
||||||
//
|
|
||||||
// Most users actually want to let the panic propagate and bring down the
|
|
||||||
// program because it implies some unrecoverable state.
|
|
||||||
//
|
|
||||||
// Add a context key to allow the recover behavior.
|
|
||||||
//
|
|
||||||
// See: https://github.com/coreos/go-oidc/issues/89
|
|
||||||
|
|
||||||
// Sync keys and close inflightCtx when that's done.
|
r.inflight.done(keys, err)
|
||||||
// Use the remoteKeySet's context instead of the requests context
|
|
||||||
// because a re-sync is unique to the keys set and will span multiple
|
|
||||||
// requests.
|
|
||||||
inflightCtx.Cancel(r.updateKeys(r.ctx))
|
|
||||||
|
|
||||||
|
// Lock to update the keys and indicate that there is no longer an
|
||||||
|
// inflight request.
|
||||||
r.mu.Lock()
|
r.mu.Lock()
|
||||||
defer r.mu.Unlock()
|
defer r.mu.Unlock()
|
||||||
r.inflightCtx = nil
|
|
||||||
}()
|
if err == nil {
|
||||||
|
r.cachedKeys = keys
|
||||||
|
r.expiry = expiry
|
||||||
}
|
}
|
||||||
|
|
||||||
inflightCtx = r.inflightCtx
|
// Free inflight so a different request can run.
|
||||||
|
r.inflight = nil
|
||||||
}()
|
}()
|
||||||
|
}
|
||||||
|
inflight := r.inflight
|
||||||
|
r.mu.Unlock()
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return nil, ctx.Err()
|
return nil, ctx.Err()
|
||||||
case <-inflightCtx.Done():
|
case <-inflight.wait():
|
||||||
if err := inflightCtx.Err(); err != nil {
|
return inflight.result()
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Since we've just updated keys, we don't care about the cache miss.
|
func (r *remoteKeySet) updateKeys() ([]jose.JSONWebKey, time.Time, error) {
|
||||||
keys, _ = r.keysWithIDFromCache(keyIDs)
|
|
||||||
return keys, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *remoteKeySet) updateKeys(ctx context.Context) error {
|
|
||||||
req, err := http.NewRequest("GET", r.jwksURL, nil)
|
req, err := http.NewRequest("GET", r.jwksURL, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("oidc: can't create request: %v", err)
|
return nil, time.Time{}, fmt.Errorf("oidc: can't create request: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := doRequest(ctx, req)
|
resp, err := doRequest(r.ctx, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("oidc: get keys failed %v", err)
|
return nil, time.Time{}, fmt.Errorf("oidc: get keys failed %v", err)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
body, err := ioutil.ReadAll(resp.Body)
|
body, err := ioutil.ReadAll(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("oidc: read response body: %v", err)
|
return nil, time.Time{}, fmt.Errorf("unable to read response body: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
return fmt.Errorf("oidc: get keys failed: %s %s", resp.Status, body)
|
return nil, time.Time{}, fmt.Errorf("oidc: get keys failed: %s %s", resp.Status, body)
|
||||||
}
|
}
|
||||||
|
|
||||||
var keySet jose.JSONWebKeySet
|
var keySet jose.JSONWebKeySet
|
||||||
if err := json.Unmarshal(body, &keySet); err != nil {
|
err = unmarshalResp(resp, body, &keySet)
|
||||||
return fmt.Errorf("oidc: failed to decode keys: %v %s", err, body)
|
if err != nil {
|
||||||
|
return nil, time.Time{}, fmt.Errorf("oidc: failed to decode keys: %v %s", err, body)
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the server doesn't provide cache control headers, assume the
|
// If the server doesn't provide cache control headers, assume the
|
||||||
@ -189,11 +224,5 @@ func (r *remoteKeySet) updateKeys(ctx context.Context) error {
|
|||||||
if err == nil && e.After(expiry) {
|
if err == nil && e.After(expiry) {
|
||||||
expiry = e
|
expiry = e
|
||||||
}
|
}
|
||||||
|
return keySet.Keys, expiry, nil
|
||||||
r.mu.Lock()
|
|
||||||
defer r.mu.Unlock()
|
|
||||||
r.cachedKeys = keySet.Keys
|
|
||||||
r.expiry = expiry
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
85
vendor/github.com/coreos/go-oidc/oidc.go
generated
vendored
85
vendor/github.com/coreos/go-oidc/oidc.go
generated
vendored
@ -3,10 +3,15 @@ package oidc
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/sha256"
|
||||||
|
"crypto/sha512"
|
||||||
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"hash"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"mime"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@ -30,6 +35,11 @@ const (
|
|||||||
ScopeOfflineAccess = "offline_access"
|
ScopeOfflineAccess = "offline_access"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
errNoAtHash = errors.New("id token did not have an access token hash")
|
||||||
|
errInvalidAtHash = errors.New("access token hash does not match value in ID token")
|
||||||
|
)
|
||||||
|
|
||||||
// ClientContext returns a new Context that carries the provided HTTP client.
|
// ClientContext returns a new Context that carries the provided HTTP client.
|
||||||
//
|
//
|
||||||
// This method sets the same context key used by the golang.org/x/oauth2 package,
|
// This method sets the same context key used by the golang.org/x/oauth2 package,
|
||||||
@ -63,7 +73,7 @@ type Provider struct {
|
|||||||
// Raw claims returned by the server.
|
// Raw claims returned by the server.
|
||||||
rawClaims []byte
|
rawClaims []byte
|
||||||
|
|
||||||
remoteKeySet *remoteKeySet
|
remoteKeySet KeySet
|
||||||
}
|
}
|
||||||
|
|
||||||
type cachedKeys struct {
|
type cachedKeys struct {
|
||||||
@ -93,18 +103,23 @@ func NewProvider(ctx context.Context, issuer string) (*Provider, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
body, err := ioutil.ReadAll(resp.Body)
|
body, err := ioutil.ReadAll(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, fmt.Errorf("unable to read response body: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
return nil, fmt.Errorf("%s: %s", resp.Status, body)
|
return nil, fmt.Errorf("%s: %s", resp.Status, body)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
|
||||||
var p providerJSON
|
var p providerJSON
|
||||||
if err := json.Unmarshal(body, &p); err != nil {
|
err = unmarshalResp(resp, body, &p)
|
||||||
|
if err != nil {
|
||||||
return nil, fmt.Errorf("oidc: failed to decode provider discovery object: %v", err)
|
return nil, fmt.Errorf("oidc: failed to decode provider discovery object: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if p.Issuer != issuer {
|
if p.Issuer != issuer {
|
||||||
return nil, fmt.Errorf("oidc: issuer did not match the issuer returned by provider, expected %q got %q", issuer, p.Issuer)
|
return nil, fmt.Errorf("oidc: issuer did not match the issuer returned by provider, expected %q got %q", issuer, p.Issuer)
|
||||||
}
|
}
|
||||||
@ -114,7 +129,7 @@ func NewProvider(ctx context.Context, issuer string) (*Provider, error) {
|
|||||||
tokenURL: p.TokenURL,
|
tokenURL: p.TokenURL,
|
||||||
userInfoURL: p.UserInfoURL,
|
userInfoURL: p.UserInfoURL,
|
||||||
rawClaims: body,
|
rawClaims: body,
|
||||||
remoteKeySet: newRemoteKeySet(ctx, p.JWKSURL, time.Now),
|
remoteKeySet: NewRemoteKeySet(ctx, p.JWKSURL),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -232,9 +247,18 @@ type IDToken struct {
|
|||||||
|
|
||||||
// Initial nonce provided during the authentication redirect.
|
// Initial nonce provided during the authentication redirect.
|
||||||
//
|
//
|
||||||
// If present, this package ensures this is a valid nonce.
|
// This package does NOT provided verification on the value of this field
|
||||||
|
// and it's the user's responsibility to ensure it contains a valid value.
|
||||||
Nonce string
|
Nonce string
|
||||||
|
|
||||||
|
// at_hash claim, if set in the ID token. Callers can verify an access token
|
||||||
|
// that corresponds to the ID token using the VerifyAccessToken method.
|
||||||
|
AccessTokenHash string
|
||||||
|
|
||||||
|
// signature algorithm used for ID token, needed to compute a verification hash of an
|
||||||
|
// access token
|
||||||
|
sigAlgorithm string
|
||||||
|
|
||||||
// Raw payload of the id_token.
|
// Raw payload of the id_token.
|
||||||
claims []byte
|
claims []byte
|
||||||
}
|
}
|
||||||
@ -260,6 +284,34 @@ func (i *IDToken) Claims(v interface{}) error {
|
|||||||
return json.Unmarshal(i.claims, v)
|
return json.Unmarshal(i.claims, v)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// VerifyAccessToken verifies that the hash of the access token that corresponds to the iD token
|
||||||
|
// matches the hash in the id token. It returns an error if the hashes don't match.
|
||||||
|
// It is the caller's responsibility to ensure that the optional access token hash is present for the ID token
|
||||||
|
// before calling this method. See https://openid.net/specs/openid-connect-core-1_0.html#CodeIDToken
|
||||||
|
func (i *IDToken) VerifyAccessToken(accessToken string) error {
|
||||||
|
if i.AccessTokenHash == "" {
|
||||||
|
return errNoAtHash
|
||||||
|
}
|
||||||
|
var h hash.Hash
|
||||||
|
switch i.sigAlgorithm {
|
||||||
|
case RS256, ES256, PS256:
|
||||||
|
h = sha256.New()
|
||||||
|
case RS384, ES384, PS384:
|
||||||
|
h = sha512.New384()
|
||||||
|
case RS512, ES512, PS512:
|
||||||
|
h = sha512.New()
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("oidc: unsupported signing algorithm %q", i.sigAlgorithm)
|
||||||
|
}
|
||||||
|
h.Write([]byte(accessToken)) // hash documents that Write will never return an error
|
||||||
|
sum := h.Sum(nil)[:h.Size()/2]
|
||||||
|
actual := base64.RawURLEncoding.EncodeToString(sum)
|
||||||
|
if actual != i.AccessTokenHash {
|
||||||
|
return errInvalidAtHash
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type idToken struct {
|
type idToken struct {
|
||||||
Issuer string `json:"iss"`
|
Issuer string `json:"iss"`
|
||||||
Subject string `json:"sub"`
|
Subject string `json:"sub"`
|
||||||
@ -267,6 +319,7 @@ type idToken struct {
|
|||||||
Expiry jsonTime `json:"exp"`
|
Expiry jsonTime `json:"exp"`
|
||||||
IssuedAt jsonTime `json:"iat"`
|
IssuedAt jsonTime `json:"iat"`
|
||||||
Nonce string `json:"nonce"`
|
Nonce string `json:"nonce"`
|
||||||
|
AtHash string `json:"at_hash"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type audience []string
|
type audience []string
|
||||||
@ -285,13 +338,6 @@ func (a *audience) UnmarshalJSON(b []byte) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a audience) MarshalJSON() ([]byte, error) {
|
|
||||||
if len(a) == 1 {
|
|
||||||
return json.Marshal(a[0])
|
|
||||||
}
|
|
||||||
return json.Marshal([]string(a))
|
|
||||||
}
|
|
||||||
|
|
||||||
type jsonTime time.Time
|
type jsonTime time.Time
|
||||||
|
|
||||||
func (j *jsonTime) UnmarshalJSON(b []byte) error {
|
func (j *jsonTime) UnmarshalJSON(b []byte) error {
|
||||||
@ -314,6 +360,15 @@ func (j *jsonTime) UnmarshalJSON(b []byte) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j jsonTime) MarshalJSON() ([]byte, error) {
|
func unmarshalResp(r *http.Response, body []byte, v interface{}) error {
|
||||||
return json.Marshal(time.Time(j).Unix())
|
err := json.Unmarshal(body, &v)
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
ct := r.Header.Get("Content-Type")
|
||||||
|
mediaType, _, parseErr := mime.ParseMediaType(ct)
|
||||||
|
if parseErr == nil && mediaType == "application/json" {
|
||||||
|
return fmt.Errorf("got Content-Type = application/json, but could not unmarshal as JSON: %v", err)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("expected Content-Type = application/json, got %q: %v", ct, err)
|
||||||
}
|
}
|
||||||
|
3
vendor/github.com/coreos/go-oidc/test
generated
vendored
3
vendor/github.com/coreos/go-oidc/test
generated
vendored
@ -11,5 +11,6 @@ LINTABLE=$( go list -tags=golint -f '
|
|||||||
|
|
||||||
go test -v -i -race github.com/coreos/go-oidc/...
|
go test -v -i -race github.com/coreos/go-oidc/...
|
||||||
go test -v -race github.com/coreos/go-oidc/...
|
go test -v -race github.com/coreos/go-oidc/...
|
||||||
golint $LINTABLE
|
golint -set_exit_status $LINTABLE
|
||||||
go vet github.com/coreos/go-oidc/...
|
go vet github.com/coreos/go-oidc/...
|
||||||
|
go build -v ./example/...
|
||||||
|
135
vendor/github.com/coreos/go-oidc/verify.go
generated
vendored
135
vendor/github.com/coreos/go-oidc/verify.go
generated
vendored
@ -19,13 +19,54 @@ const (
|
|||||||
issuerGoogleAccountsNoScheme = "accounts.google.com"
|
issuerGoogleAccountsNoScheme = "accounts.google.com"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// KeySet is a set of publc JSON Web Keys that can be used to validate the signature
|
||||||
|
// of JSON web tokens. This is expected to be backed by a remote key set through
|
||||||
|
// provider metadata discovery or an in-memory set of keys delivered out-of-band.
|
||||||
|
type KeySet interface {
|
||||||
|
// VerifySignature parses the JSON web token, verifies the signature, and returns
|
||||||
|
// the raw payload. Header and claim fields are validated by other parts of the
|
||||||
|
// package. For example, the KeySet does not need to check values such as signature
|
||||||
|
// algorithm, issuer, and audience since the IDTokenVerifier validates these values
|
||||||
|
// independently.
|
||||||
|
//
|
||||||
|
// If VerifySignature makes HTTP requests to verify the token, it's expected to
|
||||||
|
// use any HTTP client associated with the context through ClientContext.
|
||||||
|
VerifySignature(ctx context.Context, jwt string) (payload []byte, err error)
|
||||||
|
}
|
||||||
|
|
||||||
// IDTokenVerifier provides verification for ID Tokens.
|
// IDTokenVerifier provides verification for ID Tokens.
|
||||||
type IDTokenVerifier struct {
|
type IDTokenVerifier struct {
|
||||||
keySet *remoteKeySet
|
keySet KeySet
|
||||||
config *Config
|
config *Config
|
||||||
issuer string
|
issuer string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewVerifier returns a verifier manually constructed from a key set and issuer URL.
|
||||||
|
//
|
||||||
|
// It's easier to use provider discovery to construct an IDTokenVerifier than creating
|
||||||
|
// one directly. This method is intended to be used with provider that don't support
|
||||||
|
// metadata discovery, or avoiding round trips when the key set URL is already known.
|
||||||
|
//
|
||||||
|
// This constructor can be used to create a verifier directly using the issuer URL and
|
||||||
|
// JSON Web Key Set URL without using discovery:
|
||||||
|
//
|
||||||
|
// keySet := oidc.NewRemoteKeySet(ctx, "https://www.googleapis.com/oauth2/v3/certs")
|
||||||
|
// verifier := oidc.NewVerifier("https://accounts.google.com", keySet, config)
|
||||||
|
//
|
||||||
|
// Since KeySet is an interface, this constructor can also be used to supply custom
|
||||||
|
// public key sources. For example, if a user wanted to supply public keys out-of-band
|
||||||
|
// and hold them statically in-memory:
|
||||||
|
//
|
||||||
|
// // Custom KeySet implementation.
|
||||||
|
// keySet := newStatisKeySet(publicKeys...)
|
||||||
|
//
|
||||||
|
// // Verifier uses the custom KeySet implementation.
|
||||||
|
// verifier := oidc.NewVerifier("https://auth.example.com", keySet, config)
|
||||||
|
//
|
||||||
|
func NewVerifier(issuerURL string, keySet KeySet, config *Config) *IDTokenVerifier {
|
||||||
|
return &IDTokenVerifier{keySet: keySet, config: config, issuer: issuerURL}
|
||||||
|
}
|
||||||
|
|
||||||
// Config is the configuration for an IDTokenVerifier.
|
// Config is the configuration for an IDTokenVerifier.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
// Expected audience of the token. For a majority of the cases this is expected to be
|
// Expected audience of the token. For a majority of the cases this is expected to be
|
||||||
@ -34,12 +75,6 @@ type Config struct {
|
|||||||
//
|
//
|
||||||
// If not provided, users must explicitly set SkipClientIDCheck.
|
// If not provided, users must explicitly set SkipClientIDCheck.
|
||||||
ClientID string
|
ClientID string
|
||||||
// Method to verify the ID Token nonce. If a nonce is present and this method
|
|
||||||
// is nil, users must explicitly set SkipNonceCheck.
|
|
||||||
//
|
|
||||||
// If the ID Token nonce is empty, for example if the client didn't provide a nonce in
|
|
||||||
// the initial redirect, this may be nil.
|
|
||||||
ClaimNonce func(nonce string) error
|
|
||||||
// If specified, only this set of algorithms may be used to sign the JWT.
|
// If specified, only this set of algorithms may be used to sign the JWT.
|
||||||
//
|
//
|
||||||
// Since many providers only support RS256, SupportedSigningAlgs defaults to this value.
|
// Since many providers only support RS256, SupportedSigningAlgs defaults to this value.
|
||||||
@ -49,8 +84,6 @@ type Config struct {
|
|||||||
SkipClientIDCheck bool
|
SkipClientIDCheck bool
|
||||||
// If true, token expiry is not checked.
|
// If true, token expiry is not checked.
|
||||||
SkipExpiryCheck bool
|
SkipExpiryCheck bool
|
||||||
// If true, nonce claim is not checked. Must be true if ClaimNonce field is empty.
|
|
||||||
SkipNonceCheck bool
|
|
||||||
|
|
||||||
// Time function to check Token expiry. Defaults to time.Now
|
// Time function to check Token expiry. Defaults to time.Now
|
||||||
Now func() time.Time
|
Now func() time.Time
|
||||||
@ -61,21 +94,7 @@ type Config struct {
|
|||||||
// The returned IDTokenVerifier is tied to the Provider's context and its behavior is
|
// The returned IDTokenVerifier is tied to the Provider's context and its behavior is
|
||||||
// undefined once the Provider's context is canceled.
|
// undefined once the Provider's context is canceled.
|
||||||
func (p *Provider) Verifier(config *Config) *IDTokenVerifier {
|
func (p *Provider) Verifier(config *Config) *IDTokenVerifier {
|
||||||
|
return NewVerifier(p.issuer, p.remoteKeySet, config)
|
||||||
return newVerifier(p.remoteKeySet, config, p.issuer)
|
|
||||||
}
|
|
||||||
|
|
||||||
func newVerifier(keySet *remoteKeySet, config *Config, issuer string) *IDTokenVerifier {
|
|
||||||
// If SupportedSigningAlgs is empty defaults to only support RS256.
|
|
||||||
if len(config.SupportedSigningAlgs) == 0 {
|
|
||||||
config.SupportedSigningAlgs = []string{RS256}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &IDTokenVerifier{
|
|
||||||
keySet: keySet,
|
|
||||||
config: config,
|
|
||||||
issuer: issuer,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseJWT(p string) ([]byte, error) {
|
func parseJWT(p string) ([]byte, error) {
|
||||||
@ -102,6 +121,8 @@ func contains(sli []string, ele string) bool {
|
|||||||
// Verify parses a raw ID Token, verifies it's been signed by the provider, preforms
|
// Verify parses a raw ID Token, verifies it's been signed by the provider, preforms
|
||||||
// any additional checks depending on the Config, and returns the payload.
|
// any additional checks depending on the Config, and returns the payload.
|
||||||
//
|
//
|
||||||
|
// Verify does NOT do nonce validation, which is the callers responsibility.
|
||||||
|
//
|
||||||
// See: https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
|
// See: https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
|
||||||
//
|
//
|
||||||
// oauth2Token, err := oauth2Config.Exchange(ctx, r.URL.Query().Get("code"))
|
// oauth2Token, err := oauth2Config.Exchange(ctx, r.URL.Query().Get("code"))
|
||||||
@ -120,7 +141,7 @@ func contains(sli []string, ele string) bool {
|
|||||||
func (v *IDTokenVerifier) Verify(ctx context.Context, rawIDToken string) (*IDToken, error) {
|
func (v *IDTokenVerifier) Verify(ctx context.Context, rawIDToken string) (*IDToken, error) {
|
||||||
jws, err := jose.ParseSigned(rawIDToken)
|
jws, err := jose.ParseSigned(rawIDToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("oidc: mallformed jwt: %v", err)
|
return nil, fmt.Errorf("oidc: malformed jwt: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Throw out tokens with invalid claims before trying to verify the token. This lets
|
// Throw out tokens with invalid claims before trying to verify the token. This lets
|
||||||
@ -141,6 +162,7 @@ func (v *IDTokenVerifier) Verify(ctx context.Context, rawIDToken string) (*IDTok
|
|||||||
Expiry: time.Time(token.Expiry),
|
Expiry: time.Time(token.Expiry),
|
||||||
IssuedAt: time.Time(token.IssuedAt),
|
IssuedAt: time.Time(token.IssuedAt),
|
||||||
Nonce: token.Nonce,
|
Nonce: token.Nonce,
|
||||||
|
AccessTokenHash: token.AtHash,
|
||||||
claims: payload,
|
claims: payload,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -165,7 +187,7 @@ func (v *IDTokenVerifier) Verify(ctx context.Context, rawIDToken string) (*IDTok
|
|||||||
return nil, fmt.Errorf("oidc: expected audience %q got %q", v.config.ClientID, t.Audience)
|
return nil, fmt.Errorf("oidc: expected audience %q got %q", v.config.ClientID, t.Audience)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return nil, fmt.Errorf("oidc: Invalid configuration. ClientID must be provided or SkipClientIDCheck must be set.")
|
return nil, fmt.Errorf("oidc: invalid configuration, clientID must be provided or SkipClientIDCheck must be set")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -181,37 +203,29 @@ func (v *IDTokenVerifier) Verify(ctx context.Context, rawIDToken string) (*IDTok
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If a set of required algorithms has been provided, ensure that the signatures use those.
|
switch len(jws.Signatures) {
|
||||||
var keyIDs, gotAlgs []string
|
case 0:
|
||||||
for _, sig := range jws.Signatures {
|
return nil, fmt.Errorf("oidc: id token not signed")
|
||||||
if len(v.config.SupportedSigningAlgs) == 0 || contains(v.config.SupportedSigningAlgs, sig.Header.Algorithm) {
|
case 1:
|
||||||
keyIDs = append(keyIDs, sig.Header.KeyID)
|
default:
|
||||||
} else {
|
return nil, fmt.Errorf("oidc: multiple signatures on id token not supported")
|
||||||
gotAlgs = append(gotAlgs, sig.Header.Algorithm)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(keyIDs) == 0 {
|
|
||||||
return nil, fmt.Errorf("oidc: no signatures use a supported algorithm, expected %q got %q", v.config.SupportedSigningAlgs, gotAlgs)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get keys from the remote key set. This may trigger a re-sync.
|
sig := jws.Signatures[0]
|
||||||
keys, err := v.keySet.keysWithID(ctx, keyIDs)
|
supportedSigAlgs := v.config.SupportedSigningAlgs
|
||||||
|
if len(supportedSigAlgs) == 0 {
|
||||||
|
supportedSigAlgs = []string{RS256}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !contains(supportedSigAlgs, sig.Header.Algorithm) {
|
||||||
|
return nil, fmt.Errorf("oidc: id token signed with unsupported algorithm, expected %q got %q", supportedSigAlgs, sig.Header.Algorithm)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.sigAlgorithm = sig.Header.Algorithm
|
||||||
|
|
||||||
|
gotPayload, err := v.keySet.VerifySignature(ctx, rawIDToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("oidc: get keys for id token: %v", err)
|
return nil, fmt.Errorf("failed to verify signature: %v", err)
|
||||||
}
|
|
||||||
if len(keys) == 0 {
|
|
||||||
return nil, fmt.Errorf("oidc: no keys match signature ID(s) %q", keyIDs)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to use a key to validate the signature.
|
|
||||||
var gotPayload []byte
|
|
||||||
for _, key := range keys {
|
|
||||||
if p, err := jws.Verify(&key); err == nil {
|
|
||||||
gotPayload = p
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(gotPayload) == 0 {
|
|
||||||
return nil, fmt.Errorf("oidc: failed to verify id token")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure that the payload returned by the square actually matches the payload parsed earlier.
|
// Ensure that the payload returned by the square actually matches the payload parsed earlier.
|
||||||
@ -219,19 +233,6 @@ func (v *IDTokenVerifier) Verify(ctx context.Context, rawIDToken string) (*IDTok
|
|||||||
return nil, errors.New("oidc: internal error, payload parsed did not match previous payload")
|
return nil, errors.New("oidc: internal error, payload parsed did not match previous payload")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check the nonce after we've verified the token. We don't want to allow unverified
|
|
||||||
// payloads to trigger a nonce lookup.
|
|
||||||
// If SkipNonceCheck is not set ClaimNonce cannot be Nil.
|
|
||||||
if !v.config.SkipNonceCheck && t.Nonce != "" {
|
|
||||||
if v.config.ClaimNonce != nil {
|
|
||||||
if err := v.config.ClaimNonce(t.Nonce); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return nil, fmt.Errorf("oidc: Invalid configuration. ClaimNonce must be provided or SkipNonceCheck must be set.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return t, nil
|
return t, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
2
vendor/modules.txt
vendored
2
vendor/modules.txt
vendored
@ -11,7 +11,7 @@ github.com/coreos/etcd/etcdserver/api/v3rpc/rpctypes
|
|||||||
github.com/coreos/etcd/etcdserver/etcdserverpb
|
github.com/coreos/etcd/etcdserver/etcdserverpb
|
||||||
github.com/coreos/etcd/mvcc/mvccpb
|
github.com/coreos/etcd/mvcc/mvccpb
|
||||||
github.com/coreos/etcd/pkg/tlsutil
|
github.com/coreos/etcd/pkg/tlsutil
|
||||||
# github.com/coreos/go-oidc v0.0.0-20170307191026-be73733bb8cc
|
# github.com/coreos/go-oidc v2.0.0+incompatible
|
||||||
github.com/coreos/go-oidc
|
github.com/coreos/go-oidc
|
||||||
# github.com/felixge/httpsnoop v1.0.0
|
# github.com/felixge/httpsnoop v1.0.0
|
||||||
github.com/felixge/httpsnoop
|
github.com/felixge/httpsnoop
|
||||||
|
Reference in New Issue
Block a user