From 5011cdf7d417cb7485794288333e4effc026c7d4 Mon Sep 17 00:00:00 2001 From: Eric Chiang Date: Wed, 11 May 2022 09:52:37 -0700 Subject: [PATCH] oidc: add StaticKeySet This has been extremely useful for testing, but I've been hesitant to add any API surface that references our underlying JOSE library. Add a StaticKeySet that only accepts keys of type *rsa.PublicKey and *ecdsa.PublicKey. --- oidc/jwks.go | 32 ++++++++++++++++++++++++++++++++ oidc/jwks_test.go | 1 - oidc/verify.go | 11 +++-------- oidc/verify_test.go | 24 +++++------------------- 4 files changed, 40 insertions(+), 28 deletions(-) diff --git a/oidc/jwks.go b/oidc/jwks.go index a272b7ab..6d2ffee9 100644 --- a/oidc/jwks.go +++ b/oidc/jwks.go @@ -2,6 +2,9 @@ package oidc import ( "context" + "crypto" + "crypto/ecdsa" + "crypto/rsa" "errors" "fmt" "io/ioutil" @@ -12,6 +15,35 @@ import ( jose "gopkg.in/square/go-jose.v2" ) +// StaticKeySet is a verifier that validates JWT against a static set of public keys. +type StaticKeySet struct { + // PublicKeys used to verify the JWT. Supported types are *rsa.PublicKey and + // *ecdsa.PublicKey. + PublicKeys []crypto.PublicKey +} + +// VerifySignature compares the signature against a static set of public keys. +func (s *StaticKeySet) VerifySignature(ctx context.Context, jwt string) ([]byte, error) { + jws, err := jose.ParseSigned(jwt) + if err != nil { + return nil, fmt.Errorf("parsing jwt: %v", err) + } + for _, pub := range s.PublicKeys { + switch pub.(type) { + case *rsa.PublicKey: + case *ecdsa.PublicKey: + default: + return nil, fmt.Errorf("invalid public key type provided: %T", pub) + } + payload, err := jws.Verify(pub) + if err != nil { + continue + } + return payload, nil + } + return nil, fmt.Errorf("no public keys able to verify jwt") +} + // 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 diff --git a/oidc/jwks_test.go b/oidc/jwks_test.go index 4103e4bc..69b55605 100644 --- a/oidc/jwks_test.go +++ b/oidc/jwks_test.go @@ -58,7 +58,6 @@ func (s *signingKey) sign(t *testing.T, payload []byte) string { return data } -// jwk returns the public part of the signing key. func (s *signingKey) jwk() jose.JSONWebKey { return jose.JSONWebKey{Key: s.pub, Use: "sig", Algorithm: string(s.alg), KeyID: s.keyID} } diff --git a/oidc/verify.go b/oidc/verify.go index c2a9f48d..3672058e 100644 --- a/oidc/verify.go +++ b/oidc/verify.go @@ -55,15 +55,10 @@ type IDTokenVerifier struct { // 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: +// Or a static key set (e.g. for testing): // -// // Custom KeySet implementation. -// keySet := newStatisKeySet(publicKeys...) -// -// // Verifier uses the custom KeySet implementation. -// verifier := oidc.NewVerifier("https://auth.example.com", keySet, config) +// keySet := &oidc.StaticKeySet{PublicKeys: []crypto.PublicKey{pub1, pub2}} +// verifier := oidc.NewVerifier("https://accounts.google.com", keySet, config) // func NewVerifier(issuerURL string, keySet KeySet, config *Config) *IDTokenVerifier { return &IDTokenVerifier{keySet: keySet, config: config, issuer: issuerURL} diff --git a/oidc/verify_test.go b/oidc/verify_test.go index d2fffa9c..b8a13eab 100644 --- a/oidc/verify_test.go +++ b/oidc/verify_test.go @@ -2,7 +2,7 @@ package oidc import ( "context" - "fmt" + "crypto" "io" "net/http" "net/http/httptest" @@ -10,22 +10,8 @@ import ( "strconv" "testing" "time" - - jose "gopkg.in/square/go-jose.v2" ) -type testVerifier struct { - jwk jose.JSONWebKey -} - -func (t *testVerifier) 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 jws.Verify(&t.jwk) -} - func TestVerify(t *testing.T) { tests := []verificationTest{ { @@ -513,9 +499,9 @@ func (v resolverTest) testEndpoint(t *testing.T) ([]byte, error) { issuer := v.issuer var ks KeySet if v.verificationKey == nil { - ks = &testVerifier{v.signKey.jwk()} + ks = &StaticKeySet{PublicKeys: []crypto.PublicKey{v.signKey.pub}} } else { - ks = &testVerifier{v.verificationKey.jwk()} + ks = &StaticKeySet{PublicKeys: []crypto.PublicKey{v.verificationKey.pub}} } verifier := NewVerifier(issuer, ks, &v.config) @@ -560,9 +546,9 @@ func (v verificationTest) runGetToken(t *testing.T) (*IDToken, error) { } var ks KeySet if v.verificationKey == nil { - ks = &testVerifier{v.signKey.jwk()} + ks = &StaticKeySet{PublicKeys: []crypto.PublicKey{v.signKey.pub}} } else { - ks = &testVerifier{v.verificationKey.jwk()} + ks = &StaticKeySet{PublicKeys: []crypto.PublicKey{v.verificationKey.pub}} } verifier := NewVerifier(issuer, ks, &v.config)