Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Key rotation with VerificationKeySet #344

Merged
merged 10 commits into from
Sep 13, 2023
36 changes: 26 additions & 10 deletions parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,24 +74,40 @@ func (p *Parser) ParseWithClaims(tokenString string, claims Claims, keyFunc Keyf
}
}

// Lookup key
var key interface{}
// Decode signature
token.Signature, err = p.DecodeSegment(parts[2])
if err != nil {
return token, newError("could not base64 decode signature", ErrTokenMalformed, err)
}
text := strings.Join(parts[0:2], ".")

// Lookup key(s)
if keyFunc == nil {
// keyFunc was not provided. short circuiting validation
return token, newError("no keyfunc was provided", ErrTokenUnverifiable)
}
if key, err = keyFunc(token); err != nil {
return token, newError("error while executing keyfunc", ErrTokenUnverifiable, err)
}

// Decode signature
token.Signature, err = p.DecodeSegment(parts[2])
got, err := keyFunc(token)
if err != nil {
return token, newError("could not base64 decode signature", ErrTokenMalformed, err)
return token, newError("error while executing keyfunc", ErrTokenUnverifiable, err)
}

// Perform signature validation
if err = token.Method.Verify(strings.Join(parts[0:2], "."), token.Signature, key); err != nil {
switch have := got.(type) {
case PublicKeyset:
oxisto marked this conversation as resolved.
Show resolved Hide resolved
if len(have.Keys) == 0 {
return token, newError("keyfunc returned empty keyset", ErrTokenUnverifiable)
oxisto marked this conversation as resolved.
Show resolved Hide resolved
}
// Iterate through keys and verify signature, skipping the rest when a match is found.
// Return the last error if no match is found.
for _, key := range have.Keys {
if err = token.Method.Verify(text, token.Signature, key); err == nil {
break
}
}
default:
err = token.Method.Verify(text, token.Signature, have)
}
if err != nil {
return token, newError("", ErrTokenSignatureInvalid, err)
}

Expand Down
74 changes: 74 additions & 0 deletions parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,20 @@ var (
emptyKeyFunc jwt.Keyfunc = func(t *jwt.Token) (interface{}, error) { return nil, nil }
errorKeyFunc jwt.Keyfunc = func(t *jwt.Token) (interface{}, error) { return nil, errKeyFuncError }
nilKeyFunc jwt.Keyfunc = nil
multipleZeroKeyFunc jwt.Keyfunc = func(t *jwt.Token) (interface{}, error) { return []interface{}{}, nil }
multipleEmptyKeyFunc jwt.Keyfunc = func(t *jwt.Token) (interface{}, error) { return []interface{}{nil, nil}, nil }
multiplePublicKeysFunc jwt.Keyfunc = func(t *jwt.Token) (interface{}, error) {
return []jwt.PublicKey{jwtTestDefaultKey, jwtTestEC256PublicKey}, nil
}
multipleLastKeyFunc jwt.Keyfunc = func(t *jwt.Token) (interface{}, error) {
return jwt.PublicKeyset{Keys: []jwt.PublicKey{jwtTestEC256PublicKey, jwtTestDefaultKey}}, nil
}
multipleFirstKeyFunc jwt.Keyfunc = func(t *jwt.Token) (interface{}, error) {
return jwt.PublicKeyset{Keys: []jwt.PublicKey{jwtTestDefaultKey, jwtTestEC256PublicKey}}, nil
}
multipleAltTypedKeyFunc jwt.Keyfunc = func(t *jwt.Token) (interface{}, error) {
return jwt.PublicKeyset{Keys: []jwt.PublicKey{jwtTestDefaultKey, jwtTestDefaultKey}}, nil
}
)

func init() {
Expand Down Expand Up @@ -94,6 +108,46 @@ var jwtTestData = []struct {
nil,
jwt.SigningMethodRS256,
},
{
"multiple keys, last matches",
"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJmb28iOiJiYXIifQ.FhkiHkoESI_cG3NPigFrxEk9Z60_oXrOT2vGm9Pn6RDgYNovYORQmmA0zs1AoAOf09ly2Nx2YAg6ABqAYga1AcMFkJljwxTT5fYphTuqpWdy4BELeSYJx5Ty2gmr8e7RonuUztrdD5WfPqLKMm1Ozp_T6zALpRmwTIW0QPnaBXaQD90FplAg46Iy1UlDKr-Eupy0i5SLch5Q-p2ZpaL_5fnTIUDlxC3pWhJTyx_71qDI-mAA_5lE_VdroOeflG56sSmDxopPEG3bFlSu1eowyBfxtu0_CuVd-M42RU75Zc4Gsj6uV77MBtbMrf4_7M_NUTSgoIF3fRqxrj0NzihIBg",
multipleLastKeyFunc,
jwt.MapClaims{"foo": "bar"},
true,
nil,
nil,
jwt.SigningMethodRS256,
},
{
"multiple keys not []interface{} type, all match",
"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJmb28iOiJiYXIifQ.FhkiHkoESI_cG3NPigFrxEk9Z60_oXrOT2vGm9Pn6RDgYNovYORQmmA0zs1AoAOf09ly2Nx2YAg6ABqAYga1AcMFkJljwxTT5fYphTuqpWdy4BELeSYJx5Ty2gmr8e7RonuUztrdD5WfPqLKMm1Ozp_T6zALpRmwTIW0QPnaBXaQD90FplAg46Iy1UlDKr-Eupy0i5SLch5Q-p2ZpaL_5fnTIUDlxC3pWhJTyx_71qDI-mAA_5lE_VdroOeflG56sSmDxopPEG3bFlSu1eowyBfxtu0_CuVd-M42RU75Zc4Gsj6uV77MBtbMrf4_7M_NUTSgoIF3fRqxrj0NzihIBg",
multipleAltTypedKeyFunc,
jwt.MapClaims{"foo": "bar"},
true,
nil,
nil,
jwt.SigningMethodRS256,
},
{
"multiple keys, first matches",
"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJmb28iOiJiYXIifQ.FhkiHkoESI_cG3NPigFrxEk9Z60_oXrOT2vGm9Pn6RDgYNovYORQmmA0zs1AoAOf09ly2Nx2YAg6ABqAYga1AcMFkJljwxTT5fYphTuqpWdy4BELeSYJx5Ty2gmr8e7RonuUztrdD5WfPqLKMm1Ozp_T6zALpRmwTIW0QPnaBXaQD90FplAg46Iy1UlDKr-Eupy0i5SLch5Q-p2ZpaL_5fnTIUDlxC3pWhJTyx_71qDI-mAA_5lE_VdroOeflG56sSmDxopPEG3bFlSu1eowyBfxtu0_CuVd-M42RU75Zc4Gsj6uV77MBtbMrf4_7M_NUTSgoIF3fRqxrj0NzihIBg",
multipleFirstKeyFunc,
jwt.MapClaims{"foo": "bar"},
true,
nil,
nil,
jwt.SigningMethodRS256,
},
{
"public keys slice, not allowed",
"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJmb28iOiJiYXIifQ.FhkiHkoESI_cG3NPigFrxEk9Z60_oXrOT2vGm9Pn6RDgYNovYORQmmA0zs1AoAOf09ly2Nx2YAg6ABqAYga1AcMFkJljwxTT5fYphTuqpWdy4BELeSYJx5Ty2gmr8e7RonuUztrdD5WfPqLKMm1Ozp_T6zALpRmwTIW0QPnaBXaQD90FplAg46Iy1UlDKr-Eupy0i5SLch5Q-p2ZpaL_5fnTIUDlxC3pWhJTyx_71qDI-mAA_5lE_VdroOeflG56sSmDxopPEG3bFlSu1eowyBfxtu0_CuVd-M42RU75Zc4Gsj6uV77MBtbMrf4_7M_NUTSgoIF3fRqxrj0NzihIBg",
multiplePublicKeysFunc,
jwt.MapClaims{"foo": "bar"},
false,
[]error{jwt.ErrTokenSignatureInvalid},
nil,
jwt.SigningMethodRS256,
},
{
"basic expired",
"", // autogen
Expand Down Expand Up @@ -154,6 +208,26 @@ var jwtTestData = []struct {
nil,
jwt.SigningMethodRS256,
},
{
"multiple nokey",
"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJmb28iOiJiYXIifQ.FhkiHkoESI_cG3NPigFrxEk9Z60_oXrOT2vGm9Pn6RDgYNovYORQmmA0zs1AoAOf09ly2Nx2YAg6ABqAYga1AcMFkJljwxTT5fYphTuqpWdy4BELeSYJx5Ty2gmr8e7RonuUztrdD5WfPqLKMm1Ozp_T6zALpRmwTIW0QPnaBXaQD90FplAg46Iy1UlDKr-Eupy0i5SLch5Q-p2ZpaL_5fnTIUDlxC3pWhJTyx_71qDI-mAA_5lE_VdroOeflG56sSmDxopPEG3bFlSu1eowyBfxtu0_CuVd-M42RU75Zc4Gsj6uV77MBtbMrf4_7M_NUTSgoIF3fRqxrj0NzihIBg",
multipleEmptyKeyFunc,
jwt.MapClaims{"foo": "bar"},
false,
[]error{jwt.ErrTokenSignatureInvalid},
nil,
jwt.SigningMethodRS256,
},
{
"zero length key list",
"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJmb28iOiJiYXIifQ.FhkiHkoESI_cG3NPigFrxEk9Z60_oXrOT2vGm9Pn6RDgYNovYORQmmA0zs1AoAOf09ly2Nx2YAg6ABqAYga1AcMFkJljwxTT5fYphTuqpWdy4BELeSYJx5Ty2gmr8e7RonuUztrdD5WfPqLKMm1Ozp_T6zALpRmwTIW0QPnaBXaQD90FplAg46Iy1UlDKr-Eupy0i5SLch5Q-p2ZpaL_5fnTIUDlxC3pWhJTyx_71qDI-mAA_5lE_VdroOeflG56sSmDxopPEG3bFlSu1eowyBfxtu0_CuVd-M42RU75Zc4Gsj6uV77MBtbMrf4_7M_NUTSgoIF3fRqxrj0NzihIBg",
multipleZeroKeyFunc,
jwt.MapClaims{"foo": "bar"},
false,
[]error{jwt.ErrTokenSignatureInvalid},
nil,
jwt.SigningMethodRS256,
},
{
"basic errorkey",
"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJmb28iOiJiYXIifQ.FhkiHkoESI_cG3NPigFrxEk9Z60_oXrOT2vGm9Pn6RDgYNovYORQmmA0zs1AoAOf09ly2Nx2YAg6ABqAYga1AcMFkJljwxTT5fYphTuqpWdy4BELeSYJx5Ty2gmr8e7RonuUztrdD5WfPqLKMm1Ozp_T6zALpRmwTIW0QPnaBXaQD90FplAg46Iy1UlDKr-Eupy0i5SLch5Q-p2ZpaL_5fnTIUDlxC3pWhJTyx_71qDI-mAA_5lE_VdroOeflG56sSmDxopPEG3bFlSu1eowyBfxtu0_CuVd-M42RU75Zc4Gsj6uV77MBtbMrf4_7M_NUTSgoIF3fRqxrj0NzihIBg",
Expand Down
15 changes: 15 additions & 0 deletions token.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package jwt

import (
"crypto"
"encoding/base64"
"encoding/json"
)
Expand All @@ -9,8 +10,22 @@ import (
// the key for verification. The function receives the parsed, but unverified
// Token. This allows you to use properties in the Header of the token (such as
// `kid`) to identify which key to use.
//
// The returned interface{} may be a single key or a PublicKeyset containing
// multiple keys.
type Keyfunc func(*Token) (interface{}, error)

// PublicKey represents a generic public key interface.
oxisto marked this conversation as resolved.
Show resolved Hide resolved
type PublicKey interface {
crypto.PublicKey | []uint8
oxisto marked this conversation as resolved.
Show resolved Hide resolved
}

// PublicKeyset is a set of public keys that can be used to verify a token. It is used by the parser
// to verify a token.
type PublicKeyset struct {
oxisto marked this conversation as resolved.
Show resolved Hide resolved
Keys []PublicKey
}

// Token represents a JWT Token. Different fields will be used depending on
// whether you're creating or parsing/verifying a token.
type Token struct {
Expand Down