Skip to content

Commit

Permalink
Deprecate old given key creation function and write new ones with opt…
Browse files Browse the repository at this point in the history
…ion argument
  • Loading branch information
Micah Parks committed Nov 26, 2022
1 parent d327d9a commit 9a9ca87
Show file tree
Hide file tree
Showing 8 changed files with 121 additions and 71 deletions.
6 changes: 4 additions & 2 deletions examples/custom/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ func main() {
const exampleKID = "exampleKeyID"

// Register the custom signing method.
jwt.RegisterSigningMethod(method.CustomAlg, func() jwt.SigningMethod {
jwt.RegisterSigningMethod(method.CustomAlgHeader, func() jwt.SigningMethod {
return method.EmptyCustom{}
})

Expand All @@ -29,7 +29,9 @@ func main() {

// Create the JWKS from the given signing method's key.
jwks := keyfunc.NewGiven(map[string]keyfunc.GivenKey{
exampleKID: keyfunc.NewGivenCustom(key),
exampleKID: keyfunc.NewGivenCustomWithOptions(key, keyfunc.GivenKeyOptions{
Algorithm: method.CustomAlgHeader,
}),
})

// Parse the token.
Expand Down
8 changes: 4 additions & 4 deletions examples/custom/method/method.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package method

// CustomAlg is the `alg` JSON attribute's value for the example custom jwt.SigningMethod.
const CustomAlg = "customalg"
// CustomAlgHeader is the `alg` JSON attribute's value for the example custom jwt.SigningMethod.
const CustomAlgHeader = "customalg"

// EmptyCustom implements the jwt.SigningMethod interface. It will not sign or verify anything.
type EmptyCustom struct{}
Expand All @@ -13,10 +13,10 @@ func (e EmptyCustom) Verify(_, _ string, _ interface{}) error {

// Sign helps implement the jwt.SigningMethod interface. It does not sign anything.
func (e EmptyCustom) Sign(_ string, _ interface{}) (string, error) {
return CustomAlg, nil
return CustomAlgHeader, nil
}

// Alg helps implement the jwt.SigningMethod. It returns the `alg` JSON attribute for JWTs signed with this method.
func (e EmptyCustom) Alg() string {
return CustomAlg
return CustomAlgHeader
}
4 changes: 3 additions & 1 deletion examples/given/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ func main() {
hmacSecret := []byte("example secret")
const givenKID = "givenKID"
givenKeys := map[string]keyfunc.GivenKey{
givenKID: keyfunc.NewGivenHMAC(hmacSecret),
givenKID: keyfunc.NewGivenHMACCustomWithOptions(hmacSecret, keyfunc.GivenKeyOptions{
Algorithm: jwt.SigningMethodHS256.Alg(),
}),
}

// Create the keyfunc options. Use an error handler that logs. Refresh the JWKS when a JWT signed by an unknown KID
Expand Down
4 changes: 3 additions & 1 deletion examples/hmac/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ func main() {

// Create the JWKS from the HMAC key.
jwks := keyfunc.NewGiven(map[string]keyfunc.GivenKey{
exampleKID: keyfunc.NewGivenHMAC(key),
exampleKID: keyfunc.NewGivenHMACCustomWithOptions(key, keyfunc.GivenKeyOptions{
Algorithm: jwt.SigningMethodHS512.Alg(),
}),
})

// Parse the token.
Expand Down
86 changes: 79 additions & 7 deletions given.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,31 @@ import (

// GivenKey represents a cryptographic key that resides in a JWKS. In conjuncture with Options.
type GivenKey struct {
inter interface{}
algorithm string
inter interface{}
}

type GivenKeyOptions struct {
// Algorithm is the given key's signing algorithm. Its value will be compared to unverified tokens' "alg" header.
//
// See RFC 8725 Section 3.1 for details.
// https://www.rfc-editor.org/rfc/rfc8725#section-3.1
//
// For a list of possible values, please see:
// https://www.rfc-editor.org/rfc/rfc7518#section-3.1
// https://www.iana.org/assignments/jose/jose.xhtml#web-signature-encryption-algorithms
Algorithm string
}

// NewGiven creates a JWKS from a map of given keys.
func NewGiven(givenKeys map[string]GivenKey) (jwks *JWKS) {
keys := make(map[string]parsedJWK)

for kid, given := range givenKeys {
keys[kid] = parsedJWK{public: given.inter, algorithm: given.algorithm}
keys[kid] = parsedJWK{
algorithm: given.algorithm,
public: given.inter,
}
}

return &JWKS{
Expand All @@ -26,52 +41,109 @@ func NewGiven(givenKeys map[string]GivenKey) (jwks *JWKS) {
}

// NewGivenCustom creates a new GivenKey given an untyped variable. The key argument is expected to be a supported
// by the jwt package used. To specify a required algorithm use NewGivenCustomAlg.
// by the jwt package used.
//
// See the https://pkg.go.dev/github.com/golang-jwt/jwt/v4#RegisterSigningMethod function for registering an unsupported
// signing method.
//
// Deprecated: This function does not allow the user to specify the JWT's signing algorithm. Use
// NewGivenCustomWithOptions instead.
func NewGivenCustom(key interface{}) (givenKey GivenKey) {
return GivenKey{
inter: key,
}
}

// NewGivenCustomAlg creates a new GivenKey given an untyped variable and an algorithm. The key argument is expected to
// be a type supported by the jwt package used. The alg argument will be validated against the alg header of tokens.
// NewGivenCustomWithOptions creates a new GivenKey given an untyped variable. The key argument is expected to be a type
// supported by the jwt package used.
//
// Consider the options carefully as each field may have a security implication.
//
// See the https://pkg.go.dev/github.com/golang-jwt/jwt/v4#RegisterSigningMethod function for registering an unsupported
// signing method.
func NewGivenCustomAlg(key interface{}, alg string) (givenKey GivenKey) {
func NewGivenCustomWithOptions(key interface{}, options GivenKeyOptions) (givenKey GivenKey) {
return GivenKey{
algorithm: options.Algorithm,
inter: key,
algorithm: alg,
}
}

// NewGivenECDSA creates a new GivenKey given an ECDSA public key.
//
// Deprecated: This function does not allow the user to specify the JWT's signing algorithm. Use
// NewGivenECDSACustomWithOptions instead.
func NewGivenECDSA(key *ecdsa.PublicKey) (givenKey GivenKey) {
return GivenKey{
inter: key,
}
}

// NewGivenECDSACustomWithOptions creates a new GivenKey given an ECDSA public key.
//
// Consider the options carefully as each field may have a security implication.
func NewGivenECDSACustomWithOptions(key *ecdsa.PublicKey, options GivenKeyOptions) (givenKey GivenKey) {
return GivenKey{
algorithm: options.Algorithm,
inter: key,
}
}

// NewGivenEdDSA creates a new GivenKey given an EdDSA public key.
//
// Deprecated: This function does not allow the user to specify the JWT's signing algorithm. Use
// NewGivenEdDSACustomWithOptions instead.
func NewGivenEdDSA(key ed25519.PublicKey) (givenKey GivenKey) {
return GivenKey{
inter: key,
}
}

// NewGivenEdDSACustomWithOptions creates a new GivenKey given an EdDSA public key.
//
// Consider the options carefully as each field may have a security implication.
func NewGivenEdDSACustomWithOptions(key ed25519.PublicKey, options GivenKeyOptions) (givenKey GivenKey) {
return GivenKey{
algorithm: options.Algorithm,
inter: key,
}
}

// NewGivenHMAC creates a new GivenKey given an HMAC key in a byte slice.
//
// Deprecated: This function does not allow the user to specify the JWT's signing algorithm. Use
// NewGivenHMACCustomWithOptions instead.
func NewGivenHMAC(key []byte) (givenKey GivenKey) {
return GivenKey{
inter: key,
}
}

// NewGivenHMACCustomWithOptions creates a new GivenKey given an HMAC key in a byte slice.
//
// Consider the options carefully as each field may have a security implication.
func NewGivenHMACCustomWithOptions(key []byte, options GivenKeyOptions) (givenKey GivenKey) {
return GivenKey{
algorithm: options.Algorithm,
inter: key,
}
}

// NewGivenRSA creates a new GivenKey given an RSA public key.
//
// Deprecated: This function does not allow the user to specify the JWT's signing algorithm. Use
// NewGivenRSACustomWithOptions instead.
func NewGivenRSA(key *rsa.PublicKey) (givenKey GivenKey) {
return GivenKey{
inter: key,
}
}

// NewGivenRSACustomWithOptions creates a new GivenKey given an RSA public key.
//
// Consider the options carefully as each field may have a security implication.
func NewGivenRSACustomWithOptions(key *rsa.PublicKey, options GivenKeyOptions) (givenKey GivenKey) {
return GivenKey{
algorithm: options.Algorithm,
inter: key,
}
}
42 changes: 28 additions & 14 deletions given_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ const (

// TestNewGivenCustom tests that a custom jwt.SigningMethod can be used to create a JWKS and a proper jwt.Keyfunc.
func TestNewGivenCustom(t *testing.T) {
jwt.RegisterSigningMethod(method.CustomAlg, func() jwt.SigningMethod {
jwt.RegisterSigningMethod(method.CustomAlgHeader, func() jwt.SigningMethod {
return method.EmptyCustom{}
})

Expand All @@ -40,26 +40,28 @@ func TestNewGivenCustom(t *testing.T) {
jwks := keyfunc.NewGiven(givenKeys)

token := jwt.New(method.EmptyCustom{})
token.Header[algAttribute] = method.CustomAlg
token.Header[algAttribute] = method.CustomAlgHeader
token.Header[kidAttribute] = testKID

signParseValidate(t, token, key, jwks)
}

// TestNewGivenCustomAlg tests that a custom jwt.SigningMethod can be used to create a JWKS and a proper jwt.Keyfunc.
func TestNewGivenCustomAlg(t *testing.T) {
jwt.RegisterSigningMethod(method.CustomAlg, func() jwt.SigningMethod {
jwt.RegisterSigningMethod(method.CustomAlgHeader, func() jwt.SigningMethod {
return method.EmptyCustom{}
})

const key = "test-key"
givenKeys := make(map[string]keyfunc.GivenKey)
givenKeys[testKID] = keyfunc.NewGivenCustomAlg(key, method.CustomAlg)
givenKeys[testKID] = keyfunc.NewGivenCustomWithOptions(key, keyfunc.GivenKeyOptions{
Algorithm: method.CustomAlgHeader,
})

jwks := keyfunc.NewGiven(givenKeys)

token := jwt.New(method.EmptyCustom{})
token.Header[algAttribute] = method.CustomAlg
token.Header[algAttribute] = method.CustomAlgHeader
token.Header[kidAttribute] = testKID

signParseValidate(t, token, key, jwks)
Expand All @@ -68,13 +70,15 @@ func TestNewGivenCustomAlg(t *testing.T) {
// TestNewGivenCustomAlg_NegativeCase tests that a custom jwt.SigningMethod can be used to create
// a JWKS and a proper jwt.Keyfunc and that a token with a non-matching algorithm will be rejected.
func TestNewGivenCustomAlg_NegativeCase(t *testing.T) {
jwt.RegisterSigningMethod(method.CustomAlg, func() jwt.SigningMethod {
jwt.RegisterSigningMethod(method.CustomAlgHeader, func() jwt.SigningMethod {
return method.EmptyCustom{}
})

const key = jwt.UnsafeAllowNoneSignatureType // So golang-jwt isn't the one blocking this test
const key = jwt.UnsafeAllowNoneSignatureType // Allow the "none" JWT "alg" header value for golang-jwt.
givenKeys := make(map[string]keyfunc.GivenKey)
givenKeys[testKID] = keyfunc.NewGivenCustomAlg(key, method.CustomAlg)
givenKeys[testKID] = keyfunc.NewGivenCustomWithOptions(key, keyfunc.GivenKeyOptions{
Algorithm: method.CustomAlgHeader,
})

jwks := keyfunc.NewGiven(givenKeys)

Expand All @@ -89,7 +93,7 @@ func TestNewGivenCustomAlg_NegativeCase(t *testing.T) {

parsed, err := jwt.NewParser().Parse(jwtB64, jwks.Keyfunc)
if !errors.Is(err, keyfunc.ErrJWKAlgMismatch) {
t.Fatalf("Failed to return ErrJWKAlgMismatch")
t.Fatalf("Failed to return ErrJWKAlgMismatch: %v.", err)
}

if parsed.Valid {
Expand Down Expand Up @@ -164,7 +168,9 @@ func TestNewGivenKeyRSA(t *testing.T) {
// addCustom adds a new key wto the given keys map. The new key is using a test jwt.SigningMethod.
func addCustom(givenKeys map[string]keyfunc.GivenKey, kid string) (key string) {
key = ""
givenKeys[kid] = keyfunc.NewGivenCustom(key)
givenKeys[kid] = keyfunc.NewGivenCustomWithOptions(key, keyfunc.GivenKeyOptions{
Algorithm: method.CustomAlgHeader,
})
return key
}

Expand All @@ -175,7 +181,9 @@ func addECDSA(givenKeys map[string]keyfunc.GivenKey, kid string) (key *ecdsa.Pri
return nil, fmt.Errorf("failed to create ECDSA key: %w", err)
}

givenKeys[kid] = keyfunc.NewGivenECDSA(&key.PublicKey)
givenKeys[kid] = keyfunc.NewGivenECDSACustomWithOptions(&key.PublicKey, keyfunc.GivenKeyOptions{
Algorithm: jwt.SigningMethodES256.Alg(),
})

return key, nil
}
Expand All @@ -187,7 +195,9 @@ func addEdDSA(givenKeys map[string]keyfunc.GivenKey, kid string) (key ed25519.Pr
return nil, fmt.Errorf("failed to create ECDSA key: %w", err)
}

givenKeys[kid] = keyfunc.NewGivenEdDSA(pub)
givenKeys[kid] = keyfunc.NewGivenEdDSACustomWithOptions(pub, keyfunc.GivenKeyOptions{
Algorithm: jwt.SigningMethodEdDSA.Alg(),
})

return key, nil
}
Expand All @@ -200,7 +210,9 @@ func addHMAC(givenKeys map[string]keyfunc.GivenKey, kid string) (secret []byte,
return nil, fmt.Errorf("failed to create HMAC secret: %w", err)
}

givenKeys[kid] = keyfunc.NewGivenHMAC(secret)
givenKeys[kid] = keyfunc.NewGivenHMACCustomWithOptions(secret, keyfunc.GivenKeyOptions{
Algorithm: jwt.SigningMethodHS256.Alg(),
})

return secret, nil
}
Expand All @@ -212,7 +224,9 @@ func addRSA(givenKeys map[string]keyfunc.GivenKey, kid string) (key *rsa.Private
return nil, fmt.Errorf("failed to create RSA key: %w", err)
}

givenKeys[kid] = keyfunc.NewGivenRSA(&key.PublicKey)
givenKeys[kid] = keyfunc.NewGivenRSACustomWithOptions(&key.PublicKey, keyfunc.GivenKeyOptions{
Algorithm: jwt.SigningMethodRS256.Alg(),
})

return key, nil
}
Expand Down
10 changes: 0 additions & 10 deletions jwks.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,16 +160,6 @@ func (j *JWKS) KIDs() (kids []string) {
return kids
}

// KeyAlg returns the algorithm (`alg`) for the key identified by Key ID (`kid`).
func (j *JWKS) KeyAlg(kid string) string {
j.mux.RLock()
defer j.mux.RUnlock()
if pubKey, ok := j.keys[kid]; ok {
return pubKey.algorithm
}
return ""
}

// Len returns the number of keys in the JWKS.
func (j *JWKS) Len() int {
j.mux.RLock()
Expand Down
32 changes: 0 additions & 32 deletions jwks_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -296,38 +296,6 @@ func TestJWKS_Len(t *testing.T) {
}
}

// TestJWKS_KeyAlg confirms the JWKS.Len returns the algorithm for keys by kid.
func TestJWKS_KeyAlg(t *testing.T) {
jwks, err := keyfunc.NewJSON([]byte(jwksJSON))
if err != nil {
t.Fatalf(logFmt, "Failed to create a JWKS from JSON.", err)
}

expectedAlgs := map[string]string{
"zXew0UJ1h6Q4CCcd_9wxMzvcp5cEBifH0KWrCz2Kyxc": "PS256",
"ebJxnm9B3QDBljB5XJWEu72qx6BawDaMAhwz4aKPkQ0": "ES512",
"TVAAet63O3xy_KK6_bxVIu7Ra3_z1wlB543Fbwi5VaU": "ES384",
"arlUxX4hh56rNO-XdIPhDT7bqBMqcBwNQuP_TnZJNGs": "RS512",
"tW6ae7TomE6_2jooM-sf9N_6lWg7HNtaQXrDsElBzM4": "PS512",
"Lx1FmayP2YBtxaqS1SKJRJGiXRKnw2ov5WmYIMG-BLE": "PS384",
"gnmAfvmlsi3kKH3VlM1AJ85P2hekQ8ON_XvJqs3xPD8": "RS384",
"CGt0ZWS4Lc5faiKSdi0tU0fjCAdvGROQRGU9iR7tV0A": "ES256",
"C65q0EKQyhpd1m4fr7SKO2He_nAxgCtAdws64d2BLt8": "RS256",
"Q56A": "",
"hmac": "",
"WW91IGdldCBhIGdvbGQgc3RhciDwn4yfCg": "",
}

for kid, expectedAlg := range expectedAlgs {
t.Run(kid, func(t *testing.T) {
actualAlg := jwks.KeyAlg(kid)
if actualAlg != expectedAlg {
t.Errorf("Unexpected alg for key %v.\n Expected: %v\n Actual: %v\n", kid, expectedAlg, actualAlg)
}
})
}
}

// TestRateLimit performs a test to confirm the rate limiter works as expected.
func TestRateLimit(t *testing.T) {
tempDir, err := os.MkdirTemp("", "*")
Expand Down

0 comments on commit 9a9ca87

Please sign in to comment.