From 716873a23681e84b69833ff79d07f15e78ea0b6c Mon Sep 17 00:00:00 2001 From: Jan Brinker Date: Thu, 7 Sep 2017 16:39:48 +0200 Subject: [PATCH 1/8] Added leeway for date validation issue #54 --- Sources/ClaimSet.swift | 22 +++++++++++----------- Sources/Claims.swift | 6 +++--- Sources/Decode.swift | 8 ++++---- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/Sources/ClaimSet.swift b/Sources/ClaimSet.swift index 7626498..3632bc2 100644 --- a/Sources/ClaimSet.swift +++ b/Sources/ClaimSet.swift @@ -93,7 +93,7 @@ extension ClaimSet { // MARK: Validations extension ClaimSet { - public func validate(audience: String? = nil, issuer: String? = nil) throws { + public func validate(audience: String? = nil, issuer: String? = nil, leeway: TimeInterval = 0) throws { if let issuer = issuer { try validateIssuer(issuer) } @@ -101,10 +101,10 @@ extension ClaimSet { if let audience = audience { try validateAudience(audience) } - - try validateExpiary() - try validateNotBefore() - try validateIssuedAt() + + try validateExpiary(leeway: leeway) + try validateNotBefore(leeway: leeway) + try validateIssuedAt(leeway: leeway) } public func validateAudience(_ audience: String) throws { @@ -131,16 +131,16 @@ extension ClaimSet { } } - public func validateExpiary() throws { - try validateDate(claims, key: "exp", comparison: .orderedAscending, failure: .expiredSignature, decodeError: "Expiration time claim (exp) must be an integer") + public func validateExpiary(leeway: TimeInterval = 0) throws { + try validateDate(claims, key: "exp", comparison: .orderedAscending, leeway: (-1 * leeway), failure: .expiredSignature, decodeError: "Expiration time claim (exp) must be an integer") } - public func validateNotBefore() throws { - try validateDate(claims, key: "nbf", comparison: .orderedDescending, failure: .immatureSignature, decodeError: "Not before claim (nbf) must be an integer") + public func validateNotBefore(leeway: TimeInterval = 0) throws { + try validateDate(claims, key: "nbf", comparison: .orderedDescending, leeway: leeway, failure: .immatureSignature, decodeError: "Not before claim (nbf) must be an integer") } - public func validateIssuedAt() throws { - try validateDate(claims, key: "iat", comparison: .orderedDescending, failure: .invalidIssuedAt, decodeError: "Issued at claim (iat) must be an integer") + public func validateIssuedAt(leeway: TimeInterval = 0) throws { + try validateDate(claims, key: "iat", comparison: .orderedDescending, leeway: leeway, failure: .invalidIssuedAt, decodeError: "Issued at claim (iat) must be an integer") } } diff --git a/Sources/Claims.swift b/Sources/Claims.swift index ad07027..d1df6f4 100644 --- a/Sources/Claims.swift +++ b/Sources/Claims.swift @@ -1,6 +1,6 @@ import Foundation -func validateDate(_ payload: Payload, key: String, comparison: ComparisonResult, failure: InvalidToken, decodeError: String) throws { +func validateDate(_ payload: Payload, key: String, comparison: ComparisonResult, leeway: TimeInterval = 0, failure: InvalidToken, decodeError: String) throws { if payload[key] == nil { return } @@ -8,8 +8,8 @@ func validateDate(_ payload: Payload, key: String, comparison: ComparisonResult, guard let date = extractDate(payload: payload, key: key) else { throw InvalidToken.decodeError(decodeError) } - - if date.compare(Date()) == comparison { + + if date.compare(Date().addingTimeInterval(leeway)) == comparison { throw failure } } diff --git a/Sources/Decode.swift b/Sources/Decode.swift index ce802d7..3e7ca41 100644 --- a/Sources/Decode.swift +++ b/Sources/Decode.swift @@ -47,11 +47,11 @@ public enum InvalidToken: CustomStringConvertible, Error { /// Decode a JWT -public func decode(_ jwt: String, algorithms: [Algorithm], verify: Bool = true, audience: String? = nil, issuer: String? = nil) throws -> ClaimSet { +public func decode(_ jwt: String, algorithms: [Algorithm], verify: Bool = true, audience: String? = nil, issuer: String? = nil, leeway: TimeInterval = 0) throws -> ClaimSet { let (header, claims, signature, signatureInput) = try load(jwt) if verify { - try claims.validate(audience: audience, issuer: issuer) + try claims.validate(audience: audience, issuer: issuer, leeway: leeway) try verifySignature(algorithms, header: header, signingInput: signatureInput, signature: signature) } @@ -59,8 +59,8 @@ public func decode(_ jwt: String, algorithms: [Algorithm], verify: Bool = true, } /// Decode a JWT -public func decode(_ jwt: String, algorithm: Algorithm, verify: Bool = true, audience: String? = nil, issuer: String? = nil) throws -> ClaimSet { - return try decode(jwt, algorithms: [algorithm], verify: verify, audience: audience, issuer: issuer) +public func decode(_ jwt: String, algorithm: Algorithm, verify: Bool = true, audience: String? = nil, issuer: String? = nil, leeway: TimeInterval = 0) throws -> ClaimSet { + return try decode(jwt, algorithms: [algorithm], verify: verify, audience: audience, issuer: issuer, leeway: leeway) } /// Decode a JWT From 803fcea3b1b150b66dc08f7eaea624a7c4b48bed Mon Sep 17 00:00:00 2001 From: Jan Brinker Date: Thu, 7 Sep 2017 16:40:12 +0200 Subject: [PATCH 2/8] Added unit tests for date validation with leeway. --- Tests/JWTTests/JWTTests.swift | 83 +++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/Tests/JWTTests/JWTTests.swift b/Tests/JWTTests/JWTTests.swift index a0e1b8f..2659230 100644 --- a/Tests/JWTTests/JWTTests.swift +++ b/Tests/JWTTests/JWTTests.swift @@ -279,6 +279,89 @@ class DecodeTests: XCTestCase { } } +class ValidationTests: XCTestCase { + func testClaimJustExpiredWithoutLeeway() { + var claims = ClaimSet() + claims.expiration = Date().addingTimeInterval(-1) + + let expectation = XCTestExpectation(description: "Signature should be expired.") + do { + try claims.validateExpiary() + XCTFail("InvalidToken.expiredSignature error should have been thrown.") + } catch InvalidToken.expiredSignature { + expectation.fulfill() + } catch { + XCTFail("Unexpected error while validating exp claim.") + } + self.wait(for: [expectation], timeout: 0.5) + } + + func testClaimJustNotExpiredWithoutLeeway() { + var claims = ClaimSet() + claims.expiration = Date().addingTimeInterval(-1) + + do { + try claims.validateExpiary(leeway: 2) + } catch { + XCTFail("Unexpected error while validating exp claim that should be valid with leeway.") + } + } + + func testNotBeforeIsImmatureSignatureWithoutLeeway() { + var claims = ClaimSet() + claims.notBefore = Date().addingTimeInterval(1) + + let expectation = XCTestExpectation(description: "Signature should be immature.") + do { + try claims.validateNotBefore() + XCTFail("InvalidToken.immatureSignature error should have been thrown.") + } catch InvalidToken.immatureSignature { + expectation.fulfill() + } catch { + XCTFail("Unexpected error while validating nbf claim.") + } + self.wait(for: [expectation], timeout: 0.5) + } + + func testNotBeforeIsValidWithLeeway() { + var claims = ClaimSet() + claims.notBefore = Date().addingTimeInterval(1) + + do { + try claims.validateNotBefore(leeway: 2) + } catch { + XCTFail("Unexpected error while validating nbf claim that should be valid with leeway.") + } + } + + func testIssuedAtIsInFutureWithoutLeeway() { + var claims = ClaimSet() + claims.issuedAt = Date().addingTimeInterval(1) + + let expectation = XCTestExpectation(description: "iat should be in the future.") + do { + try claims.validateIssuedAt() + XCTFail("InvalidToken.invalidIssuedAt error should have been thrown.") + } catch InvalidToken.invalidIssuedAt { + expectation.fulfill() + } catch { + XCTFail("Unexpected error while validating iat claim.") + } + self.wait(for: [expectation], timeout: 0.5) + } + + func testIssuedAtIsValidWithLeeway() { + var claims = ClaimSet() + claims.issuedAt = Date().addingTimeInterval(1) + + do { + try claims.validateIssuedAt(leeway: 2) + } catch { + XCTFail("Unexpected error while validating iat claim that should be valid with leeway.") + } + } +} + // MARK: Helpers func assertSuccess(_ decoder: @autoclosure () throws -> Payload, closure: ((Payload) -> Void)? = nil) { From 78dc2010cf00a7d3c9bfcddacd554bf6096697aa Mon Sep 17 00:00:00 2001 From: Jan Brinker Date: Thu, 7 Sep 2017 16:40:34 +0200 Subject: [PATCH 3/8] Added some integration tests for verification of dates with and without leeway. --- Tests/JWTTests/JWTTests.swift | 40 +++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/Tests/JWTTests/JWTTests.swift b/Tests/JWTTests/JWTTests.swift index 2659230..4c1e8a9 100644 --- a/Tests/JWTTests/JWTTests.swift +++ b/Tests/JWTTests/JWTTests.swift @@ -362,6 +362,46 @@ class ValidationTests: XCTestCase { } } +class IntegrationTests: XCTestCase { + func testVerificationFailureWithoutLeeway() { + let token = JWT.encode(.none) { builder in + builder.issuer = "fuller.li" + builder.audience = "cocoapods" + builder.expiration = Date().addingTimeInterval(-1) // Token expired one second ago + builder.notBefore = Date().addingTimeInterval(1) // Token starts being valid in one second + builder.issuedAt = Date().addingTimeInterval(1) // Token is issued one second in the future + } + + let expectation = XCTestExpectation(description: "Verification should fail.") + do { + let _ = try JWT.decode(token, algorithm: .none) + XCTFail("InvalidToken error should have been thrown.") + } catch is InvalidToken { + expectation.fulfill() + } catch { + XCTFail("Unexpected error type while verifying token.") + } + self.wait(for: [expectation], timeout: 0.5) + } + + func testVerificationSuccessWithLeeway() { + let token = JWT.encode(.none) { builder in + builder.issuer = "fuller.li" + builder.audience = "cocoapods" + builder.expiration = Date().addingTimeInterval(-1) // Token expired one second ago + builder.notBefore = Date().addingTimeInterval(1) // Token starts being valid in one second + builder.issuedAt = Date().addingTimeInterval(1) // Token is issued one second in the future + } + + do { + let _ = try JWT.decode(token, algorithm: .none, leeway: 2) + // Due to leeway no error gets thrown. + } catch { + XCTFail("Unexpected error type while verifying token.") + } + } +} + // MARK: Helpers func assertSuccess(_ decoder: @autoclosure () throws -> Payload, closure: ((Payload) -> Void)? = nil) { From 0a3e73538185a0db5bc52d7ea339134384bc91c1 Mon Sep 17 00:00:00 2001 From: Jan Brinker Date: Thu, 7 Sep 2017 16:46:43 +0200 Subject: [PATCH 4/8] Repaired indentation mix of spaces and tabs. --- Sources/ClaimSet.swift | 4 ++-- Sources/Decode.swift | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/ClaimSet.swift b/Sources/ClaimSet.swift index 3632bc2..7dcf872 100644 --- a/Sources/ClaimSet.swift +++ b/Sources/ClaimSet.swift @@ -131,8 +131,8 @@ extension ClaimSet { } } - public func validateExpiary(leeway: TimeInterval = 0) throws { - try validateDate(claims, key: "exp", comparison: .orderedAscending, leeway: (-1 * leeway), failure: .expiredSignature, decodeError: "Expiration time claim (exp) must be an integer") + public func validateExpiary(leeway: TimeInterval = 0) throws { + try validateDate(claims, key: "exp", comparison: .orderedAscending, leeway: (-1 * leeway), failure: .expiredSignature, decodeError: "Expiration time claim (exp) must be an integer") } public func validateNotBefore(leeway: TimeInterval = 0) throws { diff --git a/Sources/Decode.swift b/Sources/Decode.swift index 3e7ca41..3054160 100644 --- a/Sources/Decode.swift +++ b/Sources/Decode.swift @@ -51,7 +51,7 @@ public func decode(_ jwt: String, algorithms: [Algorithm], verify: Bool = true, let (header, claims, signature, signatureInput) = try load(jwt) if verify { - try claims.validate(audience: audience, issuer: issuer, leeway: leeway) + try claims.validate(audience: audience, issuer: issuer, leeway: leeway) try verifySignature(algorithms, header: header, signingInput: signatureInput, signature: signature) } @@ -60,7 +60,7 @@ public func decode(_ jwt: String, algorithms: [Algorithm], verify: Bool = true, /// Decode a JWT public func decode(_ jwt: String, algorithm: Algorithm, verify: Bool = true, audience: String? = nil, issuer: String? = nil, leeway: TimeInterval = 0) throws -> ClaimSet { - return try decode(jwt, algorithms: [algorithm], verify: verify, audience: audience, issuer: issuer, leeway: leeway) + return try decode(jwt, algorithms: [algorithm], verify: verify, audience: audience, issuer: issuer, leeway: leeway) } /// Decode a JWT From 5f6907e46f097f7726cd4e82a43a87b6817da5b8 Mon Sep 17 00:00:00 2001 From: Jan Brinker Date: Fri, 8 Sep 2017 09:46:26 +0200 Subject: [PATCH 5/8] Fixing unit test build error on Linux. --- Tests/JWTTests/JWTTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/JWTTests/JWTTests.swift b/Tests/JWTTests/JWTTests.swift index 4c1e8a9..337b664 100644 --- a/Tests/JWTTests/JWTTests.swift +++ b/Tests/JWTTests/JWTTests.swift @@ -374,7 +374,7 @@ class IntegrationTests: XCTestCase { let expectation = XCTestExpectation(description: "Verification should fail.") do { - let _ = try JWT.decode(token, algorithm: .none) + let _ = try JWT.decode(token, algorithm: .none, leeway: 0) XCTFail("InvalidToken error should have been thrown.") } catch is InvalidToken { expectation.fulfill() From 6affa9356f1542124f288fedf0186dd35a4dcebc Mon Sep 17 00:00:00 2001 From: Jan Brinker Date: Fri, 8 Sep 2017 12:54:01 +0200 Subject: [PATCH 6/8] Removed XCTestExpectations from validation tests. --- Tests/JWTTests/JWTTests.swift | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/Tests/JWTTests/JWTTests.swift b/Tests/JWTTests/JWTTests.swift index 337b664..b8ffe50 100644 --- a/Tests/JWTTests/JWTTests.swift +++ b/Tests/JWTTests/JWTTests.swift @@ -283,17 +283,15 @@ class ValidationTests: XCTestCase { func testClaimJustExpiredWithoutLeeway() { var claims = ClaimSet() claims.expiration = Date().addingTimeInterval(-1) - - let expectation = XCTestExpectation(description: "Signature should be expired.") + do { try claims.validateExpiary() XCTFail("InvalidToken.expiredSignature error should have been thrown.") } catch InvalidToken.expiredSignature { - expectation.fulfill() + // Correct error thrown } catch { XCTFail("Unexpected error while validating exp claim.") } - self.wait(for: [expectation], timeout: 0.5) } func testClaimJustNotExpiredWithoutLeeway() { @@ -311,16 +309,14 @@ class ValidationTests: XCTestCase { var claims = ClaimSet() claims.notBefore = Date().addingTimeInterval(1) - let expectation = XCTestExpectation(description: "Signature should be immature.") do { try claims.validateNotBefore() XCTFail("InvalidToken.immatureSignature error should have been thrown.") } catch InvalidToken.immatureSignature { - expectation.fulfill() + // Correct error thrown } catch { XCTFail("Unexpected error while validating nbf claim.") } - self.wait(for: [expectation], timeout: 0.5) } func testNotBeforeIsValidWithLeeway() { @@ -337,17 +333,15 @@ class ValidationTests: XCTestCase { func testIssuedAtIsInFutureWithoutLeeway() { var claims = ClaimSet() claims.issuedAt = Date().addingTimeInterval(1) - - let expectation = XCTestExpectation(description: "iat should be in the future.") + do { try claims.validateIssuedAt() XCTFail("InvalidToken.invalidIssuedAt error should have been thrown.") } catch InvalidToken.invalidIssuedAt { - expectation.fulfill() + // Correct error thrown } catch { XCTFail("Unexpected error while validating iat claim.") } - self.wait(for: [expectation], timeout: 0.5) } func testIssuedAtIsValidWithLeeway() { @@ -371,17 +365,15 @@ class IntegrationTests: XCTestCase { builder.notBefore = Date().addingTimeInterval(1) // Token starts being valid in one second builder.issuedAt = Date().addingTimeInterval(1) // Token is issued one second in the future } - - let expectation = XCTestExpectation(description: "Verification should fail.") + do { let _ = try JWT.decode(token, algorithm: .none, leeway: 0) XCTFail("InvalidToken error should have been thrown.") } catch is InvalidToken { - expectation.fulfill() + // Correct error thrown } catch { XCTFail("Unexpected error type while verifying token.") } - self.wait(for: [expectation], timeout: 0.5) } func testVerificationSuccessWithLeeway() { From fef37501a783709a7cc52fb8b4523a46de9844ae Mon Sep 17 00:00:00 2001 From: Jan Brinker Date: Mon, 11 Sep 2017 14:40:54 +0200 Subject: [PATCH 7/8] Added leeway parameter introduction to changelog. --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bd856eb..9588b8f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Enhancements - Allow passing additional headers when encoding a JWT. +- Allow passing leeway parameter for date checks when verifying a JWT. ## 2.1.0 From a6fa6d8df6b391868710f495684557426cd187db Mon Sep 17 00:00:00 2001 From: Jan Brinker Date: Mon, 11 Sep 2017 14:42:20 +0200 Subject: [PATCH 8/8] Added documentation for leeway parameter for decoding. --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 830f70d..9a6fae2 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,12 @@ try JWT.decode("eyJh...5w", algorithms: [ ]) ``` +You might also want to give your iat, exp and nbf checks some kind of leeway to account for skewed clocks. You can do this by passing a `leeway` parameter like this: + +```swift +try JWT.decode("eyJh...5w", algorithm: .hs256("secret".data(using: .utf8)!), leeway: 10) +``` + #### Supported claims The library supports validating the following claims: