Merge pull request #1686 from marcusmartins/nbf-leeway

Add leeway to JWT nbf checking
This commit is contained in:
Richard Scothern 2016-07-19 17:41:57 -07:00 committed by GitHub
commit dd355e95af
2 changed files with 92 additions and 11 deletions

View file

@ -20,6 +20,9 @@ const (
// TokenSeparator is the value which separates the header, claims, and // TokenSeparator is the value which separates the header, claims, and
// signature in the compact serialization of a JSON Web Token. // signature in the compact serialization of a JSON Web Token.
TokenSeparator = "." TokenSeparator = "."
// Leeway is the Duration that will be added to NBF and EXP claim
// checks to account for clock skew as per https://tools.ietf.org/html/rfc7519#section-4.1.5
Leeway = 60 * time.Second
) )
// Errors used by token parsing and verification. // Errors used by token parsing and verification.
@ -143,9 +146,17 @@ func (t *Token) Verify(verifyOpts VerifyOptions) error {
} }
// Verify that the token is currently usable and not expired. // Verify that the token is currently usable and not expired.
currentUnixTime := time.Now().Unix() currentTime := time.Now()
if !(t.Claims.NotBefore <= currentUnixTime && currentUnixTime <= t.Claims.Expiration) {
log.Errorf("token not to be used before %d or after %d - currently %d", t.Claims.NotBefore, t.Claims.Expiration, currentUnixTime) ExpWithLeeway := time.Unix(t.Claims.Expiration, 0).Add(Leeway)
if currentTime.After(ExpWithLeeway) {
log.Errorf("token not to be used after %s - currently %s", ExpWithLeeway, currentTime)
return ErrInvalidToken
}
NotBeforeWithLeeway := time.Unix(t.Claims.NotBefore, 0).Add(-Leeway)
if currentTime.Before(NotBeforeWithLeeway) {
log.Errorf("token not to be used before %s - currently %s", NotBeforeWithLeeway, currentTime)
return ErrInvalidToken return ErrInvalidToken
} }

View file

@ -91,7 +91,7 @@ func makeTrustedKeyMap(rootKeys []libtrust.PrivateKey) map[string]libtrust.Publi
return trustedKeys return trustedKeys
} }
func makeTestToken(issuer, audience string, access []*ResourceActions, rootKey libtrust.PrivateKey, depth int) (*Token, error) { func makeTestToken(issuer, audience string, access []*ResourceActions, rootKey libtrust.PrivateKey, depth int, now time.Time, exp time.Time) (*Token, error) {
signingKey, err := makeSigningKeyWithChain(rootKey, depth) signingKey, err := makeSigningKeyWithChain(rootKey, depth)
if err != nil { if err != nil {
return nil, fmt.Errorf("unable to make signing key with chain: %s", err) return nil, fmt.Errorf("unable to make signing key with chain: %s", err)
@ -109,8 +109,6 @@ func makeTestToken(issuer, audience string, access []*ResourceActions, rootKey l
RawJWK: &rawJWK, RawJWK: &rawJWK,
} }
now := time.Now()
randomBytes := make([]byte, 15) randomBytes := make([]byte, 15)
if _, err = rand.Read(randomBytes); err != nil { if _, err = rand.Read(randomBytes); err != nil {
return nil, fmt.Errorf("unable to read random bytes for jwt id: %s", err) return nil, fmt.Errorf("unable to read random bytes for jwt id: %s", err)
@ -120,7 +118,7 @@ func makeTestToken(issuer, audience string, access []*ResourceActions, rootKey l
Issuer: issuer, Issuer: issuer,
Subject: "foo", Subject: "foo",
Audience: audience, Audience: audience,
Expiration: now.Add(5 * time.Minute).Unix(), Expiration: exp.Unix(),
NotBefore: now.Unix(), NotBefore: now.Unix(),
IssuedAt: now.Unix(), IssuedAt: now.Unix(),
JWTID: base64.URLEncoding.EncodeToString(randomBytes), JWTID: base64.URLEncoding.EncodeToString(randomBytes),
@ -188,7 +186,7 @@ func TestTokenVerify(t *testing.T) {
tokens := make([]*Token, 0, numTokens) tokens := make([]*Token, 0, numTokens)
for i := 0; i < numTokens; i++ { for i := 0; i < numTokens; i++ {
token, err := makeTestToken(issuer, audience, access, rootKeys[i], i) token, err := makeTestToken(issuer, audience, access, rootKeys[i], i, time.Now(), time.Now().Add(5*time.Minute))
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -209,6 +207,78 @@ func TestTokenVerify(t *testing.T) {
} }
} }
// This tests that we don't fail tokens with nbf within
// the defined leeway in seconds
func TestLeeway(t *testing.T) {
var (
issuer = "test-issuer"
audience = "test-audience"
access = []*ResourceActions{
{
Type: "repository",
Name: "foo/bar",
Actions: []string{"pull", "push"},
},
}
)
rootKeys, err := makeRootKeys(1)
if err != nil {
t.Fatal(err)
}
trustedKeys := makeTrustedKeyMap(rootKeys)
verifyOps := VerifyOptions{
TrustedIssuers: []string{issuer},
AcceptedAudiences: []string{audience},
Roots: nil,
TrustedKeys: trustedKeys,
}
// nbf verification should pass within leeway
futureNow := time.Now().Add(time.Duration(5) * time.Second)
token, err := makeTestToken(issuer, audience, access, rootKeys[0], 0, futureNow, futureNow.Add(5*time.Minute))
if err != nil {
t.Fatal(err)
}
if err := token.Verify(verifyOps); err != nil {
t.Fatal(err)
}
// nbf verification should fail with a skew larger than leeway
futureNow = time.Now().Add(time.Duration(61) * time.Second)
token, err = makeTestToken(issuer, audience, access, rootKeys[0], 0, futureNow, futureNow.Add(5*time.Minute))
if err != nil {
t.Fatal(err)
}
if err = token.Verify(verifyOps); err == nil {
t.Fatal("Verification should fail for token with nbf in the future outside leeway")
}
// exp verification should pass within leeway
token, err = makeTestToken(issuer, audience, access, rootKeys[0], 0, time.Now(), time.Now().Add(-59*time.Second))
if err != nil {
t.Fatal(err)
}
if err = token.Verify(verifyOps); err != nil {
t.Fatal(err)
}
// exp verification should fail with a skew larger than leeway
token, err = makeTestToken(issuer, audience, access, rootKeys[0], 0, time.Now(), time.Now().Add(-60*time.Second))
if err != nil {
t.Fatal(err)
}
if err = token.Verify(verifyOps); err == nil {
t.Fatal("Verification should fail for token with exp in the future outside leeway")
}
}
func writeTempRootCerts(rootKeys []libtrust.PrivateKey) (filename string, err error) { func writeTempRootCerts(rootKeys []libtrust.PrivateKey) (filename string, err error) {
rootCerts, err := makeRootCerts(rootKeys) rootCerts, err := makeRootCerts(rootKeys)
if err != nil { if err != nil {
@ -307,7 +377,7 @@ func TestAccessController(t *testing.T) {
Name: testAccess.Name, Name: testAccess.Name,
Actions: []string{testAccess.Action}, Actions: []string{testAccess.Action},
}}, }},
rootKeys[1], 1, // Everything is valid except the key which signed it. rootKeys[1], 1, time.Now(), time.Now().Add(5*time.Minute), // Everything is valid except the key which signed it.
) )
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -333,7 +403,7 @@ func TestAccessController(t *testing.T) {
token, err = makeTestToken( token, err = makeTestToken(
issuer, service, issuer, service,
[]*ResourceActions{}, // No access specified. []*ResourceActions{}, // No access specified.
rootKeys[0], 1, rootKeys[0], 1, time.Now(), time.Now().Add(5*time.Minute),
) )
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -363,7 +433,7 @@ func TestAccessController(t *testing.T) {
Name: testAccess.Name, Name: testAccess.Name,
Actions: []string{testAccess.Action}, Actions: []string{testAccess.Action},
}}, }},
rootKeys[0], 1, rootKeys[0], 1, time.Now(), time.Now().Add(5*time.Minute),
) )
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)