diff --git a/.gitignore b/.gitignore index 9bce6af..345bfdb 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ -xcuserdata +xcuserdata/ +*.xccheckout +.build/ +Packages/ diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 761522b..0000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "Carthage/Checkouts/CryptoSwift"] - path = Carthage/Checkouts/CryptoSwift - url = https://github.com/krzyzanowskim/CryptoSwift.git diff --git a/.travis.yml b/.travis.yml index 52c505e..2c00dc4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,38 @@ -language: objective-c -osx_image: xcode7.2 +os: + - osx + - linux +osx_image: xcode9.4 +language: generic +sudo: required +dist: trusty + +env: + global: + - SWIFT_VERSION=4.0 + - SWIFT_VERSION=4.1.2 + matrix: + - SWIFTPM_TEST=true + - XCODE_TEST_SDK=macosx + - XCODE_BUILD_SDK=iphonesimulator + - XCODE_BUILD_SDK=appletvsimulator + - XCODE_BUILD_SDK=watchsimulator + +matrix: + exclude: + - os: linux + env: XCODE_TEST_SDK=macosx + - os: linux + env: XCODE_BUILD_SDK=iphonesimulator + - os: linux + env: XCODE_BUILD_SDK=appletvsimulator + - os: linux + env: XCODE_BUILD_SDK=watchsimulator + +install: + - eval "$(curl -sL https://swiftenv.fuller.li/install.sh)" + script: -- set -o pipefail -- git submodule update --init --recursive -- xcodebuild -project JWT.xcodeproj -scheme JWT test -sdk macosx | xcpretty -c -- pod lib lint --quick +- if [ -n "$SWIFTPM_TEST" ]; then swift test; fi +- if [ -n "$XCODE_BUILD_SDK" ] || [ -n "$XCODE_TEST_SDK" ]; then swift package generate-xcodeproj; fi +- if [ -n "$XCODE_BUILD_SDK" ]; then xcodebuild -project JWT.xcodeproj -scheme JWT-Package build -sdk $XCODE_BUILD_SDK; fi +- if [ -n "$XCODE_TEST_SDK" ]; then xcodebuild -project JWT.xcodeproj -scheme JWT-Package test -sdk $XCODE_TEST_SDK; fi diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..2b9686c --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,57 @@ +# JSON Web Token Changelog + +## 2.2.0 + +### Enhancements + +- On Apple platforms, JSONWebToken will use the system CommonCrypto where possible. +- Allow passing additional headers when encoding a JWT. +- Allow passing leeway parameter for date checks when verifying a JWT. + + +## 2.1.0 + +### Enhancements + +- Introduces a new `ClaimSet` structure. The structure can be returned from + `decode` providing you convenience accessors. `encode` will now accept a + `ClaimSet`. + + `ClaimSet` provides methods to manually validate individual claims. + + ```swift + try claims.validateAudience("example.com") + try claims.validateIssuer("fuller.li") + try claims.validateExpiary() + try claims.validateNotBefore() + try claims.validateIssuedAt() + ``` + + +## 2.0.2 + +### Enhancements + +- Adds support for Linux. + + +## 2.0.1 + +This release adds support for Swift 3.0. + +### Breaking + +- Algorithms now take `Data` instead of a `String`. This improves the API + allowing you to use keys that cannot be serialised as a String. + + You can easily convert a String to Data such as in the following example: + + ```swift + .hs256("secret".data(using: .utf8)!) + ``` + + +## 1.5.0 + +This release updates the dependency on CryptoSwift to ~> 0.4.0 which adds +support for Swift 2.2. diff --git a/Cartfile b/Cartfile deleted file mode 100644 index e3a2f44..0000000 --- a/Cartfile +++ /dev/null @@ -1 +0,0 @@ -github "krzyzanowskim/CryptoSwift" "0.2.2" diff --git a/Cartfile.resolved b/Cartfile.resolved deleted file mode 100644 index e3a2f44..0000000 --- a/Cartfile.resolved +++ /dev/null @@ -1 +0,0 @@ -github "krzyzanowskim/CryptoSwift" "0.2.2" diff --git a/Carthage/Checkouts/CryptoSwift b/Carthage/Checkouts/CryptoSwift deleted file mode 160000 index 14e726d..0000000 --- a/Carthage/Checkouts/CryptoSwift +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 14e726db47efd6cbda0ff99b73dae2023ec354db diff --git a/CommonCrypto/module.modulemap b/CommonCrypto/module.modulemap new file mode 100644 index 0000000..b70f7d6 --- /dev/null +++ b/CommonCrypto/module.modulemap @@ -0,0 +1,4 @@ +module CommonCrypto [system] { + header "shim.h" + export * +} diff --git a/CommonCrypto/shim.h b/CommonCrypto/shim.h new file mode 100644 index 0000000..c332624 --- /dev/null +++ b/CommonCrypto/shim.h @@ -0,0 +1 @@ +#include diff --git a/JSONWebToken.podspec b/JSONWebToken.podspec deleted file mode 100644 index bdf8a74..0000000 --- a/JSONWebToken.podspec +++ /dev/null @@ -1,16 +0,0 @@ -Pod::Spec.new do |spec| - spec.name = 'JSONWebToken' - spec.version = '1.4.2' - spec.summary = 'Swift library for JSON Web Tokens (JWT).' - spec.homepage = 'https://github.com/kylef/JSONWebToken.swift' - spec.license = { :type => 'BSD', :file => 'LICENSE' } - spec.author = { 'Kyle Fuller' => 'kyle@fuller.li' } - spec.source = { :git => 'https://github.com/kylef/JSONWebToken.swift.git', :tag => "#{spec.version}" } - spec.source_files = 'Sources/*.swift' - spec.ios.deployment_target = '8.0' - spec.osx.deployment_target = '10.9' - spec.tvos.deployment_target = '9.0' - spec.requires_arc = true - spec.dependency 'CryptoSwift', '0.2.2' - spec.module_name = 'JWT' -end diff --git a/JWT.xcodeproj/project.pbxproj b/JWT.xcodeproj/project.pbxproj deleted file mode 100644 index 71f1e3b..0000000 --- a/JWT.xcodeproj/project.pbxproj +++ /dev/null @@ -1,572 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 46; - objects = { - -/* Begin PBXBuildFile section */ - 279D63A21AD07FFF0024E2BC /* JWT.h in Headers */ = {isa = PBXBuildFile; fileRef = 279D63A11AD07FFF0024E2BC /* JWT.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 279D63A81AD07FFF0024E2BC /* JWT.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 279D639C1AD07FFF0024E2BC /* JWT.framework */; }; - 279D63AF1AD07FFF0024E2BC /* JWTTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 279D63AE1AD07FFF0024E2BC /* JWTTests.swift */; }; - 520A71171C469F010005C709 /* Base64.swift in Sources */ = {isa = PBXBuildFile; fileRef = 520A71131C469F010005C709 /* Base64.swift */; }; - 520A71181C469F010005C709 /* Claims.swift in Sources */ = {isa = PBXBuildFile; fileRef = 520A71141C469F010005C709 /* Claims.swift */; }; - 520A71191C469F010005C709 /* Decode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 520A71151C469F010005C709 /* Decode.swift */; }; - 520A711A1C469F010005C709 /* JWT.swift in Sources */ = {isa = PBXBuildFile; fileRef = 520A71161C469F010005C709 /* JWT.swift */; }; - 66725DB61C59208400FC32F4 /* CryptoSwift.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 66725DB11C59202E00FC32F4 /* CryptoSwift.framework */; }; -/* End PBXBuildFile section */ - -/* Begin PBXContainerItemProxy section */ - 279D63A91AD07FFF0024E2BC /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 279D63931AD07FFF0024E2BC /* Project object */; - proxyType = 1; - remoteGlobalIDString = 279D639B1AD07FFF0024E2BC; - remoteInfo = JWT; - }; - 66725DAA1C59202E00FC32F4 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 66725DA21C59202E00FC32F4 /* CryptoSwift.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = 754BE45519693E190098E6F3; - remoteInfo = "CryptoSwift iOS"; - }; - 66725DAC1C59202E00FC32F4 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 66725DA21C59202E00FC32F4 /* CryptoSwift.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = 5596BDBB1BC8F220007E38D5; - remoteInfo = "CryptoSwift watchOS"; - }; - 66725DAE1C59202E00FC32F4 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 66725DA21C59202E00FC32F4 /* CryptoSwift.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = 75D614BF1BD844F2001358B2; - remoteInfo = "CryptoSwift tvOS"; - }; - 66725DB01C59202E00FC32F4 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 66725DA21C59202E00FC32F4 /* CryptoSwift.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = 75DF77721BC8EB59006E9520; - remoteInfo = "CryptoSwift OSX"; - }; - 66725DB21C59202E00FC32F4 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 66725DA21C59202E00FC32F4 /* CryptoSwift.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = 754BE46019693E190098E6F3; - remoteInfo = CryptoSwiftTests; - }; - 66725DB41C59203B00FC32F4 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 66725DA21C59202E00FC32F4 /* CryptoSwift.xcodeproj */; - proxyType = 1; - remoteGlobalIDString = 75DF77461BC8EB59006E9520; - remoteInfo = "CryptoSwift OSX"; - }; -/* End PBXContainerItemProxy section */ - -/* Begin PBXFileReference section */ - 279D639C1AD07FFF0024E2BC /* JWT.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = JWT.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 279D63A01AD07FFF0024E2BC /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 279D63A11AD07FFF0024E2BC /* JWT.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = JWT.h; sourceTree = ""; }; - 279D63A71AD07FFF0024E2BC /* JWTTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = JWTTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 279D63AD1AD07FFF0024E2BC /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 279D63AE1AD07FFF0024E2BC /* JWTTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JWTTests.swift; sourceTree = ""; }; - 520A71131C469F010005C709 /* Base64.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Base64.swift; sourceTree = ""; }; - 520A71141C469F010005C709 /* Claims.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Claims.swift; sourceTree = ""; }; - 520A71151C469F010005C709 /* Decode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Decode.swift; sourceTree = ""; }; - 520A71161C469F010005C709 /* JWT.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JWT.swift; sourceTree = ""; }; - 520A711B1C469F440005C709 /* Package.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = ""; }; - 540942F3614C41E3827F2013 /* Pods_JWT.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_JWT.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 66725DA21C59202E00FC32F4 /* CryptoSwift.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = CryptoSwift.xcodeproj; path = Carthage/Checkouts/CryptoSwift/CryptoSwift.xcodeproj; sourceTree = ""; }; - CE8198B6E30BA6B8F8125FA7 /* Pods_JWTTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_JWTTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 279D63981AD07FFF0024E2BC /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 66725DB61C59208400FC32F4 /* CryptoSwift.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 279D63A41AD07FFF0024E2BC /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 279D63A81AD07FFF0024E2BC /* JWT.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 279D63921AD07FFF0024E2BC = { - isa = PBXGroup; - children = ( - 66725DA21C59202E00FC32F4 /* CryptoSwift.xcodeproj */, - 520A711B1C469F440005C709 /* Package.swift */, - 520A71121C469F010005C709 /* Sources */, - 279D639E1AD07FFF0024E2BC /* JWT */, - 279D63AB1AD07FFF0024E2BC /* JWTTests */, - 279D639D1AD07FFF0024E2BC /* Products */, - AC8AE547FDAF3DD80EB4DB2F /* Frameworks */, - ); - indentWidth = 2; - sourceTree = ""; - tabWidth = 2; - }; - 279D639D1AD07FFF0024E2BC /* Products */ = { - isa = PBXGroup; - children = ( - 279D639C1AD07FFF0024E2BC /* JWT.framework */, - 279D63A71AD07FFF0024E2BC /* JWTTests.xctest */, - ); - name = Products; - sourceTree = ""; - }; - 279D639E1AD07FFF0024E2BC /* JWT */ = { - isa = PBXGroup; - children = ( - 279D63A11AD07FFF0024E2BC /* JWT.h */, - 279D639F1AD07FFF0024E2BC /* Supporting Files */, - ); - path = JWT; - sourceTree = ""; - }; - 279D639F1AD07FFF0024E2BC /* Supporting Files */ = { - isa = PBXGroup; - children = ( - 279D63A01AD07FFF0024E2BC /* Info.plist */, - ); - name = "Supporting Files"; - sourceTree = ""; - }; - 279D63AB1AD07FFF0024E2BC /* JWTTests */ = { - isa = PBXGroup; - children = ( - 279D63AE1AD07FFF0024E2BC /* JWTTests.swift */, - 279D63AC1AD07FFF0024E2BC /* Supporting Files */, - ); - path = JWTTests; - sourceTree = ""; - }; - 279D63AC1AD07FFF0024E2BC /* Supporting Files */ = { - isa = PBXGroup; - children = ( - 279D63AD1AD07FFF0024E2BC /* Info.plist */, - ); - name = "Supporting Files"; - sourceTree = ""; - }; - 520A71121C469F010005C709 /* Sources */ = { - isa = PBXGroup; - children = ( - 520A71131C469F010005C709 /* Base64.swift */, - 520A71141C469F010005C709 /* Claims.swift */, - 520A71151C469F010005C709 /* Decode.swift */, - 520A71161C469F010005C709 /* JWT.swift */, - ); - path = Sources; - sourceTree = ""; - }; - 66725DA31C59202E00FC32F4 /* Products */ = { - isa = PBXGroup; - children = ( - 66725DAB1C59202E00FC32F4 /* CryptoSwift.framework */, - 66725DAD1C59202E00FC32F4 /* CryptoSwift.framework */, - 66725DAF1C59202E00FC32F4 /* CryptoSwift.framework */, - 66725DB11C59202E00FC32F4 /* CryptoSwift.framework */, - 66725DB31C59202E00FC32F4 /* CryptoSwiftTests.xctest */, - ); - name = Products; - sourceTree = ""; - }; - AC8AE547FDAF3DD80EB4DB2F /* Frameworks */ = { - isa = PBXGroup; - children = ( - 540942F3614C41E3827F2013 /* Pods_JWT.framework */, - CE8198B6E30BA6B8F8125FA7 /* Pods_JWTTests.framework */, - ); - name = Frameworks; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXHeadersBuildPhase section */ - 279D63991AD07FFF0024E2BC /* Headers */ = { - isa = PBXHeadersBuildPhase; - buildActionMask = 2147483647; - files = ( - 279D63A21AD07FFF0024E2BC /* JWT.h in Headers */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXHeadersBuildPhase section */ - -/* Begin PBXNativeTarget section */ - 279D639B1AD07FFF0024E2BC /* JWT */ = { - isa = PBXNativeTarget; - buildConfigurationList = 279D63B21AD07FFF0024E2BC /* Build configuration list for PBXNativeTarget "JWT" */; - buildPhases = ( - 279D63971AD07FFF0024E2BC /* Sources */, - 279D63981AD07FFF0024E2BC /* Frameworks */, - 279D63991AD07FFF0024E2BC /* Headers */, - 66725DA11C591D9800FC32F4 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - 66725DB51C59203B00FC32F4 /* PBXTargetDependency */, - ); - name = JWT; - productName = JWT; - productReference = 279D639C1AD07FFF0024E2BC /* JWT.framework */; - productType = "com.apple.product-type.framework"; - }; - 279D63A61AD07FFF0024E2BC /* JWTTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = 279D63B51AD07FFF0024E2BC /* Build configuration list for PBXNativeTarget "JWTTests" */; - buildPhases = ( - 279D63A31AD07FFF0024E2BC /* Sources */, - 279D63A41AD07FFF0024E2BC /* Frameworks */, - 279D63A51AD07FFF0024E2BC /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - 279D63AA1AD07FFF0024E2BC /* PBXTargetDependency */, - ); - name = JWTTests; - productName = JWTTests; - productReference = 279D63A71AD07FFF0024E2BC /* JWTTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 279D63931AD07FFF0024E2BC /* Project object */ = { - isa = PBXProject; - attributes = { - LastSwiftMigration = 0700; - LastSwiftUpdateCheck = 0700; - LastUpgradeCheck = 0700; - ORGANIZATIONNAME = Cocode; - TargetAttributes = { - 279D639B1AD07FFF0024E2BC = { - CreatedOnToolsVersion = 6.2; - }; - 279D63A61AD07FFF0024E2BC = { - CreatedOnToolsVersion = 6.2; - }; - }; - }; - buildConfigurationList = 279D63961AD07FFF0024E2BC /* Build configuration list for PBXProject "JWT" */; - compatibilityVersion = "Xcode 3.2"; - developmentRegion = English; - hasScannedForEncodings = 0; - knownRegions = ( - en, - ); - mainGroup = 279D63921AD07FFF0024E2BC; - productRefGroup = 279D639D1AD07FFF0024E2BC /* Products */; - projectDirPath = ""; - projectReferences = ( - { - ProductGroup = 66725DA31C59202E00FC32F4 /* Products */; - ProjectRef = 66725DA21C59202E00FC32F4 /* CryptoSwift.xcodeproj */; - }, - ); - projectRoot = ""; - targets = ( - 279D639B1AD07FFF0024E2BC /* JWT */, - 279D63A61AD07FFF0024E2BC /* JWTTests */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXReferenceProxy section */ - 66725DAB1C59202E00FC32F4 /* CryptoSwift.framework */ = { - isa = PBXReferenceProxy; - fileType = wrapper.framework; - path = CryptoSwift.framework; - remoteRef = 66725DAA1C59202E00FC32F4 /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; - 66725DAD1C59202E00FC32F4 /* CryptoSwift.framework */ = { - isa = PBXReferenceProxy; - fileType = wrapper.framework; - path = CryptoSwift.framework; - remoteRef = 66725DAC1C59202E00FC32F4 /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; - 66725DAF1C59202E00FC32F4 /* CryptoSwift.framework */ = { - isa = PBXReferenceProxy; - fileType = wrapper.framework; - path = CryptoSwift.framework; - remoteRef = 66725DAE1C59202E00FC32F4 /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; - 66725DB11C59202E00FC32F4 /* CryptoSwift.framework */ = { - isa = PBXReferenceProxy; - fileType = wrapper.framework; - path = CryptoSwift.framework; - remoteRef = 66725DB01C59202E00FC32F4 /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; - 66725DB31C59202E00FC32F4 /* CryptoSwiftTests.xctest */ = { - isa = PBXReferenceProxy; - fileType = wrapper.cfbundle; - path = CryptoSwiftTests.xctest; - remoteRef = 66725DB21C59202E00FC32F4 /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; -/* End PBXReferenceProxy section */ - -/* Begin PBXResourcesBuildPhase section */ - 279D63A51AD07FFF0024E2BC /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 66725DA11C591D9800FC32F4 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 279D63971AD07FFF0024E2BC /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 520A71181C469F010005C709 /* Claims.swift in Sources */, - 520A711A1C469F010005C709 /* JWT.swift in Sources */, - 520A71191C469F010005C709 /* Decode.swift in Sources */, - 520A71171C469F010005C709 /* Base64.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 279D63A31AD07FFF0024E2BC /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 279D63AF1AD07FFF0024E2BC /* JWTTests.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXTargetDependency section */ - 279D63AA1AD07FFF0024E2BC /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 279D639B1AD07FFF0024E2BC /* JWT */; - targetProxy = 279D63A91AD07FFF0024E2BC /* PBXContainerItemProxy */; - }; - 66725DB51C59203B00FC32F4 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - name = "CryptoSwift OSX"; - targetProxy = 66725DB41C59203B00FC32F4 /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - -/* Begin XCBuildConfiguration section */ - 279D63B01AD07FFF0024E2BC /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 1; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_DYNAMIC_NO_PIC = NO; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_SYMBOLS_PRIVATE_EXTERN = NO; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.9; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = macosx; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - VERSIONING_SYSTEM = "apple-generic"; - VERSION_INFO_PREFIX = ""; - }; - name = Debug; - }; - 279D63B11AD07FFF0024E2BC /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 1; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.9; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = macosx; - VERSIONING_SYSTEM = "apple-generic"; - VERSION_INFO_PREFIX = ""; - }; - name = Release; - }; - 279D63B31AD07FFF0024E2BC /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - CLANG_ENABLE_MODULES = YES; - COMBINE_HIDPI_IMAGES = YES; - DEFINES_MODULE = YES; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - FRAMEWORK_VERSION = A; - INFOPLIST_FILE = JWT/Info.plist; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = "org.cocode.$(PRODUCT_NAME:rfc1034identifier)"; - PRODUCT_NAME = "$(TARGET_NAME)"; - SKIP_INSTALL = YES; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - }; - name = Debug; - }; - 279D63B41AD07FFF0024E2BC /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - CLANG_ENABLE_MODULES = YES; - COMBINE_HIDPI_IMAGES = YES; - DEFINES_MODULE = YES; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - FRAMEWORK_VERSION = A; - INFOPLIST_FILE = JWT/Info.plist; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = "org.cocode.$(PRODUCT_NAME:rfc1034identifier)"; - PRODUCT_NAME = "$(TARGET_NAME)"; - SKIP_INSTALL = YES; - }; - name = Release; - }; - 279D63B61AD07FFF0024E2BC /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - COMBINE_HIDPI_IMAGES = YES; - FRAMEWORK_SEARCH_PATHS = ( - "$(DEVELOPER_FRAMEWORKS_DIR)", - "$(inherited)", - ); - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - INFOPLIST_FILE = JWTTests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = "org.cocode.$(PRODUCT_NAME:rfc1034identifier)"; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Debug; - }; - 279D63B71AD07FFF0024E2BC /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - COMBINE_HIDPI_IMAGES = YES; - FRAMEWORK_SEARCH_PATHS = ( - "$(DEVELOPER_FRAMEWORKS_DIR)", - "$(inherited)", - ); - INFOPLIST_FILE = JWTTests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = "org.cocode.$(PRODUCT_NAME:rfc1034identifier)"; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 279D63961AD07FFF0024E2BC /* Build configuration list for PBXProject "JWT" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 279D63B01AD07FFF0024E2BC /* Debug */, - 279D63B11AD07FFF0024E2BC /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 279D63B21AD07FFF0024E2BC /* Build configuration list for PBXNativeTarget "JWT" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 279D63B31AD07FFF0024E2BC /* Debug */, - 279D63B41AD07FFF0024E2BC /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 279D63B51AD07FFF0024E2BC /* Build configuration list for PBXNativeTarget "JWTTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 279D63B61AD07FFF0024E2BC /* Debug */, - 279D63B71AD07FFF0024E2BC /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = 279D63931AD07FFF0024E2BC /* Project object */; -} diff --git a/JWT.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/JWT.xcodeproj/project.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index e3cefe8..0000000 --- a/JWT.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/JWT.xcodeproj/xcshareddata/xcschemes/JWT.xcscheme b/JWT.xcodeproj/xcshareddata/xcschemes/JWT.xcscheme deleted file mode 100644 index 6669b4d..0000000 --- a/JWT.xcodeproj/xcshareddata/xcschemes/JWT.xcscheme +++ /dev/null @@ -1,113 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/JWT/Info.plist b/JWT/Info.plist deleted file mode 100644 index ba3fdf1..0000000 --- a/JWT/Info.plist +++ /dev/null @@ -1,28 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - FMWK - CFBundleShortVersionString - 1.0 - CFBundleSignature - ???? - CFBundleVersion - $(CURRENT_PROJECT_VERSION) - NSHumanReadableCopyright - Copyright © 2015 Cocode. All rights reserved. - NSPrincipalClass - - - diff --git a/JWT/JWT.h b/JWT/JWT.h deleted file mode 100644 index 1af30d5..0000000 --- a/JWT/JWT.h +++ /dev/null @@ -1,17 +0,0 @@ -// -// JWT.h -// JWT -// -// Created by Kyle Fuller on 04/04/2015. -// Copyright (c) 2015 Cocode. All rights reserved. -// - -#import - -//! Project version number for JWT. -FOUNDATION_EXPORT double JWTVersionNumber; - -//! Project version string for JWT. -FOUNDATION_EXPORT const unsigned char JWTVersionString[]; - -// In this header, you should import all the public headers of your framework using statements like #import diff --git a/JWTTests/JWTTests.swift b/JWTTests/JWTTests.swift deleted file mode 100644 index 9f8e4d4..0000000 --- a/JWTTests/JWTTests.swift +++ /dev/null @@ -1,291 +0,0 @@ -import XCTest -import JWT - -class JWTEncodeTests : XCTestCase { - func testEncodingJWT() { - let payload = ["name": "Kyle"] as Payload - let jwt = JWT.encode(payload, algorithm: .HS256("secret")) - let fixture = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiS3lsZSJ9.zxm7xcp1eZtZhp4t-nlw09ATQnnFKIiSN83uG8u6cAg" - XCTAssertEqual(jwt, fixture) - } - - func testEncodingWithBuilder() { - let algorithm = Algorithm.HS256("secret") - let jwt = JWT.encode(algorithm) { builder in - builder.issuer = "fuller.li" - } - - assertSuccess(try JWT.decode(jwt, algorithm: algorithm)) { payload in - XCTAssertEqual(payload as NSDictionary, ["iss": "fuller.li"]) - } - } -} - -class JWTPayloadBuilder : XCTestCase { - func testIssuer() { - JWT.encode(.None) { builder in - builder.issuer = "fuller.li" - XCTAssertEqual(builder.issuer, "fuller.li") - XCTAssertEqual(builder["iss"] as? String, "fuller.li") - } - } - - func testAudience() { - JWT.encode(.None) { builder in - builder.audience = "cocoapods" - XCTAssertEqual(builder.audience, "cocoapods") - XCTAssertEqual(builder["aud"] as? String, "cocoapods") - } - } - - func testExpiration() { - JWT.encode(.None) { builder in - let date = NSDate(timeIntervalSince1970: NSDate().timeIntervalSince1970) - builder.expiration = date - XCTAssertEqual(builder.expiration, date) - XCTAssertEqual(builder["exp"] as? NSTimeInterval, date.timeIntervalSince1970) - } - } - - func testNotBefore() { - JWT.encode(.None) { builder in - let date = NSDate(timeIntervalSince1970: NSDate().timeIntervalSince1970) - builder.notBefore = date - XCTAssertEqual(builder.notBefore, date) - XCTAssertEqual(builder["nbf"] as? NSTimeInterval, date.timeIntervalSince1970) - } - } - - func testIssuedAt() { - JWT.encode(.None) { builder in - let date = NSDate(timeIntervalSince1970: NSDate().timeIntervalSince1970) - builder.issuedAt = date - XCTAssertEqual(builder.issuedAt, date) - XCTAssertEqual(builder["iat"] as? NSTimeInterval, date.timeIntervalSince1970) - } - } - - func testCustomAttributes() { - JWT.encode(.None) { builder in - builder["user"] = "kyle" - XCTAssertEqual(builder["user"] as? String, "kyle") - } - } -} - -class JWTDecodeTests : XCTestCase { - func testDecodingValidJWT() { - let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiS3lsZSJ9.zxm7xcp1eZtZhp4t-nlw09ATQnnFKIiSN83uG8u6cAg" - - assertSuccess(try JWT.decode(jwt, algorithm: .HS256("secret"))) { payload in - XCTAssertEqual(payload as NSDictionary, ["name": "Kyle"]) - } - } - - func testFailsToDecodeInvalidStringWithoutThreeSegments() { - assertDecodeError(try decode("a.b", algorithm: .None), error: "Not enough segments") - } - - // MARK: Disable verify - - func testDisablingVerify() { - let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.2_8pWJfyPup0YwOXK7g9Dn0cF1E3pdn299t4hSeJy5w" - assertSuccess(try decode(jwt, algorithm: .None, verify:false, issuer:"fuller.li")) - } - - // MARK: Issuer claim - - func testSuccessfulIssuerValidation() { - let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJmdWxsZXIubGkifQ.d7B7PAQcz1E6oNhrlxmHxHXHgg39_k7X7wWeahl8kSQ" - assertSuccess(try decode(jwt, algorithm: .HS256("secret"), issuer:"fuller.li")) { payload in - XCTAssertEqual(payload as NSDictionary, ["iss": "fuller.li"]) - } - } - - func testIncorrectIssuerValidation() { - let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJmdWxsZXIubGkifQ.wOhJ9_6lx-3JGJPmJmtFCDI3kt7uMAMmhHIslti7ryI" - assertFailure(try decode(jwt, algorithm: .HS256("secret"), issuer:"querykit.org")) - } - - func testMissingIssuerValidation() { - let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.2_8pWJfyPup0YwOXK7g9Dn0cF1E3pdn299t4hSeJy5w" - assertFailure(try decode(jwt, algorithm: .HS256("secret"), issuer:"fuller.li")) - } - - // MARK: Expiration claim - - func testExpiredClaim() { - let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0MjgxODg0OTF9.cy6b2szsNkKnHFnz2GjTatGjoHBTs8vBKnPGZgpp91I" - assertFailure(try decode(jwt, algorithm: .HS256("secret"))) - } - - func testInvalidExpiaryClaim() { - let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOlsiMTQyODE4ODQ5MSJdfQ.OwF-wd3THjxrEGUhh6IdnNhxQZ7ydwJ3Z6J_dfl9MBs" - assertFailure(try decode(jwt, algorithm: .HS256("secret"))) - } - - func testUnexpiredClaim() { - // If this just started failing, hello 2024! - let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MjgxODg0OTF9.EW7k-8Mvnv0GpvOKJalFRLoCB3a3xGG3i7hAZZXNAz0" - assertSuccess(try decode(jwt, algorithm: .HS256("secret"))) { payload in - XCTAssertEqual(payload as NSDictionary, ["exp": 1728188491]) - } - } - - func testUnexpiredClaimString() { - // If this just started failing, hello 2024! - let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOiIxNzI4MTg4NDkxIn0.y4w7lNLrfRRPzuNUfM-ZvPkoOtrTU_d8ZVYasLdZGpk" - assertSuccess(try decode(jwt, algorithm: .HS256("secret"))) { payload in - XCTAssertEqual(payload as NSDictionary, ["exp": "1728188491"]) - } - } - - // MARK: Not before claim - - func testNotBeforeClaim() { - let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOjE0MjgxODk3MjB9.jFT0nXAJvEwyG6R7CMJlzNJb7FtZGv30QRZpYam5cvs" - assertSuccess(try decode(jwt, algorithm: .HS256("secret"))) { payload in - XCTAssertEqual(payload as NSDictionary, ["nbf": 1428189720]) - } - } - - func testNotBeforeClaimString() { - let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOiIxNDI4MTg5NzIwIn0.qZsj36irdmIAeXv6YazWDSFbpuxHtEh4Deof5YTpnVI" - assertSuccess(try decode(jwt, algorithm: .HS256("secret"))) { payload in - XCTAssertEqual(payload as NSDictionary, ["nbf": "1428189720"]) - } - } - - func testInvalidNotBeforeClaim() { - let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOlsxNDI4MTg5NzIwXX0.PUL1FQubzzJa4MNXe2D3d5t5cMaqFr3kYlzRUzly-C8" - assertDecodeError(try decode(jwt, algorithm: .HS256("secret")), error: "Not before claim (nbf) must be an integer") - } - - func testUnmetNotBeforeClaim() { - // If this just started failing, hello 2024! - let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOjE3MjgxODg0OTF9.Tzhu1tu-7BXcF5YEIFFE1Vmg4tEybUnaz58FR4PcblQ" - assertFailure(try decode(jwt, algorithm: .HS256("secret"))) - } - - // MARK: Issued at claim - - func testIssuedAtClaimInThePast() { - let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE0MjgxODk3MjB9.I_5qjRcCUZVQdABLwG82CSuu2relSdIyJOyvXWUAJh4" - assertSuccess(try decode(jwt, algorithm: .HS256("secret"))) { payload in - XCTAssertEqual(payload as NSDictionary, ["iat": 1428189720]) - } - } - - func testIssuedAtClaimInThePastString() { - let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOiIxNDI4MTg5NzIwIn0.M8veWtsY52oBwi7LRKzvNnzhjK0QBS8Su1r0atlns2k" - assertSuccess(try decode(jwt, algorithm: .HS256("secret"))) { payload in - XCTAssertEqual(payload as NSDictionary, ["iat": "1428189720"]) - } - } - - func testIssuedAtClaimInTheFuture() { - // If this just started failing, hello 2024! - let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE3MjgxODg0OTF9.owHiJyJmTcW1lBW5y_Rz3iBfSbcNiXlbZ2fY9qR7-aU" - assertFailure(try decode(jwt, algorithm: .HS256("secret"))) - } - - func testInvalidIssuedAtClaim() { - // If this just started failing, hello 2024! - let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOlsxNzI4MTg4NDkxXX0.ND7QMWtLkXDXH38OaXM3SQgLo3Z5TNgF_pcfWHV_alQ" - assertDecodeError(try decode(jwt, algorithm: .HS256("secret")), error: "Issued at claim (iat) must be an integer") - } - - // MARK: Audience claims - - func testAudiencesClaim() { - let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsibWF4aW5lIiwia2F0aWUiXX0.-PKvdNLCClrWG7CvesHP6PB0-vxu-_IZcsYhJxBy5JM" - assertSuccess(try decode(jwt, algorithm: .HS256("secret"), audience:"maxine")) { payload in - XCTAssertEqual(payload as NSDictionary, ["aud": ["maxine", "katie"]]) - } - } - - func testAudienceClaim() { - let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJreWxlIn0.dpgH4JOwueReaBoanLSxsGTc7AjKUvo7_M1sAfy_xVE" - assertSuccess(try decode(jwt, algorithm: .HS256("secret"), audience:"kyle")) { payload in - XCTAssertEqual(payload as NSDictionary, ["aud": "kyle"]) - } - } - - func testMismatchAudienceClaim() { - let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJreWxlIn0.VEB_n06pTSLlTXPFkc46ARADJ9HXNUBUPo3VhL9RDe4" // kyle - assertFailure(try decode(jwt, algorithm: .HS256("secret"), audience:"maxine")) - } - - func testMissingAudienceClaim() { - let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.2_8pWJfyPup0YwOXK7g9Dn0cF1E3pdn299t4hSeJy5w" - assertFailure(try decode(jwt, algorithm: .HS256("secret"), audience:"kyle")) - } - - // MARK: Signature verification - - func testNoneAlgorithm() { - let jwt = "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJ0ZXN0IjoiaW5nIn0." - assertSuccess(try decode(jwt, algorithm:.None)) { payload in - XCTAssertEqual(payload as NSDictionary, ["test": "ing"]) - } - } - - func testNoneFailsWithSecretAlgorithm() { - let jwt = "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJ0ZXN0IjoiaW5nIn0." - assertFailure(try decode(jwt, algorithm: .HS256("secret"))) - } - - func testMatchesAnyAlgorithm() { - let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.2_8pWJfyPup0YwOXK7g9Dn0cF1E3pdn299t4hSeJy5w." - assertFailure(try decode(jwt, algorithms: [.HS256("anothersecret"), .HS256("secret")])) - } - - func testHS384Algorithm() { - let jwt = "eyJhbGciOiJIUzM4NCIsInR5cCI6IkpXVCJ9.eyJzb21lIjoicGF5bG9hZCJ9.lddiriKLoo42qXduMhCTKZ5Lo3njXxOC92uXyvbLyYKzbq4CVVQOb3MpDwnI19u4" - assertSuccess(try decode(jwt, algorithm: .HS384("secret"))) { payload in - XCTAssertEqual(payload as NSDictionary, ["some": "payload"]) - } - } - - func testHS512Algorithm() { - let jwt = "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJzb21lIjoicGF5bG9hZCJ9.WTzLzFO079PduJiFIyzrOah54YaM8qoxH9fLMQoQhKtw3_fMGjImIOokijDkXVbyfBqhMo2GCNu4w9v7UXvnpA" - assertSuccess(try decode(jwt, algorithm: .HS512("secret"))) { payload in - XCTAssertEqual(payload as NSDictionary, ["some": "payload"]) - } - } -} - -// MARK: Helpers - -func assertSuccess(@autoclosure decoder:() throws -> Payload, closure:(Payload -> ())? = nil) { - do { - let payload = try decoder() - closure?(payload) - } catch { - XCTFail("Failed to decode while expecting success. \(error)") - } -} - -func assertFailure(@autoclosure decoder:() throws -> Payload, closure:(InvalidToken -> ())? = nil) { - do { - _ = try decoder() - XCTFail("Decoding succeeded, expected a failure.") - } catch let error as InvalidToken { - closure?(error) - } catch { - XCTFail("Unexpected error") - } -} - -func assertDecodeError(@autoclosure decoder:() throws -> Payload, error:String) { - assertFailure(try decoder()) { failure in - switch failure { - case .DecodeError(let decodeError): - if decodeError != error { - XCTFail("Incorrect decode error \(decodeError) != \(error)") - } - default: - XCTFail("Failure for the wrong reason \(failure)") - } - } -} diff --git a/Package.swift b/Package.swift index c0e4673..01b5e95 100644 --- a/Package.swift +++ b/Package.swift @@ -1,8 +1,37 @@ +// swift-tools-version:4.0 + import PackageDescription + +#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) +#if canImport(CommonCrypto) +let dependencies: [Package.Dependency] = [] +#else +let dependencies = [ + Package.Dependency.package(url: "https://github.com/kylef-archive/CommonCrypto.git", from: "1.0.0"), +] +#endif +let excludes = ["HMAC/HMACCryptoSwift.swift"] +let targetDependencies: [Target.Dependency] = [] +#else +let dependencies = [ + Package.Dependency.package(url: "https://github.com/krzyzanowskim/CryptoSwift.git", from: "0.10.0"), +] +let excludes = ["HMAC/HMACCommonCrypto.swift"] +let targetDependencies: [Target.Dependency] = ["CryptoSwift"] +#endif + + let package = Package( name: "JWT", - dependencies: [ - .Package(url: "https://github.com/krzyzanowskim/CryptoSwift", versions: Version(0,2,2).. String { - let data = input.base64EncodedDataWithOptions(NSDataBase64EncodingOptions(rawValue: 0)) - let string = NSString(data: data, encoding: NSUTF8StringEncoding) as! String - return string - .stringByReplacingOccurrencesOfString("+", withString: "-", options: NSStringCompareOptions(rawValue: 0), range: nil) - .stringByReplacingOccurrencesOfString("/", withString: "_", options: NSStringCompareOptions(rawValue: 0), range: nil) - .stringByReplacingOccurrencesOfString("=", withString: "", options: NSStringCompareOptions(rawValue: 0), range: nil) -} - -/// URI Safe base64 decode -func base64decode(input:String) -> NSData? { - let rem = input.characters.count % 4 - - var ending = "" - if rem > 0 { - let amount = 4 - rem - ending = String(count: amount, repeatedValue: Character("=")) - } - - let base64 = input.stringByReplacingOccurrencesOfString("-", withString: "+", options: NSStringCompareOptions(rawValue: 0), range: nil) - .stringByReplacingOccurrencesOfString("_", withString: "/", options: NSStringCompareOptions(rawValue: 0), range: nil) + ending - - return NSData(base64EncodedString: base64, options: NSDataBase64DecodingOptions(rawValue: 0)) -} diff --git a/Sources/Claims.swift b/Sources/Claims.swift deleted file mode 100644 index fe7141b..0000000 --- a/Sources/Claims.swift +++ /dev/null @@ -1,53 +0,0 @@ -import Foundation - -func validateClaims(payload:Payload, audience:String?, issuer:String?) -> InvalidToken? { - return validateIssuer(payload, issuer: issuer) ?? validateAudience(payload, audience: audience) ?? - validateDate(payload, key: "exp", comparison: .OrderedAscending, failure: .ExpiredSignature, decodeError: "Expiration time claim (exp) must be an integer") ?? - validateDate(payload, key: "nbf", comparison: .OrderedDescending, failure: .ImmatureSignature, decodeError: "Not before claim (nbf) must be an integer") ?? - validateDate(payload, key: "iat", comparison: .OrderedDescending, failure: .InvalidIssuedAt, decodeError: "Issued at claim (iat) must be an integer") -} - -func validateAudience(payload:Payload, audience:String?) -> InvalidToken? { - if let audience = audience { - if let aud = payload["aud"] as? [String] { - if !aud.contains(audience) { - return .InvalidAudience - } - } else if let aud = payload["aud"] as? String { - if aud != audience { - return .InvalidAudience - } - } else { - return .DecodeError("Invalid audience claim, must be a string or an array of strings") - } - } - - return nil -} - -func validateIssuer(payload:Payload, issuer:String?) -> InvalidToken? { - if let issuer = issuer { - if let iss = payload["iss"] as? String { - if iss != issuer { - return .InvalidIssuer - } - } else { - return .InvalidIssuer - } - } - - return nil -} - -func validateDate(payload:Payload, key:String, comparison:NSComparisonResult, failure:InvalidToken, decodeError:String) -> InvalidToken? { - if let timestamp = payload[key] as? NSTimeInterval ?? payload[key]?.doubleValue as NSTimeInterval? { - let date = NSDate(timeIntervalSince1970: timestamp) - if date.compare(NSDate()) == comparison { - return failure - } - } else if payload[key] != nil { - return .DecodeError(decodeError) - } - - return nil -} diff --git a/Sources/Decode.swift b/Sources/Decode.swift deleted file mode 100644 index c22a710..0000000 --- a/Sources/Decode.swift +++ /dev/null @@ -1,131 +0,0 @@ -import Foundation - - -/// Failure reasons from decoding a JWT -public enum InvalidToken : CustomStringConvertible, ErrorType { - /// Decoding the JWT itself failed - case DecodeError(String) - - /// The JWT uses an unsupported algorithm - case InvalidAlgorithm - - /// The issued claim has expired - case ExpiredSignature - - /// The issued claim is for the future - case ImmatureSignature - - /// The claim is for the future - case InvalidIssuedAt - - /// The audience of the claim doesn't match - case InvalidAudience - - /// The issuer claim failed to verify - case InvalidIssuer - - /// Returns a readable description of the error - public var description:String { - switch self { - case .DecodeError(let error): - return "Decode Error: \(error)" - case .InvalidIssuer: - return "Invalid Issuer" - case .ExpiredSignature: - return "Expired Signature" - case .ImmatureSignature: - return "The token is not yet valid (not before claim)" - case .InvalidIssuedAt: - return "Issued at claim (iat) is in the future" - case InvalidAudience: - return "Invalid Audience" - case InvalidAlgorithm: - return "Unsupported algorithm or incorrect key" - } - } -} - - -/// Decode a JWT -public func decode(jwt:String, algorithms:[Algorithm], verify:Bool = true, audience:String? = nil, issuer:String? = nil) throws -> Payload { - switch load(jwt) { - case let .Success(header, payload, signature, signatureInput): - if verify { - if let failure = validateClaims(payload, audience: audience, issuer: issuer) ?? verifySignature(algorithms, header: header, signingInput: signatureInput, signature: signature) { - throw failure - } - } - - return payload - case .Failure(let failure): - throw failure - } -} - -/// Decode a JWT -public func decode(jwt:String, algorithm:Algorithm, verify:Bool = true, audience:String? = nil, issuer:String? = nil) throws -> Payload { - return try decode(jwt, algorithms: [algorithm], verify: verify, audience: audience, issuer: issuer) -} - -// MARK: Parsing a JWT - -enum LoadResult { - case Success(header:Payload, payload:Payload, signature:NSData, signatureInput:String) - case Failure(InvalidToken) -} - -func load(jwt:String) -> LoadResult { - let segments = jwt.componentsSeparatedByString(".") - if segments.count != 3 { - return .Failure(.DecodeError("Not enough segments")) - } - - let headerSegment = segments[0] - let payloadSegment = segments[1] - let signatureSegment = segments[2] - let signatureInput = "\(headerSegment).\(payloadSegment)" - - let headerData = base64decode(headerSegment) - if headerData == nil { - return .Failure(.DecodeError("Header is not correctly encoded as base64")) - } - - let header = (try? NSJSONSerialization.JSONObjectWithData(headerData!, options: NSJSONReadingOptions(rawValue: 0))) as? Payload - if header == nil { - return .Failure(.DecodeError("Invalid header")) - } - - let payloadData = base64decode(payloadSegment) - if payloadData == nil { - return .Failure(.DecodeError("Payload is not correctly encoded as base64")) - } - - let payload = (try? NSJSONSerialization.JSONObjectWithData(payloadData!, options: NSJSONReadingOptions(rawValue: 0))) as? Payload - if payload == nil { - return .Failure(.DecodeError("Invalid payload")) - } - - let signature = base64decode(signatureSegment) - if signature == nil { - return .Failure(.DecodeError("Signature is not correctly encoded as base64")) - } - - return .Success(header:header!, payload:payload!, signature:signature!, signatureInput:signatureInput) -} - -// MARK: Signature Verification - -func verifySignature(algorithms:[Algorithm], header:Payload, signingInput:String, signature:NSData) -> InvalidToken? { - if let alg = header["alg"] as? String { - let matchingAlgorithms = algorithms.filter { algorithm in algorithm.description == alg } - let results = matchingAlgorithms.map { algorithm in algorithm.verify(signingInput, signature: signature) } - let successes = results.filter { $0 } - if successes.count > 0 { - return nil - } - - return .InvalidAlgorithm - } - - return .DecodeError("Missing Algorithm") -} diff --git a/Sources/JWA/HMAC/HMAC.swift b/Sources/JWA/HMAC/HMAC.swift new file mode 100644 index 0000000..9252a03 --- /dev/null +++ b/Sources/JWA/HMAC/HMAC.swift @@ -0,0 +1,36 @@ +import Foundation + + +final public class HMACAlgorithm: Algorithm { + public let key: Data + public let hash: Hash + + public enum Hash { + case sha256 + case sha384 + case sha512 + } + + public init(key: Data, hash: Hash) { + self.key = key + self.hash = hash + } + + public init?(key: String, hash: Hash) { + guard let key = key.data(using: .utf8) else { return nil } + + self.key = key + self.hash = hash + } + + public var name: String { + switch hash { + case .sha256: + return "HS256" + case .sha384: + return "HS384" + case .sha512: + return "HS512" + } + } +} diff --git a/Sources/JWA/HMAC/HMACCommonCrypto.swift b/Sources/JWA/HMAC/HMACCommonCrypto.swift new file mode 100644 index 0000000..6dffce0 --- /dev/null +++ b/Sources/JWA/HMAC/HMACCommonCrypto.swift @@ -0,0 +1,48 @@ +import Foundation +import CommonCrypto + + +extension HMACAlgorithm: SignAlgorithm, VerifyAlgorithm { + public func sign(_ message: Data) -> Data { + let context = UnsafeMutablePointer.allocate(capacity: 1) + defer { context.deallocate() } + + key.withUnsafeBytes() { (buffer: UnsafePointer) in + CCHmacInit(context, hash.commonCryptoAlgorithm, buffer, size_t(key.count)) + } + + message.withUnsafeBytes { (buffer: UnsafePointer) in + CCHmacUpdate(context, buffer, size_t(message.count)) + } + + var hmac = Array(repeating: 0, count: Int(hash.commonCryptoDigestLength)) + CCHmacFinal(context, &hmac) + + return Data(hmac) + } +} + + +extension HMACAlgorithm.Hash { + var commonCryptoAlgorithm: CCHmacAlgorithm { + switch self { + case .sha256: + return CCHmacAlgorithm(kCCHmacAlgSHA256) + case .sha384: + return CCHmacAlgorithm(kCCHmacAlgSHA384) + case .sha512: + return CCHmacAlgorithm(kCCHmacAlgSHA512) + } + } + + var commonCryptoDigestLength: Int32 { + switch self { + case .sha256: + return CC_SHA256_DIGEST_LENGTH + case .sha384: + return CC_SHA384_DIGEST_LENGTH + case .sha512: + return CC_SHA512_DIGEST_LENGTH + } + } +} diff --git a/Sources/JWA/HMAC/HMACCryptoSwift.swift b/Sources/JWA/HMAC/HMACCryptoSwift.swift new file mode 100644 index 0000000..6153049 --- /dev/null +++ b/Sources/JWA/HMAC/HMACCryptoSwift.swift @@ -0,0 +1,32 @@ +import Foundation +import CryptoSwift + + +extension HMACAlgorithm: SignAlgorithm, VerifyAlgorithm { + public func sign(_ message: Data) -> Data { + let mac = HMAC(key: key.bytes, variant: hash.cryptoSwiftVariant) + + let result: [UInt8] + do { + result = try mac.authenticate(message.bytes) + } catch { + result = [] + } + + return Data(bytes: result) + } +} + + +extension HMACAlgorithm.Hash { + var cryptoSwiftVariant: HMAC.Variant { + switch self { + case .sha256: + return .sha256 + case .sha384: + return .sha384 + case .sha512: + return .sha512 + } + } +} diff --git a/Sources/JWA/JWA.swift b/Sources/JWA/JWA.swift new file mode 100644 index 0000000..2e02f7d --- /dev/null +++ b/Sources/JWA/JWA.swift @@ -0,0 +1,32 @@ +import Foundation + + +/// Represents a JSON Web Algorithm (JWA) +/// https://tools.ietf.org/html/draft-ietf-jose-json-web-algorithms-40 +public protocol Algorithm: class { + var name: String { get } +} + + +// MARK: Signing + +/// Represents a JSON Web Algorithm (JWA) that is capable of signing +public protocol SignAlgorithm: Algorithm { + func sign(_ message: Data) -> Data +} + + +// MARK: Verifying + +/// Represents a JSON Web Algorithm (JWA) that is capable of verifying +public protocol VerifyAlgorithm: Algorithm { + func verify(_ message: Data, signature: Data) -> Bool +} + + +extension SignAlgorithm { + /// Verify a signature for a message using the algorithm + public func verify(_ message: Data, signature: Data) -> Bool { + return sign(message) == signature + } +} diff --git a/Sources/JWA/None.swift b/Sources/JWA/None.swift new file mode 100644 index 0000000..26a238d --- /dev/null +++ b/Sources/JWA/None.swift @@ -0,0 +1,15 @@ +import Foundation + + +/// No Algorithm, i-e, insecure +public final class NoneAlgorithm: Algorithm, SignAlgorithm, VerifyAlgorithm { + public var name: String { + return "none" + } + + public init() {} + + public func sign(_ message: Data) -> Data { + return Data() + } +} diff --git a/Sources/JWT.swift b/Sources/JWT.swift deleted file mode 100644 index bd6ba27..0000000 --- a/Sources/JWT.swift +++ /dev/null @@ -1,185 +0,0 @@ -import Foundation -import CryptoSwift - -public typealias Payload = [String:AnyObject] - -/// The supported Algorithms -public enum Algorithm : CustomStringConvertible { - /// No Algorithm, i-e, insecure - case None - - /// HMAC using SHA-256 hash algorithm - case HS256(String) - - /// HMAC using SHA-384 hash algorithm - case HS384(String) - - /// HMAC using SHA-512 hash algorithm - case HS512(String) - - static func algorithm(name:String, key:String?) -> Algorithm? { - if name == "none" { - if key != nil { - return nil // We don't allow nil when we configured a key - } - return Algorithm.None - } else if let key = key { - if name == "HS256" { - return .HS256(key) - } else if name == "HS384" { - return .HS384(key) - } else if name == "HS512" { - return .HS512(key) - } - } - - return nil - } - - public var description:String { - switch self { - case .None: - return "none" - case .HS256: - return "HS256" - case .HS384: - return "HS384" - case .HS512: - return "HS512" - } - } - - /// Sign a message using the algorithm - func sign(message:String) -> String { - func signHS(key:String, variant:CryptoSwift.HMAC.Variant) -> String { - let keyData = key.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false)! - let messageData = message.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false)! - let mac = Authenticator.HMAC(key: keyData.arrayOfBytes(), variant:variant) - let result: [UInt8] - do { - result = try mac.authenticate(messageData.arrayOfBytes()) - } catch { - result = [] - } - return base64encode(NSData.withBytes(result)) - } - - switch self { - case .None: - return "" - - case .HS256(let key): - return signHS(key, variant: .sha256) - - case .HS384(let key): - return signHS(key, variant: .sha384) - - case .HS512(let key): - return signHS(key, variant: .sha512) - } - } - - /// Verify a signature for a message using the algorithm - func verify(message:String, signature:NSData) -> Bool { - return sign(message) == base64encode(signature) - } -} - -// MARK: Encoding - -/*** Encode a payload - - parameter payload: The payload to sign - - parameter algorithm: The algorithm to sign the payload with - - returns: The JSON web token as a String -*/ -public func encode(payload:Payload, algorithm:Algorithm) -> String { - func encodeJSON(payload:Payload) -> String? { - if let data = try? NSJSONSerialization.dataWithJSONObject(payload, options: NSJSONWritingOptions(rawValue: 0)) { - return base64encode(data) - } - - return nil - } - - let header = encodeJSON(["typ": "JWT", "alg": algorithm.description])! - let payload = encodeJSON(payload)! - let signingInput = "\(header).\(payload)" - let signature = algorithm.sign(signingInput) - return "\(signingInput).\(signature)" -} - -public class PayloadBuilder { - var payload = Payload() - - public var issuer:String? { - get { - return payload["iss"] as? String - } - set { - payload["iss"] = newValue - } - } - - public var audience:String? { - get { - return payload["aud"] as? String - } - set { - payload["aud"] = newValue - } - } - - public var expiration:NSDate? { - get { - if let expiration = payload["exp"] as? NSTimeInterval { - return NSDate(timeIntervalSince1970: expiration) - } - - return nil - } - set { - payload["exp"] = newValue?.timeIntervalSince1970 - } - } - - public var notBefore:NSDate? { - get { - if let notBefore = payload["nbf"] as? NSTimeInterval { - return NSDate(timeIntervalSince1970: notBefore) - } - - return nil - } - set { - payload["nbf"] = newValue?.timeIntervalSince1970 - } - } - - public var issuedAt:NSDate? { - get { - if let issuedAt = payload["iat"] as? NSTimeInterval { - return NSDate(timeIntervalSince1970: issuedAt) - } - - return nil - } - set { - payload["iat"] = newValue?.timeIntervalSince1970 - } - } - - public subscript(key: String) -> AnyObject? { - get { - return payload[key] - } - set { - payload[key] = newValue - } - } -} - -public func encode(algorithm:Algorithm, closure:(PayloadBuilder -> ())) -> String { - let builder = PayloadBuilder() - closure(builder) - return encode(builder.payload, algorithm: algorithm) -} diff --git a/Sources/JWT/Algorithm.swift b/Sources/JWT/Algorithm.swift new file mode 100644 index 0000000..f1d1e91 --- /dev/null +++ b/Sources/JWT/Algorithm.swift @@ -0,0 +1,36 @@ +import Foundation +import JWA + + +/// Represents a JSON Web Algorithm (JWA) +/// https://tools.ietf.org/html/draft-ietf-jose-json-web-algorithms-40 +public enum Algorithm: CustomStringConvertible { + /// No Algorithm, i-e, insecure + case none + + /// HMAC using SHA-256 hash algorithm + case hs256(Data) + + /// HMAC using SHA-384 hash algorithm + case hs384(Data) + + /// HMAC using SHA-512 hash algorithm + case hs512(Data) + + var algorithm: SignAlgorithm { + switch self { + case .none: + return NoneAlgorithm() + case .hs256(let key): + return HMACAlgorithm(key: key, hash: .sha256) + case .hs384(let key): + return HMACAlgorithm(key: key, hash: .sha384) + case .hs512(let key): + return HMACAlgorithm(key: key, hash: .sha512) + } + } + + public var description: String { + return algorithm.name + } +} diff --git a/Sources/JWT/Base64.swift b/Sources/JWT/Base64.swift new file mode 100644 index 0000000..4a93f7f --- /dev/null +++ b/Sources/JWT/Base64.swift @@ -0,0 +1,27 @@ +import Foundation + +/// URI Safe base64 encode +func base64encode(_ input: Data) -> String { + let data = input.base64EncodedData() + let string = String(data: data, encoding: .utf8)! + return string + .replacingOccurrences(of: "+", with: "-") + .replacingOccurrences(of: "/", with: "_") + .replacingOccurrences(of: "=", with: "") +} + +/// URI Safe base64 decode +func base64decode(_ input: String) -> Data? { + let rem = input.count % 4 + + var ending = "" + if rem > 0 { + let amount = 4 - rem + ending = String(repeating: "=", count: amount) + } + + let base64 = input.replacingOccurrences(of: "-", with: "+") + .replacingOccurrences(of: "_", with: "/") + ending + + return Data(base64Encoded: base64) +} diff --git a/Sources/JWT/ClaimSet.swift b/Sources/JWT/ClaimSet.swift new file mode 100644 index 0000000..4b253be --- /dev/null +++ b/Sources/JWT/ClaimSet.swift @@ -0,0 +1,225 @@ +import Foundation + +func parseTimeInterval(_ value: Any?) -> Date? { + guard let value = value else { return nil } + + if let string = value as? String, let interval = TimeInterval(string) { + return Date(timeIntervalSince1970: interval) + } + + if let interval = value as? Int { + let double = Double(interval) + return Date(timeIntervalSince1970: double) + } + + if let interval = value as? TimeInterval { + return Date(timeIntervalSince1970: interval) + } + + return nil +} + +public struct ClaimSet { + var claims: [String: Any] + + public init(claims: [String: Any]? = nil) { + self.claims = claims ?? [:] + } + + public subscript(key: String) -> Any? { + get { + return claims[key] + } + + set { + if let newValue = newValue, let date = newValue as? Date { + claims[key] = date.timeIntervalSince1970 + } else { + claims[key] = newValue + } + } + } +} + + +// MARK: Accessors + +extension ClaimSet { + public var issuer: String? { + get { + return claims["iss"] as? String + } + + set { + claims["iss"] = newValue + } + } + + public var audience: String? { + get { + return claims["aud"] as? String + } + + set { + claims["aud"] = newValue + } + } + + public var expiration: Date? { + get { + return parseTimeInterval(claims["exp"]) + } + + set { + self["exp"] = newValue + } + } + + public var notBefore: Date? { + get { + return parseTimeInterval(claims["nbf"]) + } + + set { + self["nbf"] = newValue + } + } + + public var issuedAt: Date? { + get { + return parseTimeInterval(claims["iat"]) + } + + set { + self["iat"] = newValue + } + } +} + + +// MARK: Validations + +extension ClaimSet { + public func validate(audience: String? = nil, issuer: String? = nil, leeway: TimeInterval = 0) throws { + if let issuer = issuer { + try validateIssuer(issuer) + } + + if let audience = audience { + try validateAudience(audience) + } + + try validateExpiry(leeway: leeway) + try validateNotBefore(leeway: leeway) + try validateIssuedAt(leeway: leeway) + } + + public func validateAudience(_ audience: String) throws { + if let aud = self["aud"] as? [String] { + if !aud.contains(audience) { + throw InvalidToken.invalidAudience + } + } else if let aud = self["aud"] as? String { + if aud != audience { + throw InvalidToken.invalidAudience + } + } else { + throw InvalidToken.decodeError("Invalid audience claim, must be a string or an array of strings") + } + } + + public func validateIssuer(_ issuer: String) throws { + if let iss = self["iss"] as? String { + if iss != issuer { + throw InvalidToken.invalidIssuer + } + } else { + throw InvalidToken.invalidIssuer + } + } + + @available(*, deprecated, message: "This method's name is misspelled. Please instead use validateExpiry(leeway:).") + public func validateExpiary(leeway: TimeInterval = 0) throws { + try validateExpiry(leeway: leeway) + } + + public func validateExpiry(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 { + try validateDate(claims, key: "nbf", comparison: .orderedDescending, leeway: leeway, failure: .immatureSignature, decodeError: "Not before claim (nbf) 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") + } +} + +// MARK: Builder + +public class ClaimSetBuilder { + var claims = ClaimSet() + + public var issuer: String? { + get { + return claims.issuer + } + + set { + claims.issuer = newValue + } + } + + public var audience: String? { + get { + return claims.audience + } + + set { + claims.audience = newValue + } + } + + public var expiration: Date? { + get { + return claims.expiration + } + + set { + claims.expiration = newValue + } + } + + public var notBefore: Date? { + get { + return claims.notBefore + } + + set { + claims.notBefore = newValue + } + } + + public var issuedAt: Date? { + get { + return claims.issuedAt + } + + set { + claims.issuedAt = newValue + } + } + + public subscript(key: String) -> Any? { + get { + return claims[key] + } + + set { + claims[key] = newValue + } + } +} + +typealias PayloadBuilder = ClaimSetBuilder diff --git a/Sources/JWT/Claims.swift b/Sources/JWT/Claims.swift new file mode 100644 index 0000000..d1df6f4 --- /dev/null +++ b/Sources/JWT/Claims.swift @@ -0,0 +1,31 @@ +import Foundation + +func validateDate(_ payload: Payload, key: String, comparison: ComparisonResult, leeway: TimeInterval = 0, failure: InvalidToken, decodeError: String) throws { + if payload[key] == nil { + return + } + + guard let date = extractDate(payload: payload, key: key) else { + throw InvalidToken.decodeError(decodeError) + } + + if date.compare(Date().addingTimeInterval(leeway)) == comparison { + throw failure + } +} + +fileprivate func extractDate(payload: Payload, key: String) -> Date? { + if let timestamp = payload[key] as? TimeInterval { + return Date(timeIntervalSince1970: timestamp) + } + + if let timestamp = payload[key] as? Int { + return Date(timeIntervalSince1970: Double(timestamp)) + } + + if let timestampString = payload[key] as? String, let timestamp = Double(timestampString) { + return Date(timeIntervalSince1970: timestamp) + } + + return nil +} diff --git a/Sources/JWT/CompactJSONDecoder.swift b/Sources/JWT/CompactJSONDecoder.swift new file mode 100644 index 0000000..bb353d5 --- /dev/null +++ b/Sources/JWT/CompactJSONDecoder.swift @@ -0,0 +1,32 @@ +import Foundation + +class CompactJSONDecoder: JSONDecoder { + override func decode(_ type: T.Type, from data: Data) throws -> T where T : Decodable { + guard let string = String(data: data, encoding: .ascii) else { + throw InvalidToken.decodeError("data should contain only ASCII characters") + } + + return try decode(type, from: string) + } + + func decode(_ type: T.Type, from string: String) throws -> T where T : Decodable { + guard let decoded = base64decode(string) else { + throw InvalidToken.decodeError("data should be a valid base64 string") + } + + return try super.decode(type, from: decoded) + } + + func decode(from string: String) throws -> Payload { + guard let decoded = base64decode(string) else { + throw InvalidToken.decodeError("Payload is not correctly encoded as base64") + } + + let object = try JSONSerialization.jsonObject(with: decoded) + guard let payload = object as? Payload else { + throw InvalidToken.decodeError("Invalid payload") + } + + return payload + } +} diff --git a/Sources/JWT/CompactJSONEncoder.swift b/Sources/JWT/CompactJSONEncoder.swift new file mode 100644 index 0000000..2e4a10c --- /dev/null +++ b/Sources/JWT/CompactJSONEncoder.swift @@ -0,0 +1,20 @@ +import Foundation + + +class CompactJSONEncoder: JSONEncoder { + override func encode(_ value: T) throws -> Data { + return try encodeString(value).data(using: .ascii) ?? Data() + } + + func encodeString(_ value: T) throws -> String { + return base64encode(try super.encode(value)) + } + + func encodeString(_ value: [String: Any]) -> String? { + if let data = try? JSONSerialization.data(withJSONObject: value) { + return base64encode(data) + } + + return nil + } +} diff --git a/Sources/JWT/Decode.swift b/Sources/JWT/Decode.swift new file mode 100644 index 0000000..b6aa428 --- /dev/null +++ b/Sources/JWT/Decode.swift @@ -0,0 +1,104 @@ +import Foundation + + +/// Failure reasons from decoding a JWT +public enum InvalidToken: CustomStringConvertible, Error { + /// Decoding the JWT itself failed + case decodeError(String) + + /// The JWT uses an unsupported algorithm + case invalidAlgorithm + + /// The issued claim has expired + case expiredSignature + + /// The issued claim is for the future + case immatureSignature + + /// The claim is for the future + case invalidIssuedAt + + /// The audience of the claim doesn't match + case invalidAudience + + /// The issuer claim failed to verify + case invalidIssuer + + /// Returns a readable description of the error + public var description: String { + switch self { + case .decodeError(let error): + return "Decode Error: \(error)" + case .invalidIssuer: + return "Invalid Issuer" + case .expiredSignature: + return "Expired Signature" + case .immatureSignature: + return "The token is not yet valid (not before claim)" + case .invalidIssuedAt: + return "Issued at claim (iat) is in the future" + case .invalidAudience: + return "Invalid Audience" + case .invalidAlgorithm: + return "Unsupported algorithm or incorrect key" + } + } +} + + +/// Decode a JWT +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, leeway: leeway) + try verifySignature(algorithms, header: header, signingInput: signatureInput, signature: signature) + } + + return claims +} + +/// 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) +} + +// MARK: Parsing a JWT + +func load(_ jwt: String) throws -> (header: JOSEHeader, payload: ClaimSet, signature: Data, signatureInput: String) { + let segments = jwt.components(separatedBy: ".") + if segments.count != 3 { + throw InvalidToken.decodeError("Not enough segments") + } + + let headerSegment = segments[0] + let payloadSegment = segments[1] + let signatureSegment = segments[2] + let signatureInput = "\(headerSegment).\(payloadSegment)" + + let decoder = CompactJSONDecoder() + let header = try decoder.decode(JOSEHeader.self, from: headerSegment) + let payload = try decoder.decode(from: payloadSegment) + + guard let signature = base64decode(signatureSegment) else { + throw InvalidToken.decodeError("Signature is not correctly encoded as base64") + } + + return (header: header, payload: ClaimSet(claims: payload), signature: signature, signatureInput: signatureInput) +} + +// MARK: Signature Verification + +func verifySignature(_ algorithms: [Algorithm], header: JOSEHeader, signingInput: String, signature: Data) throws { + guard let alg = header.algorithm else { + throw InvalidToken.decodeError("Missing Algorithm") + } + + let verifiedAlgorithms = algorithms + .filter { algorithm in algorithm.description == alg } + .filter { algorithm in algorithm.algorithm.verify(signingInput.data(using: .utf8)!, signature: signature) } + + if verifiedAlgorithms.isEmpty { + throw InvalidToken.invalidAlgorithm + } +} diff --git a/Sources/JWT/Encode.swift b/Sources/JWT/Encode.swift new file mode 100644 index 0000000..22d1960 --- /dev/null +++ b/Sources/JWT/Encode.swift @@ -0,0 +1,40 @@ +import Foundation + + +/*** Encode a set of claims + - parameter claims: The set of claims + - parameter algorithm: The algorithm to sign the payload with + - returns: The JSON web token as a String + */ +public func encode(claims: ClaimSet, algorithm: Algorithm, headers: [String: String]? = nil) -> String { + let encoder = CompactJSONEncoder() + + var headers = headers ?? [:] + if !headers.keys.contains("typ") { + headers["typ"] = "JWT" + } + headers["alg"] = algorithm.description + + let header = try! encoder.encodeString(headers) + let payload = encoder.encodeString(claims.claims)! + let signingInput = "\(header).\(payload)" + let signature = base64encode(algorithm.algorithm.sign(signingInput.data(using: .utf8)!)) + return "\(signingInput).\(signature)" +} + +/*** Encode a dictionary of claims + - parameter claims: The dictionary of claims + - parameter algorithm: The algorithm to sign the payload with + - returns: The JSON web token as a String + */ +public func encode(claims: [String: Any], algorithm: Algorithm, headers: [String: String]? = nil) -> String { + return encode(claims: ClaimSet(claims: claims), algorithm: algorithm, headers: headers) +} + + +/// Encode a set of claims using the builder pattern +public func encode(_ algorithm: Algorithm, closure: ((ClaimSetBuilder) -> Void)) -> String { + let builder = ClaimSetBuilder() + closure(builder) + return encode(claims: builder.claims, algorithm: algorithm) +} diff --git a/Sources/JWT/JOSEHeader.swift b/Sources/JWT/JOSEHeader.swift new file mode 100644 index 0000000..3e673e7 --- /dev/null +++ b/Sources/JWT/JOSEHeader.swift @@ -0,0 +1,68 @@ +// +// JOSEHeader.swift +// JWT +// +// Created by Kyle Fuller on 02/12/2016. +// Copyright © 2016 Cocode. All rights reserved. +// + +import Foundation + + +struct JOSEHeader: Codable { + /// The "alg" (algorithm) identifies the cryptographic algorithm used to secure the JWS + var algorithm: String? + + /// jwu + // TODO + + /// jwk + // TODO + + /// The "kid" (key ID) is a hint indicating which key was used to secure the JWS + var keyID: String? + + /// x5u + // TODO + + /// x5c + // TODO + + /// x5t + // TODO + + /// x5t#S256 + // TODO + + /// The "typ" (type) is used by JWS applications to declare the media type [IANA.MediaTypes] of this complete JWS + var type: String? + + /// The "cty" (content type) is used by JWS application to declare the media type [IANA.MediaTypes] of the secured content (the payload). + var contentType: String? + + /// The "crit" (critical) indicates that extensions to JWS, JWE and/or [JWA] are being used that MUST be understood and processed + // TODO + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + algorithm = try container.decodeIfPresent(String.self, forKey: .algorithm) + keyID = try container.decodeIfPresent(String.self, forKey: .keyID) + type = try container.decodeIfPresent(String.self, forKey: .type) + contentType = try container.decodeIfPresent(String.self, forKey: .contentType) + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encodeIfPresent(algorithm, forKey: .algorithm) + try container.encodeIfPresent(keyID, forKey: .keyID) + try container.encodeIfPresent(type, forKey: .type) + try container.encodeIfPresent(contentType, forKey: .contentType) + } + + enum CodingKeys: String, CodingKey { + case algorithm = "alg" + case keyID = "kid" + case type = "typ" + case contentType = "cty" + } +} diff --git a/Sources/JWT/JWT.swift b/Sources/JWT/JWT.swift new file mode 100644 index 0000000..5e5569e --- /dev/null +++ b/Sources/JWT/JWT.swift @@ -0,0 +1,3 @@ +import Foundation + +public typealias Payload = [String: Any] diff --git a/Tests/JWATests/HMACTests.swift b/Tests/JWATests/HMACTests.swift new file mode 100644 index 0000000..2991630 --- /dev/null +++ b/Tests/JWATests/HMACTests.swift @@ -0,0 +1,63 @@ +import Foundation +import XCTest +import JWA + + +class HMACAlgorithmTests: XCTestCase { + let key = "secret".data(using: .utf8)! + let message = "message".data(using: .utf8)! + let sha256Signature = Data(base64Encoded: "i19IcCmVwVmMVz2x4hhmqbgl1KeU0WnXBgoDYFeWNgs=")! + let sha384Signature = Data(base64Encoded: "rQ706A2kJ7KjPURXyXK/dZ9Qdm+7ZlaQ1Qt8s43VIX21Wck+p8vuSOKuGltKr9NL")! + let sha512Signature = Data(base64Encoded: "G7pYfHMO7box9Tq7C2ylieCd5OiU7kVeYUCAc5l1mtqvoGnux8AWR7sXPcsX9V0ir0mhgHG3SMXC7df3qCnGMg==")! + + // MARK: Name + + func testSHA256Name() { + let algorithm = HMACAlgorithm(key: key, hash: .sha256) + XCTAssertEqual(algorithm.name, "HS256") + } + + func testSHA384Name() { + let algorithm = HMACAlgorithm(key: key, hash: .sha384) + XCTAssertEqual(algorithm.name, "HS384") + } + + func testSHA512Name() { + let algorithm = HMACAlgorithm(key: key, hash: .sha512) + XCTAssertEqual(algorithm.name, "HS512") + } + + // MARK: Signing + + func testSHA256Sign() { + let algorithm = HMACAlgorithm(key: key, hash: .sha256) + XCTAssertEqual(algorithm.sign(message), sha256Signature) + } + + func testSHA384Sign() { + let algorithm = HMACAlgorithm(key: key, hash: .sha384) + XCTAssertEqual(algorithm.sign(message), sha384Signature) + } + + func testSHA512Sign() { + let algorithm = HMACAlgorithm(key: key, hash: .sha512) + XCTAssertEqual(algorithm.sign(message), sha512Signature) + } + + // MARK: Verify + + func testSHA256Verify() { + let algorithm = HMACAlgorithm(key: key, hash: .sha256) + XCTAssertTrue(algorithm.verify(message, signature: sha256Signature)) + } + + func testSHA384Verify() { + let algorithm = HMACAlgorithm(key: key, hash: .sha384) + XCTAssertTrue(algorithm.verify(message, signature: sha384Signature)) + } + + func testSHA512Verify() { + let algorithm = HMACAlgorithm(key: key, hash: .sha512) + XCTAssertTrue(algorithm.verify(message, signature: sha512Signature)) + } +} diff --git a/Tests/JWATests/NoneTests.swift b/Tests/JWATests/NoneTests.swift new file mode 100644 index 0000000..c3987c5 --- /dev/null +++ b/Tests/JWATests/NoneTests.swift @@ -0,0 +1,23 @@ +import XCTest +import JWA + + +class NoneAlgorithmTests: XCTestCase { + let message = "message".data(using: .utf8)! + let signature = Data() + + func testName() { + let algorithm = NoneAlgorithm() + XCTAssertEqual(algorithm.name, "none") + } + + func testSign() { + let algorithm = NoneAlgorithm() + XCTAssertEqual(algorithm.sign(message), signature) + } + + func testVerify() { + let algorithm = NoneAlgorithm() + XCTAssertTrue(algorithm.verify(message, signature: signature)) + } +} diff --git a/Tests/JWTTests/ClaimSetTests.swift b/Tests/JWTTests/ClaimSetTests.swift new file mode 100644 index 0000000..aa8e8dd --- /dev/null +++ b/Tests/JWTTests/ClaimSetTests.swift @@ -0,0 +1,79 @@ +import XCTest +import JWT + +class ValidationTests: XCTestCase { + func testClaimJustExpiredWithoutLeeway() { + var claims = ClaimSet() + claims.expiration = Date().addingTimeInterval(-1) + + do { + try claims.validateExpiry() + XCTFail("InvalidToken.expiredSignature error should have been thrown.") + } catch InvalidToken.expiredSignature { + // Correct error thrown + } catch { + XCTFail("Unexpected error while validating exp claim.") + } + } + + func testClaimJustNotExpiredWithoutLeeway() { + var claims = ClaimSet() + claims.expiration = Date().addingTimeInterval(-1) + + do { + try claims.validateExpiry(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) + + do { + try claims.validateNotBefore() + XCTFail("InvalidToken.immatureSignature error should have been thrown.") + } catch InvalidToken.immatureSignature { + // Correct error thrown + } catch { + XCTFail("Unexpected error while validating nbf claim.") + } + } + + 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) + + do { + try claims.validateIssuedAt() + XCTFail("InvalidToken.invalidIssuedAt error should have been thrown.") + } catch InvalidToken.invalidIssuedAt { + // Correct error thrown + } catch { + XCTFail("Unexpected error while validating iat claim.") + } + } + + 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.") + } + } +} diff --git a/Tests/JWTTests/CompactJSONDecoderTests.swift b/Tests/JWTTests/CompactJSONDecoderTests.swift new file mode 100644 index 0000000..ed1bf26 --- /dev/null +++ b/Tests/JWTTests/CompactJSONDecoderTests.swift @@ -0,0 +1,16 @@ +import XCTest +@testable import JWT + +class CompactJSONDecodable: Decodable { + let key: String +} + +class CompactJSONDecoderTests: XCTestCase { + let decoder = CompactJSONDecoder() + + func testDecoder() throws { + let expected = "eyJrZXkiOiJ2YWx1ZSJ9".data(using: .ascii)! + let value = try decoder.decode(CompactJSONDecodable.self, from: expected) + XCTAssertEqual(value.key, "value") + } +} diff --git a/Tests/JWTTests/CompactJSONEncoderTests.swift b/Tests/JWTTests/CompactJSONEncoderTests.swift new file mode 100644 index 0000000..a0f59cd --- /dev/null +++ b/Tests/JWTTests/CompactJSONEncoderTests.swift @@ -0,0 +1,23 @@ +import XCTest +@testable import JWT + +class CompactJSONEncodable: Encodable { + let key: String + + init(key: String) { + self.key = key + } +} + +class CompactJSONEncoderTests: XCTestCase { + let encoder = CompactJSONEncoder() + + func testEncode() throws { + let value = CompactJSONEncodable(key: "value") + + let encoded = try encoder.encode(value) + + XCTAssertEqual(encoded, "eyJrZXkiOiJ2YWx1ZSJ9".data(using: .ascii)!) + } +} + diff --git a/JWTTests/Info.plist b/Tests/JWTTests/Info.plist similarity index 100% rename from JWTTests/Info.plist rename to Tests/JWTTests/Info.plist diff --git a/Tests/JWTTests/IntegrationTests.swift b/Tests/JWTTests/IntegrationTests.swift new file mode 100644 index 0000000..f15b9d9 --- /dev/null +++ b/Tests/JWTTests/IntegrationTests.swift @@ -0,0 +1,40 @@ +import XCTest +import JWT + +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 + } + + do { + let _ = try JWT.decode(token, algorithm: .none, leeway: 0) + XCTFail("InvalidToken error should have been thrown.") + } catch is InvalidToken { + // Correct error thrown + } catch { + XCTFail("Unexpected error type while verifying token.") + } + } + + 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.") + } + } +} diff --git a/Tests/JWTTests/JWTDecodeTests.swift b/Tests/JWTTests/JWTDecodeTests.swift new file mode 100644 index 0000000..cde1a10 --- /dev/null +++ b/Tests/JWTTests/JWTDecodeTests.swift @@ -0,0 +1,220 @@ +import Foundation +import XCTest +@testable import JWT + +class DecodeTests: XCTestCase { + func testDecodingValidJWT() throws { + let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiS3lsZSJ9.zxm7xcp1eZtZhp4t-nlw09ATQnnFKIiSN83uG8u6cAg" + + let claims = try JWT.decode(jwt, algorithm: .hs256("secret".data(using: .utf8)!)) + XCTAssertEqual(claims["name"] as? String, "Kyle") + } + + func testFailsToDecodeInvalidStringWithoutThreeSegments() { + XCTAssertThrowsError(try decode("a.b", algorithm: .none), "Not enough segments") + } + + // MARK: Disable verify + + func testDisablingVerify() throws { + let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.2_8pWJfyPup0YwOXK7g9Dn0cF1E3pdn299t4hSeJy5w" + _ = try decode(jwt, algorithm: .none, verify: false, issuer: "fuller.li") + } + + // MARK: Issuer claim + + func testSuccessfulIssuerValidation() throws { + let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJmdWxsZXIubGkifQ.d7B7PAQcz1E6oNhrlxmHxHXHgg39_k7X7wWeahl8kSQ" + + let claims = try JWT.decode(jwt, algorithm: .hs256("secret".data(using: .utf8)!)) + XCTAssertEqual(claims.issuer, "fuller.li") + } + + func testIncorrectIssuerValidation() { + let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJmdWxsZXIubGkifQ.wOhJ9_6lx-3JGJPmJmtFCDI3kt7uMAMmhHIslti7ryI" + XCTAssertThrowsError(try decode(jwt, algorithm: .hs256("secret".data(using: .utf8)!), issuer: "querykit.org")) + } + + func testMissingIssuerValidation() { + let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.2_8pWJfyPup0YwOXK7g9Dn0cF1E3pdn299t4hSeJy5w" + XCTAssertThrowsError(try decode(jwt, algorithm: .hs256("secret".data(using: .utf8)!), issuer: "fuller.li")) + } + + // MARK: Expiration claim + + func testExpiredClaim() { + let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0MjgxODg0OTF9.cy6b2szsNkKnHFnz2GjTatGjoHBTs8vBKnPGZgpp91I" + XCTAssertThrowsError(try decode(jwt, algorithm: .hs256("secret".data(using: .utf8)!))) + } + + func testInvalidExpiryClaim() { + let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOlsiMTQyODE4ODQ5MSJdfQ.OwF-wd3THjxrEGUhh6IdnNhxQZ7ydwJ3Z6J_dfl9MBs" + XCTAssertThrowsError(try decode(jwt, algorithm: .hs256("secret".data(using: .utf8)!))) + } + + func testUnexpiredClaim() throws { + // If this just started failing, hello 2024! + let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MjgxODg0OTF9.EW7k-8Mvnv0GpvOKJalFRLoCB3a3xGG3i7hAZZXNAz0" + + let claims = try JWT.decode(jwt, algorithm: .hs256("secret".data(using: .utf8)!)) + XCTAssertEqual(claims.expiration?.timeIntervalSince1970, 1728188491) + } + + func testUnexpiredClaimString() throws { + // If this just started failing, hello 2024! + let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOiIxNzI4MTg4NDkxIn0.y4w7lNLrfRRPzuNUfM-ZvPkoOtrTU_d8ZVYasLdZGpk" + + let claims = try JWT.decode(jwt, algorithm: .hs256("secret".data(using: .utf8)!)) + XCTAssertEqual(claims.expiration?.timeIntervalSince1970, 1728188491) + } + + // MARK: Not before claim + + func testNotBeforeClaim() throws { + let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOjE0MjgxODk3MjB9.jFT0nXAJvEwyG6R7CMJlzNJb7FtZGv30QRZpYam5cvs" + + let claims = try JWT.decode(jwt, algorithm: .hs256("secret".data(using: .utf8)!)) + XCTAssertEqual(claims.notBefore?.timeIntervalSince1970, 1428189720) + } + + func testNotBeforeClaimString() throws { + let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOiIxNDI4MTg5NzIwIn0.qZsj36irdmIAeXv6YazWDSFbpuxHtEh4Deof5YTpnVI" + + let claims = try JWT.decode(jwt, algorithm: .hs256("secret".data(using: .utf8)!)) + XCTAssertEqual(claims.notBefore?.timeIntervalSince1970, 1428189720) + } + + func testInvalidNotBeforeClaim() { + let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOlsxNDI4MTg5NzIwXX0.PUL1FQubzzJa4MNXe2D3d5t5cMaqFr3kYlzRUzly-C8" + assertDecodeError(try decode(jwt, algorithm: .hs256("secret".data(using: .utf8)!)), error: "Not before claim (nbf) must be an integer") + } + + func testUnmetNotBeforeClaim() { + // If this just started failing, hello 2024! + let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOjE3MjgxODg0OTF9.Tzhu1tu-7BXcF5YEIFFE1Vmg4tEybUnaz58FR4PcblQ" + XCTAssertThrowsError(try decode(jwt, algorithm: .hs256("secret".data(using: .utf8)!))) + } + + // MARK: Issued at claim + + func testIssuedAtClaimInThePast() throws { + let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE0MjgxODk3MjB9.I_5qjRcCUZVQdABLwG82CSuu2relSdIyJOyvXWUAJh4" + + let claims = try JWT.decode(jwt, algorithm: .hs256("secret".data(using: .utf8)!)) + XCTAssertEqual(claims.issuedAt?.timeIntervalSince1970, 1428189720) + } + + func testIssuedAtClaimInThePastString() throws { + let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOiIxNDI4MTg5NzIwIn0.M8veWtsY52oBwi7LRKzvNnzhjK0QBS8Su1r0atlns2k" + + let claims = try JWT.decode(jwt, algorithm: .hs256("secret".data(using: .utf8)!)) + XCTAssertEqual(claims.issuedAt?.timeIntervalSince1970, 1428189720) + } + + func testIssuedAtClaimInTheFuture() { + // If this just started failing, hello 2024! + let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE3MjgxODg0OTF9.owHiJyJmTcW1lBW5y_Rz3iBfSbcNiXlbZ2fY9qR7-aU" + XCTAssertThrowsError(try decode(jwt, algorithm: .hs256("secret".data(using: .utf8)!))) + } + + func testInvalidIssuedAtClaim() { + // If this just started failing, hello 2024! + let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOlsxNzI4MTg4NDkxXX0.ND7QMWtLkXDXH38OaXM3SQgLo3Z5TNgF_pcfWHV_alQ" + assertDecodeError(try decode(jwt, algorithm: .hs256("secret".data(using: .utf8)!)), error: "Issued at claim (iat) must be an integer") + } + + // MARK: Audience claims + + func testAudiencesClaim() { + let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsibWF4aW5lIiwia2F0aWUiXX0.-PKvdNLCClrWG7CvesHP6PB0-vxu-_IZcsYhJxBy5JM" + assertSuccess(try decode(jwt, algorithm: .hs256("secret".data(using: .utf8)!), audience: "maxine")) { payload in + XCTAssertEqual(payload.count, 1) + XCTAssertEqual(payload["aud"] as! [String], ["maxine", "katie"]) + } + } + + func testAudienceClaim() { + let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJreWxlIn0.dpgH4JOwueReaBoanLSxsGTc7AjKUvo7_M1sAfy_xVE" + assertSuccess(try decode(jwt, algorithm: .hs256("secret".data(using: .utf8)!), audience: "kyle")) { payload in + XCTAssertEqual(payload as! [String: String], ["aud": "kyle"]) + } + } + + func testMismatchAudienceClaim() { + let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJreWxlIn0.VEB_n06pTSLlTXPFkc46ARADJ9HXNUBUPo3VhL9RDe4" // kyle + XCTAssertThrowsError(try decode(jwt, algorithm: .hs256("secret".data(using: .utf8)!), audience: "maxine")) + } + + func testMissingAudienceClaim() { + let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.2_8pWJfyPup0YwOXK7g9Dn0cF1E3pdn299t4hSeJy5w" + XCTAssertThrowsError(try decode(jwt, algorithm: .hs256("secret".data(using: .utf8)!), audience: "kyle")) + } + + // MARK: Signature verification + + func testNoneAlgorithm() { + let jwt = "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJ0ZXN0IjoiaW5nIn0." + assertSuccess(try decode(jwt, algorithm: .none)) { payload in + XCTAssertEqual(payload as! [String: String], ["test": "ing"]) + } + } + + func testNoneFailsWithSecretAlgorithm() { + let jwt = "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJ0ZXN0IjoiaW5nIn0." + XCTAssertThrowsError(try decode(jwt, algorithm: .hs256("secret".data(using: .utf8)!))) + } + + func testMatchesAnyAlgorithm() { + let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.2_8pWJfyPup0YwOXK7g9Dn0cF1E3pdn299t4hSeJy5w." + assertFailure(try decode(jwt, algorithms: [.hs256("anothersecret".data(using: .utf8)!), .hs256("secret".data(using: .utf8)!)])) + } + + func testHS384Algorithm() { + let jwt = "eyJhbGciOiJIUzM4NCIsInR5cCI6IkpXVCJ9.eyJzb21lIjoicGF5bG9hZCJ9.lddiriKLoo42qXduMhCTKZ5Lo3njXxOC92uXyvbLyYKzbq4CVVQOb3MpDwnI19u4" + assertSuccess(try decode(jwt, algorithm: .hs384("secret".data(using: .utf8)!))) { payload in + XCTAssertEqual(payload as! [String: String], ["some": "payload"]) + } + } + + func testHS512Algorithm() { + let jwt = "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJzb21lIjoicGF5bG9hZCJ9.WTzLzFO079PduJiFIyzrOah54YaM8qoxH9fLMQoQhKtw3_fMGjImIOokijDkXVbyfBqhMo2GCNu4w9v7UXvnpA" + assertSuccess(try decode(jwt, algorithm: .hs512("secret".data(using: .utf8)!))) { claims in + XCTAssertEqual(claims as! [String: String], ["some": "payload"]) + } + } +} + +// MARK: Helpers + +func assertSuccess(_ decoder: @autoclosure () throws -> ClaimSet, closure: (([String: Any]) -> Void)? = nil) { + do { + let claims = try decoder() + closure?(claims.claims as [String: Any]) + } catch { + XCTFail("Failed to decode while expecting success. \(error)") + } +} + +func assertFailure(_ decoder: @autoclosure () throws -> ClaimSet, closure: ((InvalidToken) -> Void)? = nil) { + do { + _ = try decoder() + XCTFail("Decoding succeeded, expected a failure.") + } catch let error as InvalidToken { + closure?(error) + } catch { + XCTFail("Unexpected error") + } +} + +func assertDecodeError(_ decoder: @autoclosure () throws -> ClaimSet, error: String) { + assertFailure(try decoder()) { failure in + switch failure { + case .decodeError(let decodeError): + if decodeError != error { + XCTFail("Incorrect decode error \(decodeError) != \(error)") + } + default: + XCTFail("Failure for the wrong reason \(failure)") + } + } +} diff --git a/Tests/JWTTests/JWTEncodeTests.swift b/Tests/JWTTests/JWTEncodeTests.swift new file mode 100644 index 0000000..9ff7f14 --- /dev/null +++ b/Tests/JWTTests/JWTEncodeTests.swift @@ -0,0 +1,58 @@ +import XCTest +import JWT + + +class JWTEncodeTests: XCTestCase { + func testEncodingJWT() { + let payload = ["name": "Kyle"] as Payload + let jwt = JWT.encode(claims: payload, algorithm: .hs256("secret".data(using: .utf8)!)) + + let expected = [ + // { "alg": "HS256", "typ": "JWT" } + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiS3lsZSJ9.zxm7xcp1eZtZhp4t-nlw09ATQnnFKIiSN83uG8u6cAg", + + // { "typ": "JWT", "alg": "HS256" } + "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoiS3lsZSJ9.4tCpoxfyfjbUyLjm9_zu-r52Vxn6bFq9kp6Rt9xMs4A" + ] + + XCTAssertTrue(expected.contains(jwt)) + } + + func testEncodingWithBuilder() { + let algorithm = Algorithm.hs256("secret".data(using: .utf8)!) + let jwt = JWT.encode(algorithm) { builder in + builder.issuer = "fuller.li" + } + + let expected = [ + // { "alg": "HS256", "typ": "JWT" } + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJmdWxsZXIubGkifQ.d7B7PAQcz1E6oNhrlxmHxHXHgg39_k7X7wWeahl8kSQ", + // { "typ": "JWT", "alg": "HS256" } + "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJmdWxsZXIubGkifQ.x5Fdll-kZBImOPtpT1fZH_8hDW01Ax3pbZx_EiljoLk" + ] + + XCTAssertTrue(expected.contains(jwt)) + } + + func testEncodingClaimsWithHeaders() { + let algorithm = Algorithm.hs256("secret".data(using: .utf8)!) + let jwt = JWT.encode(claims: ClaimSet(), algorithm: algorithm, headers: ["kid": "x"]) + + let expected = [ + // { "alg": "HS256", "typ": "JWT", "kid": "x" } + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IngifQ.e30.ddEotxYYMMdat5HPgYFQnkHRdPXsxPG71ooyhIUoqGA", + // { "alg": "HS256", "kid": "x", "typ": "JWT" } + "eyJhbGciOiJIUzI1NiIsImtpZCI6IngiLCJ0eXAiOiJKV1QifQ.e30.xiT6fWe5dWGeuq8zFb0je_14Maa_9mHbVPSyJhUIJ54", + // { "typ": "JWT", "alg": "HS256", "kid": "x" } + "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImtpZCI6IngifQ.e30.5t6a61tpSXFo5QBHYCnKAz2mTHrW9kaQ9n_b7e-jWw0", + // { "typ": "JWT", "kid": "x", "alg": "HS256" } + "eyJ0eXAiOiJKV1QiLCJraWQiOiJ4IiwiYWxnIjoiSFMyNTYifQ.e30.DG5nmV2CVH6mV_iEm0xXZvL0DUJ22ek2xy6fNi_pGLc", + // { "kid": "x", "typ": "JWT", "alg": "HS256" } + "eyJraWQiOiJ4IiwidHlwIjoiSldUIiwiYWxnIjoiSFMyNTYifQ.e30.h5ZvlqECBIvu9uocR5_5uF3wnhga8vTruvXpzaHpRdA", + // { "kid": "x", "alg": "HS256", "typ": "JWT" } + "eyJraWQiOiJ4IiwiYWxnIjoiSFMyNTYiLCJ0eXAiOiJKV1QifQ.e30.5KqN7N5a7Cfbe2eKN41FJIfgMjcdSZ7Nt16xqlyOeMo" + ] + + XCTAssertTrue(expected.contains(jwt)) + } +} diff --git a/Tests/JWTTests/PayloadTests.swift b/Tests/JWTTests/PayloadTests.swift new file mode 100644 index 0000000..150be11 --- /dev/null +++ b/Tests/JWTTests/PayloadTests.swift @@ -0,0 +1,54 @@ +import XCTest +import JWT + +class PayloadTests: XCTestCase { + func testIssuer() { + _ = JWT.encode(.none) { builder in + builder.issuer = "fuller.li" + XCTAssertEqual(builder.issuer, "fuller.li") + XCTAssertEqual(builder["iss"] as? String, "fuller.li") + } + } + + func testAudience() { + _ = JWT.encode(.none) { builder in + builder.audience = "cocoapods" + XCTAssertEqual(builder.audience, "cocoapods") + XCTAssertEqual(builder["aud"] as? String, "cocoapods") + } + } + + func testExpiration() { + _ = JWT.encode(.none) { builder in + let date = Date(timeIntervalSince1970: Date().timeIntervalSince1970) + builder.expiration = date + XCTAssertEqual(builder.expiration, date) + XCTAssertEqual(builder["exp"] as? TimeInterval, date.timeIntervalSince1970) + } + } + + func testNotBefore() { + _ = JWT.encode(.none) { builder in + let date = Date(timeIntervalSince1970: Date().timeIntervalSince1970) + builder.notBefore = date + XCTAssertEqual(builder.notBefore, date) + XCTAssertEqual(builder["nbf"] as? TimeInterval, date.timeIntervalSince1970) + } + } + + func testIssuedAt() { + _ = JWT.encode(.none) { builder in + let date = Date(timeIntervalSince1970: Date().timeIntervalSince1970) + builder.issuedAt = date + XCTAssertEqual(builder.issuedAt, date) + XCTAssertEqual(builder["iat"] as? TimeInterval, date.timeIntervalSince1970) + } + } + + func testCustomAttributes() { + _ = JWT.encode(.none) { builder in + builder["user"] = "kyle" + XCTAssertEqual(builder["user"] as? String, "kyle") + } + } +} diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift new file mode 100644 index 0000000..492555b --- /dev/null +++ b/Tests/LinuxMain.swift @@ -0,0 +1,136 @@ +import XCTest +@testable import JWATests +@testable import JWTTests + +extension HMACAlgorithmTests { + static var allTests: [(String, (HMACAlgorithmTests) -> () throws -> Void)] { + return [ + ("testSHA256Name", testSHA256Name), + ("testSHA384Name", testSHA384Name), + ("testSHA512Name", testSHA512Name), + ("testSHA256Sign", testSHA256Sign), + ("testSHA384Sign", testSHA384Sign), + ("testSHA512Sign", testSHA512Sign), + ("testSHA256Verify", testSHA256Verify), + ("testSHA384Verify", testSHA384Verify), + ("testSHA512Verify", testSHA512Verify) + ] + } +} + +extension NoneAlgorithmTests { + static var allTests: [(String, (NoneAlgorithmTests) -> () throws -> Void)] { + return [ + ("testName", testName), + ("testSign", testSign), + ("testVerify", testVerify) + ] + } +} + +extension CompactJSONDecoderTests { + static var allTests: [(String, (CompactJSONDecoderTests) -> () throws -> Void)] { + return [ + ("testDecoder", testDecoder) + ] + } +} + +extension CompactJSONEncoderTests { + static var allTests: [(String, (CompactJSONEncoderTests) -> () throws -> Void)] { + return [ + ("testEncode", testEncode) + ] + } +} + +extension DecodeTests { + static var allTests: [(String, (DecodeTests) -> () throws -> Void)] { + return [ + ("testDecodingValidJWT", testDecodingValidJWT), + ("testFailsToDecodeInvalidStringWithoutThreeSegments", testFailsToDecodeInvalidStringWithoutThreeSegments), + ("testDisablingVerify", testDisablingVerify), + ("testSuccessfulIssuerValidation", testSuccessfulIssuerValidation), + ("testIncorrectIssuerValidation", testIncorrectIssuerValidation), + ("testMissingIssuerValidation", testMissingIssuerValidation), + ("testExpiredClaim", testExpiredClaim), + ("testInvalidExpiryClaim", testInvalidExpiryClaim), + ("testUnexpiredClaim", testUnexpiredClaim), + ("testUnexpiredClaimString", testUnexpiredClaimString), + ("testNotBeforeClaim", testNotBeforeClaim), + ("testNotBeforeClaimString", testNotBeforeClaimString), + ("testInvalidNotBeforeClaim", testInvalidNotBeforeClaim), + ("testUnmetNotBeforeClaim", testUnmetNotBeforeClaim), + ("testIssuedAtClaimInThePast", testIssuedAtClaimInThePast), + ("testIssuedAtClaimInThePastString", testIssuedAtClaimInThePastString), + ("testIssuedAtClaimInTheFuture", testIssuedAtClaimInTheFuture), + ("testInvalidIssuedAtClaim", testInvalidIssuedAtClaim), + ("testAudiencesClaim", testAudiencesClaim), + ("testAudienceClaim", testAudienceClaim), + ("testMismatchAudienceClaim", testMismatchAudienceClaim), + ("testMissingAudienceClaim", testMissingAudienceClaim), + ("testNoneAlgorithm", testNoneAlgorithm), + ("testNoneFailsWithSecretAlgorithm", testNoneFailsWithSecretAlgorithm), + ("testMatchesAnyAlgorithm", testMatchesAnyAlgorithm), + ("testHS384Algorithm", testHS384Algorithm), + ("testHS512Algorithm", testHS512Algorithm) + ] + } +} + +extension IntegrationTests { + static var allTests: [(String, (IntegrationTests) -> () throws -> Void)] { + return [ + ("testVerificationFailureWithoutLeeway", testVerificationFailureWithoutLeeway), + ("testVerificationSuccessWithLeeway", testVerificationSuccessWithLeeway) + ] + } +} + +extension JWTEncodeTests { + static var allTests: [(String, (JWTEncodeTests) -> () throws -> Void)] { + return [ + ("testEncodingJWT", testEncodingJWT), + ("testEncodingWithBuilder", testEncodingWithBuilder), + ("testEncodingClaimsWithHeaders", testEncodingClaimsWithHeaders) + ] + } +} + +extension PayloadTests { + static var allTests: [(String, (PayloadTests) -> () throws -> Void)] { + return [ + ("testIssuer", testIssuer), + ("testAudience", testAudience), + ("testExpiration", testExpiration), + ("testNotBefore", testNotBefore), + ("testIssuedAt", testIssuedAt), + ("testCustomAttributes", testCustomAttributes) + ] + } +} + +extension ValidationTests { + static var allTests: [(String, (ValidationTests) -> () throws -> Void)] { + return [ + ("testClaimJustExpiredWithoutLeeway", testClaimJustExpiredWithoutLeeway), + ("testClaimJustNotExpiredWithoutLeeway", testClaimJustNotExpiredWithoutLeeway), + ("testNotBeforeIsImmatureSignatureWithoutLeeway", testNotBeforeIsImmatureSignatureWithoutLeeway), + ("testNotBeforeIsValidWithLeeway", testNotBeforeIsValidWithLeeway), + ("testIssuedAtIsInFutureWithoutLeeway", testIssuedAtIsInFutureWithoutLeeway), + ("testIssuedAtIsValidWithLeeway", testIssuedAtIsValidWithLeeway) + ] + } +} + +XCTMain([ + testCase(HMACAlgorithmTests.allTests), + testCase(NoneAlgorithmTests.allTests), + testCase(CompactJSONDecoderTests.allTests), + testCase(CompactJSONEncoderTests.allTests), + testCase(DecodeTests.allTests), + testCase(IntegrationTests.allTests), + testCase(JWTEncodeTests.allTests), + testCase(PayloadTests.allTests), + testCase(ValidationTests.allTests) +])