diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..345bfdb --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +xcuserdata/ +*.xccheckout +.build/ +Packages/ diff --git a/.travis.yml b/.travis.yml index 38d6b3d..2c00dc4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,38 @@ -language: objective-c -before_install: -- gem install cocoapods --no-document -- gem install xcpretty --no-document +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 -- xcodebuild -workspace JWT.xcworkspace -scheme JWT test -sdk macosx | xcpretty -c -- pod lib lint +- 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/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 a66e138..0000000 --- a/JSONWebToken.podspec +++ /dev/null @@ -1,17 +0,0 @@ -Pod::Spec.new do |spec| - spec.name = 'JSONWebToken' - spec.version = '1.2.0' - 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.social_media_url = 'http://twitter.com/kylefuller' - spec.source = { :git => 'https://github.com/kylef/JSONWebToken.swift.git', :tag => "#{spec.version}" } - spec.source_files = 'JWT/*.swift' - spec.ios.deployment_target = '8.0' - spec.osx.deployment_target = '10.9' - spec.requires_arc = true - spec.dependency 'CryptoSwift', '~> 0.0.8' - spec.module_name = 'JWT' -end - diff --git a/JWT.xcodeproj/project.pbxproj b/JWT.xcodeproj/project.pbxproj deleted file mode 100644 index 742b8b5..0000000 --- a/JWT.xcodeproj/project.pbxproj +++ /dev/null @@ -1,570 +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 */; }; - 279D63B91AD0803F0024E2BC /* JWT.swift in Sources */ = {isa = PBXBuildFile; fileRef = 279D63B81AD0803F0024E2BC /* JWT.swift */; }; - 279D63BB1AD0E3FA0024E2BC /* Claims.swift in Sources */ = {isa = PBXBuildFile; fileRef = 279D63BA1AD0E3FA0024E2BC /* Claims.swift */; }; - 279D63BD1AD0ED750024E2BC /* Decode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 279D63BC1AD0ED750024E2BC /* Decode.swift */; }; - 279D63BF1AD0EDC00024E2BC /* Base64.swift in Sources */ = {isa = PBXBuildFile; fileRef = 279D63BE1AD0EDC00024E2BC /* Base64.swift */; }; - 885619E9E1C342A9D8BD77B7 /* Pods_JWT.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 540942F3614C41E3827F2013 /* Pods_JWT.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; - EBEC5851F5183DF2D7BFE1AF /* Pods_JWTTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE8198B6E30BA6B8F8125FA7 /* Pods_JWTTests.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; -/* End PBXBuildFile section */ - -/* Begin PBXContainerItemProxy section */ - 279D63A91AD07FFF0024E2BC /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 279D63931AD07FFF0024E2BC /* Project object */; - proxyType = 1; - remoteGlobalIDString = 279D639B1AD07FFF0024E2BC; - remoteInfo = JWT; - }; -/* 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 = ""; }; - 279D63B81AD0803F0024E2BC /* JWT.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JWT.swift; sourceTree = ""; }; - 279D63BA1AD0E3FA0024E2BC /* Claims.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Claims.swift; sourceTree = ""; }; - 279D63BC1AD0ED750024E2BC /* Decode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Decode.swift; sourceTree = ""; }; - 279D63BE1AD0EDC00024E2BC /* Base64.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Base64.swift; sourceTree = ""; }; - 3BD8D638895FE8AF4FDDA8A9 /* Pods-JWTTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JWTTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-JWTTests/Pods-JWTTests.debug.xcconfig"; sourceTree = ""; }; - 540942F3614C41E3827F2013 /* Pods_JWT.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_JWT.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 56671E3EAC540766DE31974E /* Pods-JWT.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JWT.release.xcconfig"; path = "Pods/Target Support Files/Pods-JWT/Pods-JWT.release.xcconfig"; sourceTree = ""; }; - 85B0E9B465B3B29391C19D14 /* Pods-JWTTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JWTTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-JWTTests/Pods-JWTTests.release.xcconfig"; sourceTree = ""; }; - 8CDC721EB1EAFD72E8CCF46E /* Pods-JWT.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JWT.debug.xcconfig"; path = "Pods/Target Support Files/Pods-JWT/Pods-JWT.debug.xcconfig"; 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 = ( - 885619E9E1C342A9D8BD77B7 /* Pods_JWT.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 279D63A41AD07FFF0024E2BC /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 279D63A81AD07FFF0024E2BC /* JWT.framework in Frameworks */, - EBEC5851F5183DF2D7BFE1AF /* Pods_JWTTests.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 279D63921AD07FFF0024E2BC = { - isa = PBXGroup; - children = ( - 279D639E1AD07FFF0024E2BC /* JWT */, - 279D63AB1AD07FFF0024E2BC /* JWTTests */, - 279D639D1AD07FFF0024E2BC /* Products */, - 378492BA2DE66F2F8E379F49 /* Pods */, - 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 */, - 279D63B81AD0803F0024E2BC /* JWT.swift */, - 279D63BC1AD0ED750024E2BC /* Decode.swift */, - 279D63BE1AD0EDC00024E2BC /* Base64.swift */, - 279D63BA1AD0E3FA0024E2BC /* Claims.swift */, - 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 = ""; - }; - 378492BA2DE66F2F8E379F49 /* Pods */ = { - isa = PBXGroup; - children = ( - 8CDC721EB1EAFD72E8CCF46E /* Pods-JWT.debug.xcconfig */, - 56671E3EAC540766DE31974E /* Pods-JWT.release.xcconfig */, - 3BD8D638895FE8AF4FDDA8A9 /* Pods-JWTTests.debug.xcconfig */, - 85B0E9B465B3B29391C19D14 /* Pods-JWTTests.release.xcconfig */, - ); - name = Pods; - 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 = ( - 4B9936F743EA727EA58353BD /* Check Pods Manifest.lock */, - 279D63971AD07FFF0024E2BC /* Sources */, - 279D63981AD07FFF0024E2BC /* Frameworks */, - 279D63991AD07FFF0024E2BC /* Headers */, - 279D639A1AD07FFF0024E2BC /* Resources */, - 842FB8EA008161B653B5AD81 /* Embed Pods Frameworks */, - 6D3D4069FD3A7DC06168A6A2 /* Copy Pods Resources */, - ); - buildRules = ( - ); - dependencies = ( - ); - 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 = ( - 7DCEFCF83F6CF372A33D5219 /* Check Pods Manifest.lock */, - 279D63A31AD07FFF0024E2BC /* Sources */, - 279D63A41AD07FFF0024E2BC /* Frameworks */, - 279D63A51AD07FFF0024E2BC /* Resources */, - E68E26141F4DF11E3638A2F0 /* Embed Pods Frameworks */, - F7C4974401B24623F6907649 /* Copy Pods 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 = { - LastUpgradeCheck = 0620; - 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 = ""; - projectRoot = ""; - targets = ( - 279D639B1AD07FFF0024E2BC /* JWT */, - 279D63A61AD07FFF0024E2BC /* JWTTests */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 279D639A1AD07FFF0024E2BC /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 279D63A51AD07FFF0024E2BC /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXShellScriptBuildPhase section */ - 4B9936F743EA727EA58353BD /* Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Check Pods Manifest.lock"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [[ $? != 0 ]] ; then\n cat << EOM\nerror: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\nEOM\n exit 1\nfi\n"; - showEnvVarsInLog = 0; - }; - 6D3D4069FD3A7DC06168A6A2 /* Copy Pods Resources */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Copy Pods Resources"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-JWT/Pods-JWT-resources.sh\"\n"; - showEnvVarsInLog = 0; - }; - 7DCEFCF83F6CF372A33D5219 /* Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Check Pods Manifest.lock"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [[ $? != 0 ]] ; then\n cat << EOM\nerror: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\nEOM\n exit 1\nfi\n"; - showEnvVarsInLog = 0; - }; - 842FB8EA008161B653B5AD81 /* Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Embed Pods Frameworks"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-JWT/Pods-JWT-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; - E68E26141F4DF11E3638A2F0 /* Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Embed Pods Frameworks"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-JWTTests/Pods-JWTTests-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; - F7C4974401B24623F6907649 /* Copy Pods Resources */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Copy Pods Resources"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-JWTTests/Pods-JWTTests-resources.sh\"\n"; - showEnvVarsInLog = 0; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 279D63971AD07FFF0024E2BC /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 279D63BF1AD0EDC00024E2BC /* Base64.swift in Sources */, - 279D63BB1AD0E3FA0024E2BC /* Claims.swift in Sources */, - 279D63BD1AD0ED750024E2BC /* Decode.swift in Sources */, - 279D63B91AD0803F0024E2BC /* JWT.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 */; - }; -/* 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; - 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; - baseConfigurationReference = 8CDC721EB1EAFD72E8CCF46E /* Pods-JWT.debug.xcconfig */; - 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_NAME = "$(TARGET_NAME)"; - SKIP_INSTALL = YES; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - }; - name = Debug; - }; - 279D63B41AD07FFF0024E2BC /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 56671E3EAC540766DE31974E /* Pods-JWT.release.xcconfig */; - 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_NAME = "$(TARGET_NAME)"; - SKIP_INSTALL = YES; - }; - name = Release; - }; - 279D63B61AD07FFF0024E2BC /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 3BD8D638895FE8AF4FDDA8A9 /* Pods-JWTTests.debug.xcconfig */; - 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_NAME = "$(TARGET_NAME)"; - }; - name = Debug; - }; - 279D63B71AD07FFF0024E2BC /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 85B0E9B465B3B29391C19D14 /* Pods-JWTTests.release.xcconfig */; - 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_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 64d5bf3..0000000 --- a/JWT.xcodeproj/xcshareddata/xcschemes/JWT.xcscheme +++ /dev/null @@ -1,110 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/JWT.xcworkspace/contents.xcworkspacedata b/JWT.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 495e8bc..0000000 --- a/JWT.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - diff --git a/JWT/Base64.swift b/JWT/Base64.swift deleted file mode 100644 index b9143ef..0000000 --- a/JWT/Base64.swift +++ /dev/null @@ -1,28 +0,0 @@ -import Foundation - - -/// URI Safe base64 encode -func base64encode(input:NSData) -> String { - let data = input.base64EncodedDataWithOptions(NSDataBase64EncodingOptions(0)) - let string = NSString(data: data, encoding: NSUTF8StringEncoding) as String - return string - .stringByReplacingOccurrencesOfString("+", withString: "-", options: NSStringCompareOptions(0), range: nil) - .stringByReplacingOccurrencesOfString("/", withString: "_", options: NSStringCompareOptions(0), range: nil) - .stringByReplacingOccurrencesOfString("=", withString: "", options: NSStringCompareOptions(0), range: nil) -} - -/// URI Safe base64 decode -func base64decode(input:String) -> NSData? { - let rem = countElements(input) % 4 - - var ending = "" - if rem > 0 { - let amount = 4 - rem - ending = String(count: amount, repeatedValue: Character("=")) - } - - let base64 = input.stringByReplacingOccurrencesOfString("-", withString: "+", options: NSStringCompareOptions(0), range: nil) - .stringByReplacingOccurrencesOfString("_", withString: "/", options: NSStringCompareOptions(0), range: nil) + ending - - return NSData(base64EncodedString: base64, options: NSDataBase64DecodingOptions(0)) -} diff --git a/JWT/Claims.swift b/JWT/Claims.swift deleted file mode 100644 index 15964c4..0000000 --- a/JWT/Claims.swift +++ /dev/null @@ -1,53 +0,0 @@ -import Foundation - -func validateClaims(payload:Payload, audience:String?, issuer:String?) -> InvalidToken? { - return validateIssuer(payload, issuer) ?? validateAudience(payload, audience) ?? - validateDate(payload, "exp", .OrderedAscending, .ExpiredSignature, "Expiration time claim (exp) must be an integer") ?? - validateDate(payload, "nbf", .OrderedDescending, .ImmatureSignature, "Not before claim (nbf) must be an integer") ?? - validateDate(payload, "iat", .OrderedDescending, .InvalidIssuedAt, "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 !contains(aud, 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 { - let date = NSDate(timeIntervalSince1970: timestamp) - if date.compare(NSDate()) == comparison { - return failure - } - } else if let timestamp:AnyObject = payload[key] { - return .DecodeError(decodeError) - } - - return nil -} diff --git a/JWT/Decode.swift b/JWT/Decode.swift deleted file mode 100644 index f44aa21..0000000 --- a/JWT/Decode.swift +++ /dev/null @@ -1,140 +0,0 @@ -import Foundation - - -/// Failure reasons from decoding a JWT -public enum InvalidToken : Printable { - /// 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" - } - } -} - - -/// Result from decoding a JWT -public enum DecodeResult { - /// Decoding succeeded - case Success(Payload) - - /// Decoding failed, take a look at the invalid token reason - case Failure(InvalidToken) -} - -/// Decode a JWT -public func decode(jwt:String, algorithms:[Algorithm], verify:Bool = true, audience:String? = nil, issuer:String? = nil) -> DecodeResult { - switch load(jwt) { - case let .Success(header, payload, signature, signatureInput): - if verify { - if let failure = validateClaims(payload, audience, issuer) ?? verifySignature(algorithms, header, signatureInput, signature) { - return .Failure(failure) - } - } - - return .Success(payload) - case .Failure(let failure): - return .Failure(failure) - } -} - -/// Decode a JWT -public func decode(jwt:String, algorithm:Algorithm, verify:Bool = true, audience:String? = nil, issuer:String? = nil) -> DecodeResult { - return decode(jwt, [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 = NSJSONSerialization.JSONObjectWithData(headerData!, options: NSJSONReadingOptions(0), error: nil) 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 = NSJSONSerialization.JSONObjectWithData(payloadData!, options: NSJSONReadingOptions(0), error: nil) 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 = filter(algorithms) { algorithm in algorithm.description == alg } - let results = map(matchingAlgorithms) { algorithm in algorithm.verify(signingInput, signature: signature) } - let successes = filter(results) { $0 } - if successes.count > 0 { - return nil - } - - return .InvalidAlgorithm - } - - return .DecodeError("Missing Algorithm") -} diff --git a/JWT/Info.plist b/JWT/Info.plist deleted file mode 100644 index dbc9736..0000000 --- a/JWT/Info.plist +++ /dev/null @@ -1,28 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - org.cocode.$(PRODUCT_NAME:rfc1034identifier) - 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/JWT/JWT.swift b/JWT/JWT.swift deleted file mode 100644 index d59eb81..0000000 --- a/JWT/JWT.swift +++ /dev/null @@ -1,154 +0,0 @@ -import Foundation -import CryptoSwift - -public typealias Payload = [String:AnyObject] - -/// The supported Algorithms -public enum Algorithm : Printable { - /// No Algorithm, i-e, insecure - case None - - /// HMAC using SHA-256 hash algorithm - case HS256(String) - - static func algorithm(name:String, key:String?) -> Algorithm? { - if name == "none" { - if let key = key { - 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) - } - } - - return nil - } - - public var description:String { - switch self { - case .None: - return "none" - case .HS256(let key): - return "HS256" - } - } - - /// Sign a message using the algorithm - func sign(message:String) -> String { - switch self { - case .None: - return "" - - case .HS256(let key): - let mac = Authenticator.HMAC(key: key.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false)!, variant:.sha256) - let result = mac.authenticate(message.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false)!)! - return base64encode(result) - } - } - - /// 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 - :param: payload The payload to sign - :param: 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 = NSJSONSerialization.dataWithJSONObject(payload, options: NSJSONWritingOptions(0), error: nil) { - 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) -} diff --git a/JWTTests/JWTTests.swift b/JWTTests/JWTTests.swift deleted file mode 100644 index 1e442ed..0000000 --- a/JWTTests/JWTTests.swift +++ /dev/null @@ -1,259 +0,0 @@ -import XCTest -import JWT - -class JWTEncodeTests : XCTestCase { - func testEncodingJWT() { - let payload = ["name": "Kyle"] as Payload - let jwt = JWT.encode(payload, .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(JWT.decode(jwt, 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" - let result = JWT.decode(jwt, .HS256("secret")) - assertSuccess(result) { payload in - XCTAssertEqual(payload as NSDictionary, ["name": "Kyle"]) - } - } - - func testFailsToDecodeInvalidStringWithoutThreeSegments() { - assertDecodeError(decode("a.b", .None), "Not enough segments") - } - - // MARK: Disable verify - - func testDisablingVerify() { - let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.2_8pWJfyPup0YwOXK7g9Dn0cF1E3pdn299t4hSeJy5w" - assertSuccess(decode(jwt, .None, verify:false, issuer:"fuller.li")) - } - - // MARK: Issuer claim - - func testSuccessfulIssuerValidation() { - let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJmdWxsZXIubGkifQ.d7B7PAQcz1E6oNhrlxmHxHXHgg39_k7X7wWeahl8kSQ" - assertSuccess(decode(jwt, .HS256("secret"), issuer:"fuller.li")) { payload in - XCTAssertEqual(payload as NSDictionary, ["iss": "fuller.li"]) - } - } - - func testIncorrectIssuerValidation() { - let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJmdWxsZXIubGkifQ.wOhJ9_6lx-3JGJPmJmtFCDI3kt7uMAMmhHIslti7ryI" - assertFailure(decode(jwt, .HS256("secret"), issuer:"querykit.org")) - } - - func testMissingIssuerValidation() { - let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.2_8pWJfyPup0YwOXK7g9Dn0cF1E3pdn299t4hSeJy5w" - assertFailure(decode(jwt, .HS256("secret"), issuer:"fuller.li")) - } - - // MARK: Expiration claim - - func testExpiredClaim() { - let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0MjgxODg0OTF9.cy6b2szsNkKnHFnz2GjTatGjoHBTs8vBKnPGZgpp91I" - assertFailure(decode(jwt, .HS256("secret"))) - } - - func testInvalidExpiaryClaim() { - let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOlsiMTQyODE4ODQ5MSJdfQ.OwF-wd3THjxrEGUhh6IdnNhxQZ7ydwJ3Z6J_dfl9MBs" - assertFailure(decode(jwt, .HS256("secret"))) - } - - func testUnexpiredClaim() { - // If this just started failing, hello 2024! - let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MjgxODg0OTF9.EW7k-8Mvnv0GpvOKJalFRLoCB3a3xGG3i7hAZZXNAz0" - assertSuccess(decode(jwt, .HS256("secret"))) { payload in - XCTAssertEqual(payload as NSDictionary, ["exp": 1728188491]) - } - } - - // MARK: Not before claim - - func testNotBeforeClaim() { - let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOjE0MjgxODk3MjB9.jFT0nXAJvEwyG6R7CMJlzNJb7FtZGv30QRZpYam5cvs" - assertSuccess(decode(jwt, .HS256("secret"))) { payload in - XCTAssertEqual(payload as NSDictionary, ["nbf": 1428189720]) - } - } - - func testInvalidNotBeforeClaim() { - let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOlsxNDI4MTg5NzIwXX0.PUL1FQubzzJa4MNXe2D3d5t5cMaqFr3kYlzRUzly-C8" - assertDecodeError(decode(jwt, .HS256("secret")), "Not before claim (nbf) must be an integer") - } - - func testUnmetNotBeforeClaim() { - // If this just started failing, hello 2024! - let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOjE3MjgxODg0OTF9.Tzhu1tu-7BXcF5YEIFFE1Vmg4tEybUnaz58FR4PcblQ" - assertFailure(decode(jwt, .HS256("secret"))) - } - - // MARK: Issued at claim - - func testIssuedAtClaimInThePast() { - let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE0MjgxODk3MjB9.I_5qjRcCUZVQdABLwG82CSuu2relSdIyJOyvXWUAJh4" - assertSuccess(decode(jwt, .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(decode(jwt, .HS256("secret"))) - } - - func testInvalidIssuedAtClaim() { - // If this just started failing, hello 2024! - let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOlsxNzI4MTg4NDkxXX0.ND7QMWtLkXDXH38OaXM3SQgLo3Z5TNgF_pcfWHV_alQ" - assertDecodeError(decode(jwt, .HS256("secret")), "Issued at claim (iat) must be an integer") - } - - // MARK: Audience claims - - func testAudiencesClaim() { - let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsibWF4aW5lIiwia2F0aWUiXX0.-PKvdNLCClrWG7CvesHP6PB0-vxu-_IZcsYhJxBy5JM" - assertSuccess(decode(jwt, .HS256("secret"), audience:"maxine")) { payload in - XCTAssertEqual(payload as NSDictionary, ["aud": ["maxine", "katie"]]) - } - } - - func testAudienceClaim() { - let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJreWxlIn0.dpgH4JOwueReaBoanLSxsGTc7AjKUvo7_M1sAfy_xVE" - assertSuccess(decode(jwt, .HS256("secret"), audience:"kyle")) { payload in - XCTAssertEqual(payload as NSDictionary, ["aud": "kyle"]) - } - } - - func testMismatchAudienceClaim() { - let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJreWxlIn0.VEB_n06pTSLlTXPFkc46ARADJ9HXNUBUPo3VhL9RDe4" // kyle - assertFailure(decode(jwt, .HS256("secret"), audience:"maxine")) - } - - func testMissingAudienceClaim() { - let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.2_8pWJfyPup0YwOXK7g9Dn0cF1E3pdn299t4hSeJy5w" - assertFailure(decode(jwt, .HS256("secret"), audience:"kyle")) - } - - // MARK: Signature verification - - func testNoneAlgorithm() { - let jwt = "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJ0ZXN0IjoiaW5nIn0." - assertSuccess(decode(jwt, .None)) { payload in - XCTAssertEqual(payload as NSDictionary, ["test": "ing"]) - } - } - - func testNoneFailsWithSecretAlgorithm() { - let jwt = "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJ0ZXN0IjoiaW5nIn0." - assertFailure(decode(jwt, .HS256("secret"))) - } - - func testMatchesAnyAlgorithm() { - let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.2_8pWJfyPup0YwOXK7g9Dn0cF1E3pdn299t4hSeJy5w." - assertFailure(decode(jwt, [.HS256("anothersecret"), .HS256("secret")])) - } -} - -// MARK: Helpers - -func assertSuccess(result:DecodeResult, closure:(Payload -> ())? = nil) { - switch result { - case .Success(let payload): - if let closure = closure { - closure(payload) - } - case .Failure(let failure): - XCTFail("Failed to decode while expecting success. \(failure)") - break - } -} - -func assertFailure(result:DecodeResult, closure:(InvalidToken -> ())? = nil) { - switch result { - case .Success(let payload): - XCTFail("Decoded when expecting a failure.") - case .Failure(let failure): - if let closure = closure { - closure(failure) - } - break - } -} - -func assertDecodeError(result:DecodeResult, error:String) { - assertFailure(result) { 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 new file mode 100644 index 0000000..01b5e95 --- /dev/null +++ b/Package.swift @@ -0,0 +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", + products: [ + .library(name: "JWT", targets: ["JWT"]), + ], + dependencies: dependencies, + targets: [ + .target(name: "JWA", dependencies: targetDependencies, exclude: excludes), + .target(name: "JWT", dependencies: ["JWA"]), + .testTarget(name: "JWATests", dependencies: ["JWA"]), + .testTarget(name: "JWTTests", dependencies: ["JWT"]), + ] +) diff --git a/Podfile b/Podfile deleted file mode 100644 index 76e44d1..0000000 --- a/Podfile +++ /dev/null @@ -1,11 +0,0 @@ -platform :osx, '10.9' -use_frameworks! - -target 'JWT' do - podspec -end - -target 'JWTTests' do - podspec -end - diff --git a/Podfile.lock b/Podfile.lock deleted file mode 100644 index d65ebdc..0000000 --- a/Podfile.lock +++ /dev/null @@ -1,10 +0,0 @@ -PODS: - - CryptoSwift (0.0.8) - -DEPENDENCIES: - - CryptoSwift (~> 0.0.8) - -SPEC CHECKSUMS: - CryptoSwift: 6d1b93af5b48e02e57366bfad28b00170af405ee - -COCOAPODS: 0.36.3 diff --git a/README.md b/README.md index c9cbc02..0874ddd 100644 --- a/README.md +++ b/README.md @@ -6,12 +6,14 @@ Swift implementation of [JSON Web Token](https://tools.ietf.org/html/draft-ietf- ## Installation -[CocoaPods](http://cocoapods.org/) is the recommended installation method. +Swift Pacakage Manager is the recommended installation method for JSONWebToken, [CocoaPods](http://cocoapods.org/) is also supported. ```ruby pod 'JSONWebToken' ``` +**NOTE:** *Carthage may be supported, however support will not be provided for this installation method, use at your own risk if you know how it works.* + ## Usage ```swift @@ -21,15 +23,26 @@ import JWT ### Encoding a claim ```swift -JWT.encode(["my": "payload"], .HS256("secret")) +JWT.encode(claims: ["my": "payload"], algorithm: .hs256("secret".data(using: .utf8)!)) +``` + +#### Encoding a claim set + +```swift +var claims = ClaimSet() +claims.issuer = "fuller.li" +claims.issuedAt = Date() +claims["custom"] = "Hi" + +JWT.encode(claims: claims, algorithm: .hs256("secret".data(using: .utf8)!)) ``` #### Building a JWT with the builder pattern ```swift -JWT.encode(.HS256("secret")) { builder in +JWT.encode(.hs256("secret".data(using: .utf8))) { builder in builder.issuer = "fuller.li" - builder.issuedAt = NSDate() + builder.issuedAt = Date() builder["custom"] = "Hi" } ``` @@ -39,13 +52,28 @@ JWT.encode(.HS256("secret")) { builder in When decoding a JWT, you must supply one or more algorithms and keys. ```swift -JWT.decode("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.2_8pWJfyPup0YwOXK7g9Dn0cF1E3pdn299t4hSeJy5w", .HS256("secret")) +do { + let claims: ClaimSet = try JWT.decode("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.2_8pWJfyPup0YwOXK7g9Dn0cF1E3pdn299t4hSeJy5w", algorithm: .hs256("secret".data(using: .utf8)!)) + print(claims) +} catch { + print("Failed to decode JWT: \(error)") +} ``` When the JWT may be signed with one out of many algorithms or keys: ```swift -JWT.decode("eyJh...5w", [.HS256("secret"), .HS256("secret2"), .HS512("secure")]) +try JWT.decode("eyJh...5w", algorithms: [ + .hs256("secret".data(using: .utf8)!), + .hs256("secret2".data(using: .utf8)!), + .hs512("secure".data(using: .utf8)!) +]) +``` + +You might also want to give your iat, exp and nbf checks some kind of leeway to account for skewed clocks. You can do this by passing a `leeway` parameter like this: + +```swift +try JWT.decode("eyJh...5w", algorithm: .hs256("secret".data(using: .utf8)!), leeway: 10) ``` #### Supported claims @@ -62,19 +90,11 @@ The library supports validating the following claims: This library supports the following algorithms: -- None - Unsecured JWTs -- HS256 - HMAC using SHA-256 hash algorithm (default) - -#### Additional Algorithms - -Support for HS384 and HS512 can be found in the `algorithms-hs` branch which depends on an unreleased version of CryptoSwift. It can be installed via: - -```ruby -pod 'JSONWebToken', :git => 'https://github.com/kylef/JSONWebToken.swift.git', :branch => 'algorithms-hs' -pod 'CryptoSwift', :head -``` +- `none` - Unsecured JWTs +- `hs256` - HMAC using SHA-256 hash algorithm (default) +- `hs384` - HMAC using SHA-384 hash algorithm +- `hs512` - HMAC using SHA-512 hash algorithm ## License JSONWebToken is licensed under the BSD license. See [LICENSE](LICENSE) for more info. - 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/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 91% rename from JWTTests/Info.plist rename to Tests/JWTTests/Info.plist index d3de7c7..ba72822 100644 --- a/JWTTests/Info.plist +++ b/Tests/JWTTests/Info.plist @@ -7,7 +7,7 @@ CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier - org.cocode.$(PRODUCT_NAME:rfc1034identifier) + $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName 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) +])