From b261a71eeea7c534bb19b349ac62405997a77aff Mon Sep 17 00:00:00 2001 From: Hernan Zalazar Date: Fri, 4 Sep 2015 18:06:33 -0700 Subject: [PATCH 01/12] Make project compile in swift 2 --- .gitignore | 1 + Cartfile.private | 4 ++-- Cartfile.resolved | 4 ++-- Carthage/Checkouts/Nimble | 2 +- Carthage/Checkouts/Quick | 2 +- JWTDecode.playground/Contents.swift | 3 +++ JWTDecode.playground/contents.xcplayground | 4 ++++ JWTDecode.playground/timeline.xctimeline | 6 ++++++ JWTDecode.xcodeproj/project.pbxproj | 3 +++ JWTDecode/JWTDecode.swift | 16 ++++++++++++---- JWTDecodeTests/A0JWTDecodeSpec.m | 9 ++++----- JWTDecodeTests/JWTDecodeSpec.swift | 7 ++++--- 12 files changed, 43 insertions(+), 18 deletions(-) create mode 100644 JWTDecode.playground/Contents.swift create mode 100644 JWTDecode.playground/contents.xcplayground create mode 100644 JWTDecode.playground/timeline.xctimeline diff --git a/.gitignore b/.gitignore index e1d2182..0e43357 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,7 @@ profile DerivedData *.hmap *.ipa +*.xcscmblueprint # Bundler .bundle diff --git a/Cartfile.private b/Cartfile.private index fe5940b..ed59bd5 100644 --- a/Cartfile.private +++ b/Cartfile.private @@ -1,4 +1,4 @@ #Quick & Nimble -github "Quick/Quick" ~> 0.3 -github "Quick/Nimble" ~> 0.4 \ No newline at end of file +github "Quick/Quick" ~> 0.6 +github "Quick/Nimble" "v2.0.0-rc.3" \ No newline at end of file diff --git a/Cartfile.resolved b/Cartfile.resolved index c9c535b..9ed9dde 100644 --- a/Cartfile.resolved +++ b/Cartfile.resolved @@ -1,2 +1,2 @@ -github "Quick/Nimble" "v0.4.2" -github "Quick/Quick" "v0.3.1" +github "Quick/Nimble" "811003c1e556d6fedd12a6e8b81da235a7479aca" +github "Quick/Quick" "v0.6.0" diff --git a/Carthage/Checkouts/Nimble b/Carthage/Checkouts/Nimble index 8927113..e239824 160000 --- a/Carthage/Checkouts/Nimble +++ b/Carthage/Checkouts/Nimble @@ -1 +1 @@ -Subproject commit 8927113f877f32c8a01b41b746c4ac261b42c48e +Subproject commit e239824d84105de9e13b815006bc36e938cdd030 diff --git a/Carthage/Checkouts/Quick b/Carthage/Checkouts/Quick index 8bf96f7..b47b9eb 160000 --- a/Carthage/Checkouts/Quick +++ b/Carthage/Checkouts/Quick @@ -1 +1 @@ -Subproject commit 8bf96f708924d728dab5f0cf7543b6f1b896a209 +Subproject commit b47b9ebf97bc8e377070bd1e868c9f89b870c4fc diff --git a/JWTDecode.playground/Contents.swift b/JWTDecode.playground/Contents.swift new file mode 100644 index 0000000..f1f3dca --- /dev/null +++ b/JWTDecode.playground/Contents.swift @@ -0,0 +1,3 @@ +//: Playground - noun: a place where people can play + +import JWTDecode diff --git a/JWTDecode.playground/contents.xcplayground b/JWTDecode.playground/contents.xcplayground new file mode 100644 index 0000000..ee7c14f --- /dev/null +++ b/JWTDecode.playground/contents.xcplayground @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/JWTDecode.playground/timeline.xctimeline b/JWTDecode.playground/timeline.xctimeline new file mode 100644 index 0000000..bf468af --- /dev/null +++ b/JWTDecode.playground/timeline.xctimeline @@ -0,0 +1,6 @@ + + + + + diff --git a/JWTDecode.xcodeproj/project.pbxproj b/JWTDecode.xcodeproj/project.pbxproj index 5fc6be9..9be6d7f 100644 --- a/JWTDecode.xcodeproj/project.pbxproj +++ b/JWTDecode.xcodeproj/project.pbxproj @@ -53,6 +53,7 @@ 5F0069401B3C828F0048928E /* A0JWTDecodeSpec.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = A0JWTDecodeSpec.m; sourceTree = ""; }; 5F05AF7D1B62E9C200C4A9E6 /* Nimble.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Nimble.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 5F05AF7E1B62E9C200C4A9E6 /* Quick.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Quick.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 5F0A5E291B9A6D4A005289CF /* JWTDecode.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; path = JWTDecode.playground; sourceTree = SOURCE_ROOT; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -116,6 +117,7 @@ 5F0068E41B3B46240048928E /* JWTDecode */ = { isa = PBXGroup; children = ( + 5F0A5E291B9A6D4A005289CF /* JWTDecode.playground */, 5F0068E71B3B46240048928E /* JWTDecode.h */, 5F0068E51B3B46240048928E /* Supporting Files */, 5F0069021B3B511F0048928E /* JWTDecode.swift */, @@ -252,6 +254,7 @@ 5F0068D91B3B46240048928E /* Project object */ = { isa = PBXProject; attributes = { + LastSwiftUpdateCheck = 0700; LastUpgradeCheck = 0630; ORGANIZATIONNAME = Auth0; TargetAttributes = { diff --git a/JWTDecode/JWTDecode.swift b/JWTDecode/JWTDecode.swift index 57a9bdd..cd9e47c 100644 --- a/JWTDecode/JWTDecode.swift +++ b/JWTDecode/JWTDecode.swift @@ -29,7 +29,7 @@ Decodes the JWT and return it's payload :returns: the JWT payload or nil when it can be decoded */ -public func payload(#jwt: String) -> [String: AnyObject]? { +public func payload(jwt jwt: String) -> [String: AnyObject]? { return JWTDecoder(jwt: jwt).payloadWithError(nil) } @@ -41,7 +41,7 @@ If the `exp` claim is missing or the jwt can't be decoded it will return true :returns: if the JWT is expired or not */ -public func expired(#jwt: String) -> Bool { +public func expired(jwt jwt: String) -> Bool { return JWTDecoder(jwt: jwt).expired } @@ -52,7 +52,7 @@ Returns the value of the `exp` claim :returns: date that the JWT will expire or nil */ -public func expireDate(#jwt: String) -> NSDate? { +public func expireDate(jwt jwt: String) -> NSDate? { return JWTDecoder(jwt: jwt).expireDate } @@ -105,7 +105,15 @@ public class JWTDecoder: NSObject { base64 = base64.stringByAppendingString(padding) } if let data = NSData(base64EncodedString: base64, options: .IgnoreUnknownCharacters) { - return NSJSONSerialization.JSONObjectWithData(data, options: .allZeros, error: error) as? [String: AnyObject] + do { + let json = try NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions()) + return json as? [String: AnyObject] + } + catch let jsonError as NSError { + if error != nil { + error.memory = jsonError + } + } } else { if error != nil { error.memory = errorWithDescription(NSLocalizedString("malformed jwt token \(jwt). failed to decode base64 payload", comment: "Invalid base64")) diff --git a/JWTDecodeTests/A0JWTDecodeSpec.m b/JWTDecodeTests/A0JWTDecodeSpec.m index 490957f..90b498e 100644 --- a/JWTDecodeTests/A0JWTDecodeSpec.m +++ b/JWTDecodeTests/A0JWTDecodeSpec.m @@ -23,20 +23,19 @@ #define QUICK_DISABLE_SHORT_SYNTAX 1 -#import -#import - +@import Quick; +@import Nimble; @import JWTDecode; QuickSpecBegin(A0JWTDecodeSpec) -__block JWTDecoder *decoder; +__block A0JWTDecoder *decoder; __block NSError *error; describe(@"Objc support", ^{ beforeEach(^{ - decoder = [[JWTDecoder alloc] initWithJwt:@"INVALID"]; + decoder = [[A0JWTDecoder alloc] initWithJwt:@"INVALID"]; error = nil; }); diff --git a/JWTDecodeTests/JWTDecodeSpec.swift b/JWTDecodeTests/JWTDecodeSpec.swift index 86761bb..1ee7e05 100644 --- a/JWTDecodeTests/JWTDecodeSpec.swift +++ b/JWTDecodeTests/JWTDecodeSpec.swift @@ -29,13 +29,14 @@ let inTwoHours = NSDate(timeIntervalSinceNow: 2 * 60 * 60) func jwtWithPayload(payload: [String: AnyObject]) -> String { var jwt: String = "" - if let data = NSJSONSerialization.dataWithJSONObject(payload, options: .allZeros, error: nil) { - let base64 = data.base64EncodedStringWithOptions(.allZeros) + do { + let data = try NSJSONSerialization.dataWithJSONObject(payload, options: NSJSONWritingOptions()) + let base64 = data.base64EncodedStringWithOptions(NSDataBase64EncodingOptions()) .stringByReplacingOccurrencesOfString("+", withString: "-") .stringByReplacingOccurrencesOfString("/", withString: "_") .stringByReplacingOccurrencesOfString("=", withString: "") jwt = "HEADER.\(base64).SIGNATURE" - } else { + } catch _ { NSException(name: NSInvalidArgumentException, reason: "Failed to build jwt", userInfo: nil).raise() } return jwt From ad273c031907f375622eba6ce2c26133246ad9cc Mon Sep 17 00:00:00 2001 From: Hernan Zalazar Date: Fri, 4 Sep 2015 18:12:44 -0700 Subject: [PATCH 02/12] Update build settings --- JWTDecode.xcodeproj/project.pbxproj | 11 ++++++++++- .../xcshareddata/xcschemes/JWTDecode-OSX.xcscheme | 13 ++++++++----- .../xcshareddata/xcschemes/JWTDecode-iOS.xcscheme | 13 ++++++++----- JWTDecode/Info.plist | 2 +- JWTDecodeTests/Info.plist | 2 +- 5 files changed, 28 insertions(+), 13 deletions(-) diff --git a/JWTDecode.xcodeproj/project.pbxproj b/JWTDecode.xcodeproj/project.pbxproj index 9be6d7f..512d6e7 100644 --- a/JWTDecode.xcodeproj/project.pbxproj +++ b/JWTDecode.xcodeproj/project.pbxproj @@ -255,7 +255,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0700; - LastUpgradeCheck = 0630; + LastUpgradeCheck = 0700; ORGANIZATIONNAME = Auth0; TargetAttributes = { 5F0068E11B3B46240048928E = { @@ -395,6 +395,7 @@ CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; @@ -474,6 +475,7 @@ INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 8.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.auth0.JWTDecode; PRODUCT_MODULE_NAME = JWTDecode; PRODUCT_NAME = JWTDecode; SKIP_INSTALL = YES; @@ -493,6 +495,7 @@ INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 8.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.auth0.JWTDecode; PRODUCT_MODULE_NAME = JWTDecode; PRODUCT_NAME = JWTDecode; SKIP_INSTALL = YES; @@ -513,6 +516,7 @@ ); INFOPLIST_FILE = JWTDecodeTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.auth0.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "JWTDecodeTests/JWTDecode-iOSTests-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -529,6 +533,7 @@ ); INFOPLIST_FILE = JWTDecodeTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.auth0.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "JWTDecodeTests/JWTDecode-iOSTests-Bridging-Header.h"; }; @@ -552,6 +557,7 @@ INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; MACOSX_DEPLOYMENT_TARGET = 10.10; + PRODUCT_BUNDLE_IDENTIFIER = com.auth0.JWTDecode; PRODUCT_MODULE_NAME = JWTDecode; PRODUCT_NAME = JWTDecode; SDKROOT = macosx; @@ -572,6 +578,7 @@ INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; MACOSX_DEPLOYMENT_TARGET = 10.10; + PRODUCT_BUNDLE_IDENTIFIER = com.auth0.JWTDecode; PRODUCT_MODULE_NAME = JWTDecode; PRODUCT_NAME = JWTDecode; SDKROOT = macosx; @@ -595,6 +602,7 @@ INFOPLIST_FILE = JWTDecodeTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; MACOSX_DEPLOYMENT_TARGET = 10.10; + PRODUCT_BUNDLE_IDENTIFIER = "com.auth0.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = macosx; }; @@ -611,6 +619,7 @@ INFOPLIST_FILE = JWTDecodeTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; MACOSX_DEPLOYMENT_TARGET = 10.10; + PRODUCT_BUNDLE_IDENTIFIER = "com.auth0.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = macosx; }; diff --git a/JWTDecode.xcodeproj/xcshareddata/xcschemes/JWTDecode-OSX.xcscheme b/JWTDecode.xcodeproj/xcshareddata/xcschemes/JWTDecode-OSX.xcscheme index d233a39..70b540a 100644 --- a/JWTDecode.xcodeproj/xcshareddata/xcschemes/JWTDecode-OSX.xcscheme +++ b/JWTDecode.xcodeproj/xcshareddata/xcschemes/JWTDecode-OSX.xcscheme @@ -1,6 +1,6 @@ + shouldUseLaunchSchemeArgsEnv = "YES"> @@ -62,15 +62,18 @@ ReferencedContainer = "container:JWTDecode.xcodeproj"> + + + shouldUseLaunchSchemeArgsEnv = "YES"> @@ -62,15 +62,18 @@ ReferencedContainer = "container:JWTDecode.xcodeproj"> + + CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier - com.auth0.JWTDecode + $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName diff --git a/JWTDecodeTests/Info.plist b/JWTDecodeTests/Info.plist index 52c692f..1c82751 100644 --- a/JWTDecodeTests/Info.plist +++ b/JWTDecodeTests/Info.plist @@ -7,7 +7,7 @@ CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier - com.auth0.$(PRODUCT_NAME:rfc1034identifier) + $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName From a78b0695cf1def4909aa5478b6ffff2a3e7b1869 Mon Sep 17 00:00:00 2001 From: Hernan Zalazar Date: Tue, 8 Sep 2015 17:20:01 -0700 Subject: [PATCH 03/12] Refactor public interface for Swift & ObjC Single function to decode JWT. Support for custom & registered claims (only exp registered claim so far). Introduce class A0JWT to bridge for Objective-C --- JWTDecode.playground/Contents.swift | 17 ++++ JWTDecode.xcodeproj/project.pbxproj | 12 +++ JWTDecode/A0JWT.swift | 53 +++++++++++ JWTDecode/JWTDecode.swift | 127 +++++++++----------------- JWTDecodeTests/A0JWTDecodeSpec.m | 15 +--- JWTDecodeTests/JWTDecodeSpec.swift | 133 +++++++++++++++++----------- JWTDecodeTests/JWTHelper.swift | 74 ++++++++++++++++ 7 files changed, 281 insertions(+), 150 deletions(-) create mode 100644 JWTDecode/A0JWT.swift create mode 100644 JWTDecodeTests/JWTHelper.swift diff --git a/JWTDecode.playground/Contents.swift b/JWTDecode.playground/Contents.swift index f1f3dca..76aee9e 100644 --- a/JWTDecode.playground/Contents.swift +++ b/JWTDecode.playground/Contents.swift @@ -1,3 +1,20 @@ //: Playground - noun: a place where people can play import JWTDecode + +let id_token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImV4cCI6MTQ0MTM5NjgwMH0.fJHsc1QnBioH9mCJwA5yltDrxlYaIAadgmVWXPy7FXk" + + +do { + let jwt = try decode(id_token) + jwt.expiresAt + jwt.payload + jwt.signature + if let admin: Bool = jwt.claim("admin") { + admin + } +} catch let error as NSError { + error.localizedDescription +} + +let decoded = try A0JWT.decode(id_token) \ No newline at end of file diff --git a/JWTDecode.xcodeproj/project.pbxproj b/JWTDecode.xcodeproj/project.pbxproj index 512d6e7..77d363a 100644 --- a/JWTDecode.xcodeproj/project.pbxproj +++ b/JWTDecode.xcodeproj/project.pbxproj @@ -20,6 +20,10 @@ 5F05AF801B62E9C200C4A9E6 /* Nimble.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5F05AF7D1B62E9C200C4A9E6 /* Nimble.framework */; }; 5F05AF811B62E9C200C4A9E6 /* Quick.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5F05AF7E1B62E9C200C4A9E6 /* Quick.framework */; }; 5F05AF821B62E9C200C4A9E6 /* Quick.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5F05AF7E1B62E9C200C4A9E6 /* Quick.framework */; }; + 5F8B43691B9F99B400A0D5AE /* A0JWT.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F8B43681B9F99B400A0D5AE /* A0JWT.swift */; settings = {ASSET_TAGS = (); }; }; + 5F8B436A1B9F99B400A0D5AE /* A0JWT.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F8B43681B9F99B400A0D5AE /* A0JWT.swift */; settings = {ASSET_TAGS = (); }; }; + 5F8B436C1B9F9EDB00A0D5AE /* JWTHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F8B436B1B9F9EDB00A0D5AE /* JWTHelper.swift */; settings = {ASSET_TAGS = (); }; }; + 5F8B436D1B9F9EDB00A0D5AE /* JWTHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F8B436B1B9F9EDB00A0D5AE /* JWTHelper.swift */; settings = {ASSET_TAGS = (); }; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -54,6 +58,8 @@ 5F05AF7D1B62E9C200C4A9E6 /* Nimble.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Nimble.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 5F05AF7E1B62E9C200C4A9E6 /* Quick.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Quick.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 5F0A5E291B9A6D4A005289CF /* JWTDecode.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; path = JWTDecode.playground; sourceTree = SOURCE_ROOT; }; + 5F8B43681B9F99B400A0D5AE /* A0JWT.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = A0JWT.swift; sourceTree = ""; }; + 5F8B436B1B9F9EDB00A0D5AE /* JWTHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JWTHelper.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -120,6 +126,7 @@ 5F0A5E291B9A6D4A005289CF /* JWTDecode.playground */, 5F0068E71B3B46240048928E /* JWTDecode.h */, 5F0068E51B3B46240048928E /* Supporting Files */, + 5F8B43681B9F99B400A0D5AE /* A0JWT.swift */, 5F0069021B3B511F0048928E /* JWTDecode.swift */, ); path = JWTDecode; @@ -140,6 +147,7 @@ 5F0069221B3C4A7F0048928E /* JWTDecodeSpec.swift */, 5F0069401B3C828F0048928E /* A0JWTDecodeSpec.m */, 5F00693F1B3C828E0048928E /* JWTDecode-iOSTests-Bridging-Header.h */, + 5F8B436B1B9F9EDB00A0D5AE /* JWTHelper.swift */, ); path = JWTDecodeTests; sourceTree = ""; @@ -328,6 +336,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 5F8B43691B9F99B400A0D5AE /* A0JWT.swift in Sources */, 5F0069031B3B511F0048928E /* JWTDecode.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -336,6 +345,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 5F8B436C1B9F9EDB00A0D5AE /* JWTHelper.swift in Sources */, 5F0069231B3C4A7F0048928E /* JWTDecodeSpec.swift in Sources */, 5F0069411B3C828F0048928E /* A0JWTDecodeSpec.m in Sources */, ); @@ -345,6 +355,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 5F8B436A1B9F99B400A0D5AE /* A0JWT.swift in Sources */, 5F00693E1B3C7B930048928E /* JWTDecode.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -354,6 +365,7 @@ buildActionMask = 2147483647; files = ( 5F0069241B3C4A860048928E /* JWTDecodeSpec.swift in Sources */, + 5F8B436D1B9F9EDB00A0D5AE /* JWTHelper.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/JWTDecode/A0JWT.swift b/JWTDecode/A0JWT.swift new file mode 100644 index 0000000..f773903 --- /dev/null +++ b/JWTDecode/A0JWT.swift @@ -0,0 +1,53 @@ +// A0JWT.swift +// +// Copyright (c) 2015 Auth0 (http://auth0.com) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +public class A0JWT: NSObject { + + var jwt: JWT + + init(jwt: JWT) { + self.jwt = jwt + } + + public var header: [String: AnyObject] { + return self.jwt.header + } + + public var body: [String: AnyObject] { + return self.jwt.body + } + + public var signature: String? { + return self.jwt.signature + } + + public var expiresAt: NSDate? { + return self.jwt.expiresAt + } + + public class func decode(jwtValue: String) throws -> A0JWT { + let jwt = try DecodedJWT(jwt: jwtValue) + return A0JWT(jwt: jwt) + } +} \ No newline at end of file diff --git a/JWTDecode/JWTDecode.swift b/JWTDecode/JWTDecode.swift index cd9e47c..59ff5ae 100644 --- a/JWTDecode/JWTDecode.swift +++ b/JWTDecode/JWTDecode.swift @@ -22,77 +22,35 @@ import Foundation -/** -Decodes the JWT and return it's payload - -:param: jwt to be decoded - -:returns: the JWT payload or nil when it can be decoded -*/ -public func payload(jwt jwt: String) -> [String: AnyObject]? { - return JWTDecoder(jwt: jwt).payloadWithError(nil) +public func decode(jwt: String) throws -> JWT { + return try DecodedJWT(jwt: jwt) } -/** -Check if the JWT is expired using the `exp` claim. -If the `exp` claim is missing or the jwt can't be decoded it will return true - -:param: jwt that will be checked for expiration - -:returns: if the JWT is expired or not -*/ -public func expired(jwt jwt: String) -> Bool { - return JWTDecoder(jwt: jwt).expired -} - -/** -Returns the value of the `exp` claim - -:param: jwt to be decoded +public protocol JWT { + var header: [String: AnyObject] { get } + var body: [String: AnyObject] { get } + var signature: String? { get } -:returns: date that the JWT will expire or nil -*/ -public func expireDate(jwt jwt: String) -> NSDate? { - return JWTDecoder(jwt: jwt).expireDate + var expiresAt: NSDate? { get } + var expired: Bool { get } } -private func errorWithDescription(description: String) -> NSError { - return NSError(domain: "com.auth0.JWTDecode", code: 0, userInfo: [NSLocalizedDescriptionKey: description]) -} - -/** -Class that decodes a JWT payload from Base64 -*/ -@objc(A0JWTDecoder) -public class JWTDecoder: NSObject { - - let jwt: String - - /** - Create a new instance of JWTDecoder - - :param: jwt to decode - - :returns: a new instance - */ - public init(jwt: String) { - self.jwt = jwt +public extension JWT { + public func claim(name: String) -> T? { + return self.body[name] as? T } +} - /** - Returns the payload of the JWT +struct DecodedJWT: JWT { - :param: error if the JWT can't be decoded + let header: [String: AnyObject] + let body: [String: AnyObject] + let signature: String? - :returns: dictionary with JWT payload - */ - public func payloadWithError(error: NSErrorPointer) -> [String: AnyObject]? { + init(jwt: String) throws { let parts = jwt.componentsSeparatedByString(".") - if parts.count != 3 { - if error != nil { - error.memory = errorWithDescription(NSLocalizedString("malformed jwt token \(jwt) only has \(parts.count) parts (3 parts are required)", comment: "Not enough jwt parts")) - } - return nil + guard parts.count == 3 else { + throw errorWithDescription(NSLocalizedString("malformed jwt token \(jwt) only has \(parts.count) parts (3 parts are required)", comment: "Not enough jwt parts")) } var base64 = parts[1] .stringByReplacingOccurrencesOfString("-", withString: "+") @@ -104,38 +62,33 @@ public class JWTDecoder: NSObject { let padding = "".stringByPaddingToLength(Int(paddingLength), withString: "=", startingAtIndex: 0) base64 = base64.stringByAppendingString(padding) } - if let data = NSData(base64EncodedString: base64, options: .IgnoreUnknownCharacters) { - do { - let json = try NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions()) - return json as? [String: AnyObject] - } - catch let jsonError as NSError { - if error != nil { - error.memory = jsonError - } - } - } else { - if error != nil { - error.memory = errorWithDescription(NSLocalizedString("malformed jwt token \(jwt). failed to decode base64 payload", comment: "Invalid base64")) - } + guard let data = NSData(base64EncodedString: base64, options: .IgnoreUnknownCharacters) else { + throw errorWithDescription(NSLocalizedString("malformed jwt token \(jwt). failed to decode base64 payload", comment: "Invalid base64")) } - return nil - } - - /// If the JWT is expired or not - public var expired: Bool { - if let date = self.expireDate { - return date.compare(NSDate()) == .OrderedAscending + guard let json = try NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions()) as? [String: AnyObject] else { + throw errorWithDescription(NSLocalizedString("malformed jwt token \(jwt). failed to decode base64 payload", comment: "Invalid base64")) } - return true + self.header = [:] + self.body = json + self.signature = parts[2] } - /// Date when the JWT will expire - public var expireDate: NSDate? { - if let payload = self.payloadWithError(nil), let exp = payload["exp"] as? Double { + var expiresAt: NSDate? { + if let exp:Double = claim("exp") { return NSDate(timeIntervalSince1970: exp) + } else { + return nil } - return nil } + var expired: Bool { + guard let date = self.expiresAt else { + return false + } + return date.compare(NSDate()) != NSComparisonResult.OrderedDescending + } } + +private func errorWithDescription(description: String) -> NSError { + return NSError(domain: "com.auth0.JWTDecode", code: 0, userInfo: [NSLocalizedDescriptionKey: description]) +} \ No newline at end of file diff --git a/JWTDecodeTests/A0JWTDecodeSpec.m b/JWTDecodeTests/A0JWTDecodeSpec.m index 90b498e..e9fc2bd 100644 --- a/JWTDecodeTests/A0JWTDecodeSpec.m +++ b/JWTDecodeTests/A0JWTDecodeSpec.m @@ -29,28 +29,19 @@ QuickSpecBegin(A0JWTDecodeSpec) -__block A0JWTDecoder *decoder; __block NSError *error; -describe(@"Objc support", ^{ +describe(@"Objective-C support", ^{ beforeEach(^{ - decoder = [[A0JWTDecoder alloc] initWithJwt:@"INVALID"]; error = nil; }); - it(@"should return no payload and an error", ^{ - expect([decoder payloadWithError:&error]).to(beNil()); + it(@"should return nil jwt and an error", ^{ + expect([A0JWT decode:@"INVALID" error:&error]).to(beNil()); expect(error).toNot(beNil()); }); - it(@"should check expiration", ^{ - expect(@(decoder.expired)).to(beTruthy()); - }); - - it(@"should return exp date", ^{ - expect(decoder.expireDate).to(beNil()); - }); }); QuickSpecEnd diff --git a/JWTDecodeTests/JWTDecodeSpec.swift b/JWTDecodeTests/JWTDecodeSpec.swift index 1ee7e05..09e7eea 100644 --- a/JWTDecodeTests/JWTDecodeSpec.swift +++ b/JWTDecodeTests/JWTDecodeSpec.swift @@ -24,92 +24,123 @@ import Quick import Nimble import JWTDecode -let twoHoursAgo = NSDate(timeIntervalSinceNow: -2 * 60 * 60) -let inTwoHours = NSDate(timeIntervalSinceNow: 2 * 60 * 60) - -func jwtWithPayload(payload: [String: AnyObject]) -> String { - var jwt: String = "" - do { - let data = try NSJSONSerialization.dataWithJSONObject(payload, options: NSJSONWritingOptions()) - let base64 = data.base64EncodedStringWithOptions(NSDataBase64EncodingOptions()) - .stringByReplacingOccurrencesOfString("+", withString: "-") - .stringByReplacingOccurrencesOfString("/", withString: "_") - .stringByReplacingOccurrencesOfString("=", withString: "") - jwt = "HEADER.\(base64).SIGNATURE" - } catch _ { - NSException(name: NSInvalidArgumentException, reason: "Failed to build jwt", userInfo: nil).raise() - } - return jwt -} - -func jwtThatExpiresAt(date: NSDate) -> String { - return jwtWithPayload(["exp": date.timeIntervalSince1970]) -} - class JWTDecodeSpec: QuickSpec { override func spec() { - describe("Module functions") { - - let nonExpiredJWT = jwtThatExpiresAt(inTwoHours) - let expiredJWT = jwtThatExpiresAt(twoHoursAgo) + describe("decode") { it("should tell a jwt is expired") { - expect(JWTDecode.expired(jwt: expiredJWT)).to(beTruthy()) + expect(expiredJWT().expired).to(beTruthy()) } it("should tell a jwt is not expired") { - expect(JWTDecode.expired(jwt: nonExpiredJWT)).to(beFalsy()) + expect(nonExpiredJWT().expired).to(beFalsy()) + } + + it("should tell a jwt is expired with a close enough timestamp") { + expect(jwtThatExpiresAt(NSDate()).expired).to(beTruthy()) } it("should obtain payload") { - let jwt = jwtWithPayload(["sub": "myid", "name": "Shawarma Monk"]) - let payload = JWTDecode.payload(jwt: jwt) as! [String: String] + let jwt = jwtWithBody(["sub": "myid", "name": "Shawarma Monk"]) + let payload = jwt.body as! [String: String] expect(payload).to(equal(["sub": "myid", "name": "Shawarma Monk"])) } it("should return expire date") { - expect(JWTDecode.expireDate(jwt: expiredJWT)).toNot(beNil()) + expect(expiredJWT().expiresAt).toNot(beNil()) } - it("should return nil payload for invalid jwt") { - expect(JWTDecode.payload(jwt: "INVALID")).to(beNil()) + it("should decode valid jwt") { + let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJjb20uc29td2hlcmUuZmFyLmJleW9uZDphcGkiLCJpc3MiOiJhdXRoMCIsInVzZXJfcm9sZSI6ImFkbWluIn0.sS84motSLj9HNTgrCPcAjgZIQ99jXNN7_W9fEIIfxz0" + expect(try! decode(jwt)).toNot(beNil()) } + + it("should raise exception with invalid jwt") { + expect { try decode("HEADER.BODY.SIGNATURE") }.to(throwError()) + } + + it("should raise exception with missing parts") { + expect { try decode("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJzdWIifQ") }.to(throwError()) + } + } - describe("JWTDecoder") { + describe("jwt parts") { + let jwt = try! decode("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJzdWIifQ.xXcD7WOvUDHJ94E6aVHYgXdsJHLl2oW7ZXm4QpVvXnY") - let decoder = JWTDecoder(jwt: jwtThatExpiresAt(inTwoHours)) + pending("should return header") { + expect(jwt.header as? [String: String]).to(equal(["alg": "HS256", "typ": "JWT"])) + } - it("should check is not expired") { - expect(decoder.expired).to(beFalsy()) + it("should return body") { + expect(jwt.body as? [String: String]).to(equal(["sub": "sub"])) } - it("should return the payload") { - expect(decoder.payloadWithError(nil)).toNot(beNil()) + it("should return signature") { + expect(jwt.signature).to(equal("xXcD7WOvUDHJ94E6aVHYgXdsJHLl2oW7ZXm4QpVvXnY")) } + } - it("should return exp date") { - expect(decoder.expireDate).toNot(beNil()) + describe("claims") { + var jwt: JWT! + + describe("expiresAt claim") { + + it("should handle expired jwt") { + jwt = expiredJWT() + expect(jwt.expiresAt).toNot(beNil()) + expect(jwt.expired).to(beTruthy()) + } + + it("should handle non-expired jwt") { + jwt = nonExpiredJWT() + expect(jwt.expiresAt).toNot(beNil()) + expect(jwt.expired).to(beFalsy()) + } + + it("should handle jwt without expiresAt claim") { + jwt = jwtWithBody(["sub": NSUUID().UUIDString]) + expect(jwt.expiresAt).to(beNil()) + expect(jwt.expired).to(beFalsy()) + } } - context("invalid JWT") { - let decoder = JWTDecoder(jwt: "INVALID") + describe("custom claim") { - it("should return an error for invalid jwt") { - var error: NSError? - expect(decoder.payloadWithError(&error)).to(beNil()) - expect(error).toNot(beNil()) + beforeEach { + jwt = jwtWithBody(["sub": NSUUID().UUIDString, "custom_claim": "Shawarma Friday!", "custom_integer_claim": 10]) } - it("should return is expired") { - expect(decoder.expired).to(beTruthy()) + it("should return custom claims") { + let stringValue: String? = jwt.claim("custom_claim") + expect(stringValue).to(equal("Shawarma Friday!")) + let integerValue: Int? = jwt.claim("custom_integer_claim") + expect(integerValue).to(equal(10)) } - it("should return exp date") { - expect(decoder.expireDate).to(beNil()) + it("should return nil when claim is not present") { + let unknownClaim: String? = jwt.claim("missing_claim") + expect(unknownClaim).to(beNil()) } } } + + describe("JWTDecoder") { + + let decoder = nonExpiredJWT() + + it("should check is not expired") { + expect(decoder.expired).to(beFalsy()) + } + + it("should return the payload") { + expect(decoder.body).toNot(raiseException()) + } + + it("should return exp date") { + expect(decoder.expiresAt).toNot(beNil()) + } + } } } diff --git a/JWTDecodeTests/JWTHelper.swift b/JWTDecodeTests/JWTHelper.swift new file mode 100644 index 0000000..16d5297 --- /dev/null +++ b/JWTDecodeTests/JWTHelper.swift @@ -0,0 +1,74 @@ +// JWTHelper.swift +// +// Copyright (c) 2015 Auth0 (http://auth0.com) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation +import JWTDecode + +func jwtWithBody(body: [String: AnyObject]) -> JWT { + var jwt: String = "" + do { + let data = try NSJSONSerialization.dataWithJSONObject(body, options: NSJSONWritingOptions()) + let base64 = data.base64EncodedStringWithOptions(NSDataBase64EncodingOptions()) + .stringByReplacingOccurrencesOfString("+", withString: "-") + .stringByReplacingOccurrencesOfString("/", withString: "_") + .stringByReplacingOccurrencesOfString("=", withString: "") + jwt = "HEADER.\(base64).SIGNATURE" + } catch _ { + NSException(name: NSInvalidArgumentException, reason: "Failed to build jwt", userInfo: nil).raise() + } + return try! decode(jwt) +} + +func jwtThatExpiresAt(date: NSDate) -> JWT { + return jwtWithBody(["exp": date.timeIntervalSince1970]) +} + +func expiredJWT() -> JWT { + let seconds = Int(arc4random_uniform(60) + 1) * -1 + let date = NSCalendar.currentCalendar().dateByAddingUnit(.Second, value: seconds, toDate: NSDate(), options: NSCalendarOptions()) + return jwtThatExpiresAt(date!) +} + +func nonExpiredJWT() -> JWT { + let hours = Int(arc4random_uniform(200) + 1) + let date = NSCalendar.currentCalendar().dateByAddingUnit(.Hour, value: hours, toDate: NSDate(), options: NSCalendarOptions()) + return jwtThatExpiresAt(date!) +} + +class JWTHelper: NSObject { + + class func newJWTWithBody(body: [String: AnyObject]) -> JWT { + return jwtWithBody(body) + } + + class func newJWTThatExpiresAt(date: NSDate) -> JWT { + return jwtThatExpiresAt(date) + } + + class func newExpiredJWT() -> JWT { + return expiredJWT() + } + + class func newNonExpiredJWT() -> JWT { + return nonExpiredJWT() + } +} From 22192f02b2bc87c0cb6f185a5c40beecb47713e2 Mon Sep 17 00:00:00 2001 From: Hernan Zalazar Date: Tue, 8 Sep 2015 17:36:53 -0700 Subject: [PATCH 04/12] Now return header part of JWT --- JWTDecode/JWTDecode.swift | 48 ++++++++++++++++++------------ JWTDecodeTests/JWTDecodeSpec.swift | 2 +- JWTDecodeTests/JWTHelper.swift | 2 +- 3 files changed, 31 insertions(+), 21 deletions(-) diff --git a/JWTDecode/JWTDecode.swift b/JWTDecode/JWTDecode.swift index 59ff5ae..4bb8f34 100644 --- a/JWTDecode/JWTDecode.swift +++ b/JWTDecode/JWTDecode.swift @@ -41,6 +41,31 @@ public extension JWT { } } +func base64UrlDecode(value: String) -> NSData? { + var base64 = value + .stringByReplacingOccurrencesOfString("-", withString: "+") + .stringByReplacingOccurrencesOfString("_", withString: "/") + let length = Double(base64.lengthOfBytesUsingEncoding(NSUTF8StringEncoding)) + let requiredLength = 4 * ceil(length / 4.0) + let paddingLength = requiredLength - length + if paddingLength > 0 { + let padding = "".stringByPaddingToLength(Int(paddingLength), withString: "=", startingAtIndex: 0) + base64 = base64.stringByAppendingString(padding) + } + return NSData(base64EncodedString: base64, options: .IgnoreUnknownCharacters) +} + +func decodeJWTPart(value: String) throws -> [String: AnyObject] { + guard let bodyData = base64UrlDecode(value) else { + throw errorWithDescription(NSLocalizedString("Malformed jwt token, failed to decode base64Url value \(value)", comment: "Invalid JWT token base64Url value")) + } + + guard let json = try NSJSONSerialization.JSONObjectWithData(bodyData, options: NSJSONReadingOptions()) as? [String: AnyObject] else { + throw errorWithDescription(NSLocalizedString("Malformed jwt token, failed to parse JSON value from base64Url \(value)", comment: "Invalid JSON value inside base64Url")) + } + return json +} + struct DecodedJWT: JWT { let header: [String: AnyObject] @@ -50,26 +75,11 @@ struct DecodedJWT: JWT { init(jwt: String) throws { let parts = jwt.componentsSeparatedByString(".") guard parts.count == 3 else { - throw errorWithDescription(NSLocalizedString("malformed jwt token \(jwt) only has \(parts.count) parts (3 parts are required)", comment: "Not enough jwt parts")) - } - var base64 = parts[1] - .stringByReplacingOccurrencesOfString("-", withString: "+") - .stringByReplacingOccurrencesOfString("_", withString: "/") - let length = Double(base64.lengthOfBytesUsingEncoding(NSUTF8StringEncoding)) - let requiredLength = 4 * ceil(length / 4.0) - let paddingLength = requiredLength - length - if paddingLength > 0 { - let padding = "".stringByPaddingToLength(Int(paddingLength), withString: "=", startingAtIndex: 0) - base64 = base64.stringByAppendingString(padding) + throw errorWithDescription(NSLocalizedString("Malformed jwt token \(jwt) only has \(parts.count) parts (3 parts are required)", comment: "Not enough jwt parts")) } - guard let data = NSData(base64EncodedString: base64, options: .IgnoreUnknownCharacters) else { - throw errorWithDescription(NSLocalizedString("malformed jwt token \(jwt). failed to decode base64 payload", comment: "Invalid base64")) - } - guard let json = try NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions()) as? [String: AnyObject] else { - throw errorWithDescription(NSLocalizedString("malformed jwt token \(jwt). failed to decode base64 payload", comment: "Invalid base64")) - } - self.header = [:] - self.body = json + + self.header = try decodeJWTPart(parts[0]) + self.body = try decodeJWTPart(parts[1]) self.signature = parts[2] } diff --git a/JWTDecodeTests/JWTDecodeSpec.swift b/JWTDecodeTests/JWTDecodeSpec.swift index 09e7eea..0874590 100644 --- a/JWTDecodeTests/JWTDecodeSpec.swift +++ b/JWTDecodeTests/JWTDecodeSpec.swift @@ -69,7 +69,7 @@ class JWTDecodeSpec: QuickSpec { describe("jwt parts") { let jwt = try! decode("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJzdWIifQ.xXcD7WOvUDHJ94E6aVHYgXdsJHLl2oW7ZXm4QpVvXnY") - pending("should return header") { + it("should return header") { expect(jwt.header as? [String: String]).to(equal(["alg": "HS256", "typ": "JWT"])) } diff --git a/JWTDecodeTests/JWTHelper.swift b/JWTDecodeTests/JWTHelper.swift index 16d5297..6d2c97f 100644 --- a/JWTDecodeTests/JWTHelper.swift +++ b/JWTDecodeTests/JWTHelper.swift @@ -31,7 +31,7 @@ func jwtWithBody(body: [String: AnyObject]) -> JWT { .stringByReplacingOccurrencesOfString("+", withString: "-") .stringByReplacingOccurrencesOfString("/", withString: "_") .stringByReplacingOccurrencesOfString("=", withString: "") - jwt = "HEADER.\(base64).SIGNATURE" + jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.\(base64).SIGNATURE" } catch _ { NSException(name: NSInvalidArgumentException, reason: "Failed to build jwt", userInfo: nil).raise() } From f5be2b8189977af2f6e8e67c2ade61110fc5ed1a Mon Sep 17 00:00:00 2001 From: Hernan Zalazar Date: Tue, 8 Sep 2015 18:18:38 -0700 Subject: [PATCH 05/12] Add support for missing registered claims --- JWTDecode/JWTDecode.swift | 76 +++++++++++++++++++----------- JWTDecodeTests/JWTDecodeSpec.swift | 56 +++++++++++++++------- 2 files changed, 87 insertions(+), 45 deletions(-) diff --git a/JWTDecode/JWTDecode.swift b/JWTDecode/JWTDecode.swift index 4bb8f34..f3e1605 100644 --- a/JWTDecode/JWTDecode.swift +++ b/JWTDecode/JWTDecode.swift @@ -32,6 +32,13 @@ public protocol JWT { var signature: String? { get } var expiresAt: NSDate? { get } + var issuer: String? { get } + var subject: String? { get } + var audience: [String]? { get } + var issuedAt: NSDate? { get } + var notBefore: NSDate? { get } + var identifier: String? { get } + var expired: Bool { get } } @@ -41,31 +48,6 @@ public extension JWT { } } -func base64UrlDecode(value: String) -> NSData? { - var base64 = value - .stringByReplacingOccurrencesOfString("-", withString: "+") - .stringByReplacingOccurrencesOfString("_", withString: "/") - let length = Double(base64.lengthOfBytesUsingEncoding(NSUTF8StringEncoding)) - let requiredLength = 4 * ceil(length / 4.0) - let paddingLength = requiredLength - length - if paddingLength > 0 { - let padding = "".stringByPaddingToLength(Int(paddingLength), withString: "=", startingAtIndex: 0) - base64 = base64.stringByAppendingString(padding) - } - return NSData(base64EncodedString: base64, options: .IgnoreUnknownCharacters) -} - -func decodeJWTPart(value: String) throws -> [String: AnyObject] { - guard let bodyData = base64UrlDecode(value) else { - throw errorWithDescription(NSLocalizedString("Malformed jwt token, failed to decode base64Url value \(value)", comment: "Invalid JWT token base64Url value")) - } - - guard let json = try NSJSONSerialization.JSONObjectWithData(bodyData, options: NSJSONReadingOptions()) as? [String: AnyObject] else { - throw errorWithDescription(NSLocalizedString("Malformed jwt token, failed to parse JSON value from base64Url \(value)", comment: "Invalid JSON value inside base64Url")) - } - return json -} - struct DecodedJWT: JWT { let header: [String: AnyObject] @@ -83,9 +65,22 @@ struct DecodedJWT: JWT { self.signature = parts[2] } - var expiresAt: NSDate? { - if let exp:Double = claim("exp") { - return NSDate(timeIntervalSince1970: exp) + var expiresAt: NSDate? { return claim("exp") } + var issuer: String? { return claim("iss") } + var subject: String? { return claim("sub") } + var audience: [String]? { + guard let aud: String = claim("aud") else { + return claim("aud") + } + return [aud] + } + var issuedAt: NSDate? { return claim("iat") } + var notBefore: NSDate? { return claim("nbf") } + var identifier: String? { return claim("jti") } + + private func claim(name: String) -> NSDate? { + if let timestamp:Double = claim(name) { + return NSDate(timeIntervalSince1970: timestamp) } else { return nil } @@ -99,6 +94,31 @@ struct DecodedJWT: JWT { } } +private func base64UrlDecode(value: String) -> NSData? { + var base64 = value + .stringByReplacingOccurrencesOfString("-", withString: "+") + .stringByReplacingOccurrencesOfString("_", withString: "/") + let length = Double(base64.lengthOfBytesUsingEncoding(NSUTF8StringEncoding)) + let requiredLength = 4 * ceil(length / 4.0) + let paddingLength = requiredLength - length + if paddingLength > 0 { + let padding = "".stringByPaddingToLength(Int(paddingLength), withString: "=", startingAtIndex: 0) + base64 = base64.stringByAppendingString(padding) + } + return NSData(base64EncodedString: base64, options: .IgnoreUnknownCharacters) +} + +private func decodeJWTPart(value: String) throws -> [String: AnyObject] { + guard let bodyData = base64UrlDecode(value) else { + throw errorWithDescription(NSLocalizedString("Malformed jwt token, failed to decode base64Url value \(value)", comment: "Invalid JWT token base64Url value")) + } + + guard let json = try NSJSONSerialization.JSONObjectWithData(bodyData, options: NSJSONReadingOptions()) as? [String: AnyObject] else { + throw errorWithDescription(NSLocalizedString("Malformed jwt token, failed to parse JSON value from base64Url \(value)", comment: "Invalid JSON value inside base64Url")) + } + return json +} + private func errorWithDescription(description: String) -> NSError { return NSError(domain: "com.auth0.JWTDecode", code: 0, userInfo: [NSLocalizedDescriptionKey: description]) } \ No newline at end of file diff --git a/JWTDecodeTests/JWTDecodeSpec.swift b/JWTDecodeTests/JWTDecodeSpec.swift index 0874590..ffdff86 100644 --- a/JWTDecodeTests/JWTDecodeSpec.swift +++ b/JWTDecodeTests/JWTDecodeSpec.swift @@ -106,6 +106,45 @@ class JWTDecodeSpec: QuickSpec { } } + describe("registered claims") { + + let jwt = try! decode("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3NhbXBsZXMuYXV0aDAuY29tIiwic3ViIjoiYXV0aDB8MTAxMDEwMTAxMCIsImF1ZCI6Imh0dHBzOi8vc2FtcGxlcy5hdXRoMC5jb20iLCJleHAiOjEzNzI2NzQzMzYsImlhdCI6MTM3MjYzODMzNiwianRpIjoicXdlcnR5MTIzNDU2IiwibmJmIjoxMzcyNjM4MzM2fQ.LvF9wSheCB5xarpydmurWgi9NOZkdES5AbNb_UWk9Ew") + + + it("should return issuer") { + expect(jwt.issuer).to(equal("https://samples.auth0.com")) + } + + it("should return subject") { + expect(jwt.subject).to(equal("auth0|1010101010")) + } + + it("should return single audience") { + expect(jwt.audience).to(equal(["https://samples.auth0.com"])) + } + + context("multiple audiences") { + + let jwt = try! decode("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiaHR0cHM6Ly9zYW1wbGVzLmF1dGgwLmNvbSIsImh0dHBzOi8vYXBpLnNhbXBsZXMuYXV0aDAuY29tIl19.cfWFPuJbQ7NToa-BjHgHD1tHn3P2tOP5wTQaZc1qg6M") + + it("should return all audiences") { + expect(jwt.audience).to(equal(["https://samples.auth0.com", "https://api.samples.auth0.com"])) + } + } + + it("should return issued at") { + expect(jwt.issuedAt).to(equal(NSDate(timeIntervalSince1970: 1372638336))) + } + + it("should return not before") { + expect(jwt.notBefore).to(equal(NSDate(timeIntervalSince1970: 1372638336))) + } + + it("should return jwt id") { + expect(jwt.identifier).to(equal("qwerty123456")) + } + } + describe("custom claim") { beforeEach { @@ -125,22 +164,5 @@ class JWTDecodeSpec: QuickSpec { } } } - - describe("JWTDecoder") { - - let decoder = nonExpiredJWT() - - it("should check is not expired") { - expect(decoder.expired).to(beFalsy()) - } - - it("should return the payload") { - expect(decoder.body).toNot(raiseException()) - } - - it("should return exp date") { - expect(decoder.expiresAt).toNot(beNil()) - } - } } } From 17b53d4a2017ea3b271fae526e79fad0f54327dd Mon Sep 17 00:00:00 2001 From: Hernan Zalazar Date: Wed, 9 Sep 2015 14:53:40 -0700 Subject: [PATCH 06/12] Moved around functions and refactored errors --- JWTDecode.playground/Contents.swift | 9 +++--- JWTDecode.xcodeproj/project.pbxproj | 12 ++++++++ JWTDecode/Errors.swift | 47 +++++++++++++++++++++++++++++ JWTDecode/JWT.swift | 45 +++++++++++++++++++++++++++ JWTDecode/JWTDecode.swift | 40 ++++++------------------ JWTDecodeTests/JWTDecodeSpec.swift | 22 ++++++++++++-- 6 files changed, 136 insertions(+), 39 deletions(-) create mode 100644 JWTDecode/Errors.swift create mode 100644 JWTDecode/JWT.swift diff --git a/JWTDecode.playground/Contents.swift b/JWTDecode.playground/Contents.swift index 76aee9e..56c2dd1 100644 --- a/JWTDecode.playground/Contents.swift +++ b/JWTDecode.playground/Contents.swift @@ -4,17 +4,16 @@ import JWTDecode let id_token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImV4cCI6MTQ0MTM5NjgwMH0.fJHsc1QnBioH9mCJwA5yltDrxlYaIAadgmVWXPy7FXk" - do { let jwt = try decode(id_token) jwt.expiresAt - jwt.payload + jwt.body jwt.signature if let admin: Bool = jwt.claim("admin") { admin } -} catch let error as NSError { - error.localizedDescription +} catch let error { + print(error) } -let decoded = try A0JWT.decode(id_token) \ No newline at end of file +let decoded = try A0JWT.decode(id_token) diff --git a/JWTDecode.xcodeproj/project.pbxproj b/JWTDecode.xcodeproj/project.pbxproj index 77d363a..4d8101d 100644 --- a/JWTDecode.xcodeproj/project.pbxproj +++ b/JWTDecode.xcodeproj/project.pbxproj @@ -24,6 +24,10 @@ 5F8B436A1B9F99B400A0D5AE /* A0JWT.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F8B43681B9F99B400A0D5AE /* A0JWT.swift */; settings = {ASSET_TAGS = (); }; }; 5F8B436C1B9F9EDB00A0D5AE /* JWTHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F8B436B1B9F9EDB00A0D5AE /* JWTHelper.swift */; settings = {ASSET_TAGS = (); }; }; 5F8B436D1B9F9EDB00A0D5AE /* JWTHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F8B436B1B9F9EDB00A0D5AE /* JWTHelper.swift */; settings = {ASSET_TAGS = (); }; }; + 5FE49DCD1BA0D5F700DE57D3 /* JWT.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FE49DCC1BA0D5F700DE57D3 /* JWT.swift */; settings = {ASSET_TAGS = (); }; }; + 5FE49DCE1BA0D5F700DE57D3 /* JWT.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FE49DCC1BA0D5F700DE57D3 /* JWT.swift */; settings = {ASSET_TAGS = (); }; }; + 5FE49DD01BA0D66F00DE57D3 /* Errors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FE49DCF1BA0D66F00DE57D3 /* Errors.swift */; settings = {ASSET_TAGS = (); }; }; + 5FE49DD11BA0D66F00DE57D3 /* Errors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FE49DCF1BA0D66F00DE57D3 /* Errors.swift */; settings = {ASSET_TAGS = (); }; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -60,6 +64,8 @@ 5F0A5E291B9A6D4A005289CF /* JWTDecode.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; path = JWTDecode.playground; sourceTree = SOURCE_ROOT; }; 5F8B43681B9F99B400A0D5AE /* A0JWT.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = A0JWT.swift; sourceTree = ""; }; 5F8B436B1B9F9EDB00A0D5AE /* JWTHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JWTHelper.swift; sourceTree = ""; }; + 5FE49DCC1BA0D5F700DE57D3 /* JWT.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JWT.swift; sourceTree = ""; }; + 5FE49DCF1BA0D66F00DE57D3 /* Errors.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Errors.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -128,6 +134,8 @@ 5F0068E51B3B46240048928E /* Supporting Files */, 5F8B43681B9F99B400A0D5AE /* A0JWT.swift */, 5F0069021B3B511F0048928E /* JWTDecode.swift */, + 5FE49DCC1BA0D5F700DE57D3 /* JWT.swift */, + 5FE49DCF1BA0D66F00DE57D3 /* Errors.swift */, ); path = JWTDecode; sourceTree = ""; @@ -336,8 +344,10 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 5FE49DD01BA0D66F00DE57D3 /* Errors.swift in Sources */, 5F8B43691B9F99B400A0D5AE /* A0JWT.swift in Sources */, 5F0069031B3B511F0048928E /* JWTDecode.swift in Sources */, + 5FE49DCD1BA0D5F700DE57D3 /* JWT.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -355,8 +365,10 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 5FE49DD11BA0D66F00DE57D3 /* Errors.swift in Sources */, 5F8B436A1B9F99B400A0D5AE /* A0JWT.swift in Sources */, 5F00693E1B3C7B930048928E /* JWTDecode.swift in Sources */, + 5FE49DCE1BA0D5F700DE57D3 /* JWT.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/JWTDecode/Errors.swift b/JWTDecode/Errors.swift new file mode 100644 index 0000000..104de64 --- /dev/null +++ b/JWTDecode/Errors.swift @@ -0,0 +1,47 @@ +// Errors.swift +// +// Copyright (c) 2015 Auth0 (http://auth0.com) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +public enum DecodeErrorCode: Int { + case InvalidBase64UrlValue + case InvalidJSONValue + case InvalidPartCount +} + +private let ErrorDomain = "com.auth0.JWTDecode" + +private func errorWithCode(code: DecodeErrorCode, description: String) -> NSError { + return NSError(domain: ErrorDomain, code: code.rawValue, userInfo: [NSLocalizedDescriptionKey: description]) +} + +func invalidPartCountInJWT(jwt: String, parts: Int) -> ErrorType { + return errorWithCode(.InvalidPartCount, description: NSLocalizedString("Malformed jwt token \(jwt) has \(parts) parts when it should have 3 parts", comment: "Invalid amount of jwt parts")) +} + +func invalidBase64UrlValue(value: String) -> ErrorType { + return errorWithCode(.InvalidBase64UrlValue, description: NSLocalizedString("Malformed jwt token, failed to decode base64Url value \(value)", comment: "Invalid JWT token base64Url value")) +} + +func invalidJSONValue(value: String) -> ErrorType { + return errorWithCode(.InvalidJSONValue, description: NSLocalizedString("Malformed jwt token, failed to parse JSON value from base64Url \(value)", comment: "Invalid JSON value inside base64Url")) +} \ No newline at end of file diff --git a/JWTDecode/JWT.swift b/JWTDecode/JWT.swift new file mode 100644 index 0000000..52d7a2a --- /dev/null +++ b/JWTDecode/JWT.swift @@ -0,0 +1,45 @@ +// JWT.swift +// +// Copyright (c) 2015 Auth0 (http://auth0.com) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +public protocol JWT { + var header: [String: AnyObject] { get } + var body: [String: AnyObject] { get } + var signature: String? { get } + + var expiresAt: NSDate? { get } + var issuer: String? { get } + var subject: String? { get } + var audience: [String]? { get } + var issuedAt: NSDate? { get } + var notBefore: NSDate? { get } + var identifier: String? { get } + + var expired: Bool { get } +} + +public extension JWT { + public func claim(name: String) -> T? { + return self.body[name] as? T + } +} \ No newline at end of file diff --git a/JWTDecode/JWTDecode.swift b/JWTDecode/JWTDecode.swift index f3e1605..ddc0119 100644 --- a/JWTDecode/JWTDecode.swift +++ b/JWTDecode/JWTDecode.swift @@ -26,28 +26,6 @@ public func decode(jwt: String) throws -> JWT { return try DecodedJWT(jwt: jwt) } -public protocol JWT { - var header: [String: AnyObject] { get } - var body: [String: AnyObject] { get } - var signature: String? { get } - - var expiresAt: NSDate? { get } - var issuer: String? { get } - var subject: String? { get } - var audience: [String]? { get } - var issuedAt: NSDate? { get } - var notBefore: NSDate? { get } - var identifier: String? { get } - - var expired: Bool { get } -} - -public extension JWT { - public func claim(name: String) -> T? { - return self.body[name] as? T - } -} - struct DecodedJWT: JWT { let header: [String: AnyObject] @@ -57,7 +35,7 @@ struct DecodedJWT: JWT { init(jwt: String) throws { let parts = jwt.componentsSeparatedByString(".") guard parts.count == 3 else { - throw errorWithDescription(NSLocalizedString("Malformed jwt token \(jwt) only has \(parts.count) parts (3 parts are required)", comment: "Not enough jwt parts")) + throw invalidPartCountInJWT(jwt, parts: parts.count) } self.header = try decodeJWTPart(parts[0]) @@ -110,15 +88,15 @@ private func base64UrlDecode(value: String) -> NSData? { private func decodeJWTPart(value: String) throws -> [String: AnyObject] { guard let bodyData = base64UrlDecode(value) else { - throw errorWithDescription(NSLocalizedString("Malformed jwt token, failed to decode base64Url value \(value)", comment: "Invalid JWT token base64Url value")) + throw invalidBase64UrlValue(value) } - guard let json = try NSJSONSerialization.JSONObjectWithData(bodyData, options: NSJSONReadingOptions()) as? [String: AnyObject] else { - throw errorWithDescription(NSLocalizedString("Malformed jwt token, failed to parse JSON value from base64Url \(value)", comment: "Invalid JSON value inside base64Url")) + do { + guard let json = try NSJSONSerialization.JSONObjectWithData(bodyData, options: NSJSONReadingOptions()) as? [String: AnyObject] else { + throw invalidJSONValue(value) + } + return json + } catch { + throw invalidJSONValue(value) } - return json -} - -private func errorWithDescription(description: String) -> NSError { - return NSError(domain: "com.auth0.JWTDecode", code: 0, userInfo: [NSLocalizedDescriptionKey: description]) } \ No newline at end of file diff --git a/JWTDecodeTests/JWTDecodeSpec.swift b/JWTDecodeTests/JWTDecodeSpec.swift index ffdff86..e60079a 100644 --- a/JWTDecodeTests/JWTDecodeSpec.swift +++ b/JWTDecodeTests/JWTDecodeSpec.swift @@ -56,12 +56,18 @@ class JWTDecodeSpec: QuickSpec { expect(try! decode(jwt)).toNot(beNil()) } - it("should raise exception with invalid jwt") { - expect { try decode("HEADER.BODY.SIGNATURE") }.to(throwError()) + it("should raise exception with invalid json in jwt") { + expect { try decode("HEADER.BODY.SIGNATURE") } + .to(throwError { (error: ErrorType) in + expect(error).to(beDecodeErrorWithCode(.InvalidJSONValue)) + }) } it("should raise exception with missing parts") { - expect { try decode("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJzdWIifQ") }.to(throwError()) + expect { try decode("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJzdWIifQ") } + .to(throwError { (error: ErrorType) in + expect(error).to(beDecodeErrorWithCode(.InvalidPartCount)) + }) } } @@ -166,3 +172,13 @@ class JWTDecodeSpec: QuickSpec { } } } + +public func beDecodeErrorWithCode(code: DecodeErrorCode) -> NonNilMatcherFunc { + return NonNilMatcherFunc { (actualExpression, failureMessage) throws in + failureMessage.postfixMessage = "be decode error with code <\(code)>" + guard let actual = try actualExpression.evaluate() else { + return false + } + return actual._domain == "com.auth0.JWTDecode" && actual._code == code.rawValue + } +} From 9d9137945cd49e74baadd62748dcadc684f5aee1 Mon Sep 17 00:00:00 2001 From: Hernan Zalazar Date: Wed, 9 Sep 2015 16:04:36 -0700 Subject: [PATCH 07/12] Formatted xcplayground --- JWTDecode.playground/Contents.swift | 59 ++++++++++++++++++---- JWTDecode.playground/contents.xcplayground | 2 +- JWTDecode.playground/timeline.xctimeline | 35 +++++++++++++ JWTDecode.xcodeproj/project.pbxproj | 4 +- JWTDecode/JWTDecode.swift | 5 +- 5 files changed, 88 insertions(+), 17 deletions(-) diff --git a/JWTDecode.playground/Contents.swift b/JWTDecode.playground/Contents.swift index 56c2dd1..cb076f3 100644 --- a/JWTDecode.playground/Contents.swift +++ b/JWTDecode.playground/Contents.swift @@ -1,19 +1,56 @@ -//: Playground - noun: a place where people can play +/*: +# JWTDecode.swift +A swift framework to help you decode a [JWT](http://jwt.io) token in your iOS/OSX applications +*/ +//: First we need to import JWTDecode framework import JWTDecode -let id_token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImV4cCI6MTQ0MTM5NjgwMH0.fJHsc1QnBioH9mCJwA5yltDrxlYaIAadgmVWXPy7FXk" +/*: +Then paste here the token you wish to decode +*/ +let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3NhbXBsZXMuYXV0aDAuY29tIiwic3ViIjoiYXV0aDB8MTAxMDEwMTAxMCIsImF1ZCI6Imh0dHBzOi8vc2FtcGxlcy5hdXRoMC5jb20iLCJleHAiOjEzNzI2NzQzMzYsImlhdCI6MTM3MjYzODMzNiwianRpIjoicXdlcnR5MTIzNDU2IiwibmJmIjoxMzcyNjM4MzM2fQ.LvF9wSheCB5xarpydmurWgi9NOZkdES5AbNb_UWk9Ew" +//: You can generate a new token in [jwt.io](http://jwt.io) +/*: +## Decode +Then here we try to decode it calling `decode(token: String) -> JWT` that will return an object with all the decoded values +*/ do { - let jwt = try decode(id_token) - jwt.expiresAt + let jwt = try decode(token) + +//: ### JWT parts + +//: Header dictionary + jwt.header +//: Claims in token body jwt.body +//: Token signature jwt.signature - if let admin: Bool = jwt.claim("admin") { - admin - } -} catch let error { - print(error) -} -let decoded = try A0JWT.decode(id_token) +//: ### Registered Claims + +//: "aud" (Audience) + jwt.audience +//: "sub" (Subject) + jwt.subject +//: "jti" (JWT ID) + jwt.identifier +//: "iss" (Issuer) + jwt.issuer +//: "nbf" (Not Before) + jwt.notBefore +//: "iat" (Issued At) + jwt.issuedAt +//: "exp" (Expiration Time) + jwt.expiresAt + +//: ### Custom Claims +//: If we also have our custom claims we can retrive them calling `claim(name: String) -> T?` where `T` is the value type of the claim, e.g.: a `String` + + let custom: String? = jwt.claim("email") +//: ### Error Handling +//: If the token is invalid an `NSError` will be thrown +} catch let error as NSError { + error.localizedDescription +} diff --git a/JWTDecode.playground/contents.xcplayground b/JWTDecode.playground/contents.xcplayground index ee7c14f..af4f38c 100644 --- a/JWTDecode.playground/contents.xcplayground +++ b/JWTDecode.playground/contents.xcplayground @@ -1,4 +1,4 @@ - + \ No newline at end of file diff --git a/JWTDecode.playground/timeline.xctimeline b/JWTDecode.playground/timeline.xctimeline index bf468af..210b22f 100644 --- a/JWTDecode.playground/timeline.xctimeline +++ b/JWTDecode.playground/timeline.xctimeline @@ -2,5 +2,40 @@ + + + + + + + + + + + + + + diff --git a/JWTDecode.xcodeproj/project.pbxproj b/JWTDecode.xcodeproj/project.pbxproj index 4d8101d..4a41b12 100644 --- a/JWTDecode.xcodeproj/project.pbxproj +++ b/JWTDecode.xcodeproj/project.pbxproj @@ -111,6 +111,7 @@ children = ( 5F0068E41B3B46240048928E /* JWTDecode */, 5F0068F11B3B46240048928E /* JWTDecodeTests */, + 5F0A5E291B9A6D4A005289CF /* JWTDecode.playground */, 5F0068E31B3B46240048928E /* Products */, ); sourceTree = ""; @@ -129,13 +130,12 @@ 5F0068E41B3B46240048928E /* JWTDecode */ = { isa = PBXGroup; children = ( - 5F0A5E291B9A6D4A005289CF /* JWTDecode.playground */, 5F0068E71B3B46240048928E /* JWTDecode.h */, - 5F0068E51B3B46240048928E /* Supporting Files */, 5F8B43681B9F99B400A0D5AE /* A0JWT.swift */, 5F0069021B3B511F0048928E /* JWTDecode.swift */, 5FE49DCC1BA0D5F700DE57D3 /* JWT.swift */, 5FE49DCF1BA0D66F00DE57D3 /* Errors.swift */, + 5F0068E51B3B46240048928E /* Supporting Files */, ); path = JWTDecode; sourceTree = ""; diff --git a/JWTDecode/JWTDecode.swift b/JWTDecode/JWTDecode.swift index ddc0119..86be1db 100644 --- a/JWTDecode/JWTDecode.swift +++ b/JWTDecode/JWTDecode.swift @@ -57,11 +57,10 @@ struct DecodedJWT: JWT { var identifier: String? { return claim("jti") } private func claim(name: String) -> NSDate? { - if let timestamp:Double = claim(name) { - return NSDate(timeIntervalSince1970: timestamp) - } else { + guard let timestamp:Double = claim(name) else { return nil } + return NSDate(timeIntervalSince1970: timestamp) } var expired: Bool { From ea6ba9132a43b446cf03426895fe8dc91cb83945 Mon Sep 17 00:00:00 2001 From: Hernan Zalazar Date: Thu, 10 Sep 2015 12:58:52 -0700 Subject: [PATCH 08/12] Add a bit of documentation --- JWTDecode/A0JWT.swift | 12 ++++++++++++ JWTDecode/Errors.swift | 7 +++++++ JWTDecode/JWT.swift | 21 +++++++++++++++++++++ JWTDecode/JWTDecode.swift | 8 ++++++++ 4 files changed, 48 insertions(+) diff --git a/JWTDecode/A0JWT.swift b/JWTDecode/A0JWT.swift index f773903..ebecb94 100644 --- a/JWTDecode/A0JWT.swift +++ b/JWTDecode/A0JWT.swift @@ -22,6 +22,7 @@ import Foundation +/// Class to allow Objective-C code to decode a JWT public class A0JWT: NSObject { var jwt: JWT @@ -30,22 +31,33 @@ public class A0JWT: NSObject { self.jwt = jwt } + /// token header part public var header: [String: AnyObject] { return self.jwt.header } + /// token body part or claims public var body: [String: AnyObject] { return self.jwt.body } + /// token signature part public var signature: String? { return self.jwt.signature } + /// value of the `exp` claim public var expiresAt: NSDate? { return self.jwt.expiresAt } + /** + Creates a new instance of `A0JWT` and decodes the given jwt token. + + :param: jwtValue of the token to decode + + :returns: a new instance of `A0JWT` that holds the decode token + */ public class func decode(jwtValue: String) throws -> A0JWT { let jwt = try DecodedJWT(jwt: jwtValue) return A0JWT(jwt: jwt) diff --git a/JWTDecode/Errors.swift b/JWTDecode/Errors.swift index 104de64..aec2875 100644 --- a/JWTDecode/Errors.swift +++ b/JWTDecode/Errors.swift @@ -22,6 +22,13 @@ import Foundation +/** +JWT decode error codes + +- InvalidBase64UrlValue: when either the header or body parts cannot be base64 decoded +- InvalidJSONValue: when either the header or body decoded values is not a valid JSON object +- InvalidPartCount: when the token doesnt have the required amount of parts (header, body and signature) +*/ public enum DecodeErrorCode: Int { case InvalidBase64UrlValue case InvalidJSONValue diff --git a/JWTDecode/JWT.swift b/JWTDecode/JWT.swift index 52d7a2a..beead65 100644 --- a/JWTDecode/JWT.swift +++ b/JWTDecode/JWT.swift @@ -22,23 +22,44 @@ import Foundation +/** +* Protocol that defines what a decoded JWT token should be. +*/ public protocol JWT { + /// token header part contents var header: [String: AnyObject] { get } + /// token body part values or token claims var body: [String: AnyObject] { get } + /// token signature part var signature: String? { get } + /// value of `exp` claim if available var expiresAt: NSDate? { get } + /// value of `iss` claim if available var issuer: String? { get } + /// value of `sub` claim if available var subject: String? { get } + /// value of `aud` claim if available var audience: [String]? { get } + /// value of `iat` claim if available var issuedAt: NSDate? { get } + /// value of `nbf` claim if available var notBefore: NSDate? { get } + /// value of `jti` claim if available var identifier: String? { get } + /// Checks if the token is currently expired using the `exp` claim. If there is no claim present it will deem the token not expired var expired: Bool { get } } public extension JWT { + /** + Returns a specific claim by its name whose value if of type `T`. + + :param: name of the claim to return + + :returns: the value of the claim as the generic type `T` if available + */ public func claim(name: String) -> T? { return self.body[name] as? T } diff --git a/JWTDecode/JWTDecode.swift b/JWTDecode/JWTDecode.swift index 86be1db..3d2dca1 100644 --- a/JWTDecode/JWTDecode.swift +++ b/JWTDecode/JWTDecode.swift @@ -22,6 +22,14 @@ import Foundation +/** +Decodes a JWT token into an object that holds the decoded body (along with token header and signature parts). +If the token cannot be decoded a `NSError` will be thrown. + +:param: jwt string value to decode + +:returns: a decoded token as an instance of JWT +*/ public func decode(jwt: String) throws -> JWT { return try DecodedJWT(jwt: jwt) } From e1532272cd8a223e2a08cdbcec235cf055376f52 Mon Sep 17 00:00:00 2001 From: Hernan Zalazar Date: Mon, 14 Sep 2015 09:26:36 -0700 Subject: [PATCH 09/12] Use Xcode 7 image from TravisCI --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index c10b8a0..e45760b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ reference: http://www.objc.io/issue-6/travis-ci.html language: objective-c -osx_image: xcode6.4 +osx_image: xcode7 before_install: true install: true git: From a8514d8f1a0611ead60a540eba5aaf3dcc5022ab Mon Sep 17 00:00:00 2001 From: Hernan Zalazar Date: Tue, 15 Sep 2015 12:52:45 -0400 Subject: [PATCH 10/12] Use latest xctool due to issue with GM --- .travis.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index e45760b..2ef54f1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,9 @@ before_install: true install: true git: submodules: false -script: script/cibuild +script: + - brew upgrade xctool --HEAD + - script/cibuild branches: only: - master From 6dff80abfefa239d94881a286c49ebfcee3ac326 Mon Sep 17 00:00:00 2001 From: Hernan Zalazar Date: Tue, 15 Sep 2015 18:41:10 -0400 Subject: [PATCH 11/12] Changed build scripts Avoid relying in xctool --- script/.env | 4 + script/LICENSE.md | 18 --- script/README.md | 82 ------------- script/bootstrap | 230 +++++++++++++++++++++++++++++++---- script/build | 59 +++++++++ script/certificates/.gitkeep | 0 script/cibuild | 194 +++++++++-------------------- script/coverage | 27 ++++ script/git_hooks/pre-push | 27 ++++ script/schemes.awk | 10 -- script/script_hooks/.gitkeep | 0 script/test | 77 ++++++++++++ script/update | 27 ++++ script/xctool.awk | 25 ---- 14 files changed, 486 insertions(+), 294 deletions(-) create mode 100644 script/.env delete mode 100644 script/LICENSE.md delete mode 100644 script/README.md create mode 100755 script/build create mode 100644 script/certificates/.gitkeep create mode 100755 script/coverage create mode 100755 script/git_hooks/pre-push delete mode 100644 script/schemes.awk create mode 100644 script/script_hooks/.gitkeep create mode 100755 script/test create mode 100755 script/update delete mode 100644 script/xctool.awk diff --git a/script/.env b/script/.env new file mode 100644 index 0000000..044351b --- /dev/null +++ b/script/.env @@ -0,0 +1,4 @@ + +PROJECT_NAME=JWTDecode +XCODE_WORKSPACE=JWTDecode.xcworkspace +XCODE_PROJECT= diff --git a/script/LICENSE.md b/script/LICENSE.md deleted file mode 100644 index 8d92384..0000000 --- a/script/LICENSE.md +++ /dev/null @@ -1,18 +0,0 @@ -**Copyright (c) 2013 Justin Spahr-Summers** - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/script/README.md b/script/README.md deleted file mode 100644 index f66206f..0000000 --- a/script/README.md +++ /dev/null @@ -1,82 +0,0 @@ -# objc-build-scripts - -This project is a collection of scripts created with two goals: - - 1. To standardize how Objective-C projects are bootstrapped after cloning - 1. To easily build Objective-C projects on continuous integration servers - -## Scripts - -Right now, there are two important scripts: [`bootstrap`](#bootstrap) and -[`cibuild`](#cibuild). Both are Bash scripts, to maximize compatibility and -eliminate pesky system configuration issues (like setting up a working Ruby -environment). - -The structure of the scripts on disk is meant to follow that of a typical Ruby -project: - -``` -script/ - bootstrap - cibuild -``` - -### bootstrap - -This script is responsible for bootstrapping (initializing) your project after -it's been checked out. Here, you should install or clone any dependencies that -are required for a working build and development environment. - -By default, the script will verify that [xctool][] is installed, then initialize -and update submodules recursively. If any submodules contain `script/bootstrap`, -that will be run as well. - -To check that other tools are installed, you can set the `REQUIRED_TOOLS` -environment variable before running `script/bootstrap`, or edit it within the -script directly. Note that no installation is performed automatically, though -this can always be added within your specific project. - -### cibuild - -This script is responsible for building the project, as you would want it built -for continuous integration. This is preferable to putting the logic on the CI -server itself, since it ensures that any changes are versioned along with the -source. - -By default, the script will run [`bootstrap`](#bootstrap), look for any Xcode -workspace or project in the working directory, then build all targets/schemes -(as found by `xcodebuild -list`) using [xctool][]. - -You can also specify the schemes to build by passing them into the script: - -```sh -script/cibuild ReactiveCocoa-Mac ReactiveCocoa-iOS -``` - -As with the `bootstrap` script, there are several environment variables that can -be used to customize behavior. They can be set on the command line before -invoking the script, or the defaults changed within the script directly. - -## Getting Started - -To add the scripts to your project, read the contents of this repository into -a `script` folder: - -``` -$ git remote add objc-build-scripts https://github.com/jspahrsummers/objc-build-scripts.git -$ git fetch objc-build-scripts -$ git read-tree --prefix=script/ -u objc-build-scripts/master -``` - -Then commit the changes, to incorporate the scripts into your own repository's -history. You can also freely tweak the scripts for your specific project's -needs. - -To merge in upstream changes later: - -``` -$ git fetch -p objc-build-scripts -$ git merge --ff --squash -Xsubtree=script objc-build-scripts/master -``` - -[xctool]: https://github.com/facebook/xctool diff --git a/script/bootstrap b/script/bootstrap index d61c863..6350089 100755 --- a/script/bootstrap +++ b/script/bootstrap @@ -1,47 +1,227 @@ #!/bin/bash -export SCRIPT_DIR=$(dirname "$0") +set -e -## -## Bootstrap Process -## - -main () +install_homebrew () { - local submodules=$(git submodule status) - local result=$? + if [ -z $GITHUB_ACCESS_TOKEN ] + then + export HOMEBREW_GITHUB_API_TOKEN=$GITHUB_ACCESS_TOKEN + fi + + if type brew > /dev/null + then + echo " ✔ brew is already installed" + echo "" + echo " → Updating homebrew formulas" + brew update > /dev/null || brew update > /dev/null + echo " ✔ formulas updated" + else + command -v ruby >/dev/null 2>&1 || { echo >&2 "Error: Some ruby of version is required to install homebrew. Aborting"; exit 1; } + ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" + fi +} - if [ "$result" -ne "0" ] +# param $1 formula name +# param $2 [optional] tap path +brew_install () +{ + formula_version=`brew list --versions $1` + if [ -z "$formula_version" ] + then + if [ -z $2 ] then - exit $result + formula_name=$1 + else + formula_name="$2/$1" fi + echo "" + echo " → Installing brew formula $formula_name" + brew install -v $formula_name > /dev/null 2>&1 - if [ -n "$submodules" ] + # Extract version + regexp="^.*([0-9]\.[0-9]\.[0-9]).*$" + installed_version="" + eval "output=\"$(brew info $1)\"" + if [[ $output =~ $regexp ]] then - echo "*** Updating submodules..." - update_submodules + installed_version=${BASH_REMATCH[1]} fi + + echo " ✔ $formula_name $installed_version has been installed" + else + echo " ✔ $1 is already installed" + fi } -bootstrap_submodule () +print_gem_install_cmd () { - local bootstrap="script/bootstrap" - - if [ -e "$bootstrap" ] + regexp="gem ['\"]([a-zA-Z0-9_-]+)['\"](,.*)?" + gems="" + while read -r line + do + if [[ $line =~ $regexp ]] then - echo "*** Bootstrapping $name..." - "$bootstrap" >/dev/null - else - update_submodules + gems="$gems ${BASH_REMATCH[1]}" fi + done < Gemfile + + echo "" + echo " $> 'sudo gem install$gems'" + echo "" +} + +bundle_install () +{ + echo "" + echo " → Installing gems" + echo "" + if type bundle > /dev/null + then + bundle install + else + # TODO ask user if he/she wants the script to try to install + # rbenv, ruby and bundler. + printf "\033[1;33m⚠ WARNING: Ruby gems in Gemfile could not be installed because 'bundler' is not available.\n" \ + "You should install rbenv or rvm and bundler" \ + "or try to install the gems globally by running the following command:" + print_gem_install_cmd + printf "\033[0m" + fi +} + +install_git_hooks () +{ + if [ ! -z "$INSTALL_GITHOOKS" ] + then + echo "" + echo " → Installing git hooks" + echo "" + for hook in script/git_hooks/* + do + cp $hook .git/hooks + echo " ✔ $hook successfully installed" + done + echo "" + fi +} + +bootstrap_carthage () +{ + echo "" + echo " → Bootstrapping Carthage" + echo "" + carthage_cmd="time carthage bootstrap --platform ios" + + if [ "$USE_SSH" == "true" ] + then + carthage_cmd="$carthage_cmd --use-ssh" + fi + if [ "$USE_SUBMODULES" == "true" ] + then + carthage_cmd="$carthage_cmd --use-submodules --no-build" + fi + eval $carthage_cmd +} + +bootstrap_cocoapods () +{ + echo "" + echo " → Bootstrapping Cocoapods" + echo "" + if type bundle > /dev/null && bundle show pod > /dev/null + then + bundle exec pod install + else + pod install + fi +} + +echo_submodule_name () +{ + echo " ✔ $name successfully initialized" } -update_submodules () +init_submodules () { - git submodule sync --quiet && git submodule update --init && git submodule foreach --quiet bootstrap_submodule + echo "" + echo " → Initializing submodules ..." + echo "" + git submodule update --quiet --init --recursive > /dev/null + git submodule foreach --quiet echo_submodule_name +} + +install_dependencies () +{ + echo "" + echo " → Installing dependencies" + echo "" + install_homebrew + brew_install "xcode-coveralls" "macmade/tap" + + if [ -f script/script_hooks/bootstrap ] + then + script/script_hooks/bootstrap + fi +} + +main () +{ + source script/.env + + echo "" + echo " Bootstrapping $PROJECT_NAME" + echo "" + + install_git_hooks + install_dependencies + + if [ -f Cartfile ] + then + brew_install "carthage" + bootstrap_carthage + fi + + if [ -f Gemfile ] + then + bundle_install + fi + + if [ -f Podfile ] + then + bootstrap_cocoapods + fi + + if [ -f .gitmodules ] + then + init_submodules + fi + + open_file_name="" + if [ -z "$XCODE_WORKSPACE" ] + then + open_file_name=$XCODE_PROJECT + else + open_file_name=$XCODE_WORKSPACE + fi + + echo "" + echo " $PROJECT_NAME successfully bootstrapped" + echo "" + echo " Usefull scripts:" + echo "" + echo " * 'script/test' to run tests." + echo " * 'script/build' to build the project." + echo " * 'script/update' to update project's dependencies." + echo "" + echo " You can start hacking by executing:" + echo "" + echo " open $open_file_name" + echo "" } -export -f bootstrap_submodule -export -f update_submodules +export -f init_submodules +export -f echo_submodule_name +export -f brew_install main diff --git a/script/build b/script/build new file mode 100755 index 0000000..086429c --- /dev/null +++ b/script/build @@ -0,0 +1,59 @@ +#!/bin/bash + +set -e + +build_using_carthage () +{ + carthage_cmd="carthage build --no-skip-current --platform ios" + if [ "$USE_SSH" == "true" ] + then + carthage_cmd="$carthage_cmd --use-ssh" + fi + eval $carthage_cmd +} + +build_using_xcodebuild () +{ + build_command="set -o pipefail && xcodebuild -scheme $1" + if [ -z "$XCODE_WORKSPACE" ] + then + build_command="$build_command -project $XCODE_PROJECT" + else + build_command="$build_command -workspace $XCODE_WORKSPACE" + fi + build_command="$build_command build" + + if type bundle > /dev/null && bundle show xcpretty > /dev/null + then + build_command="$build_command | xcpretty -c" + fi + + echo "" + echo " → Building scheme '$1'" + echo "" + eval $build_command +} + +schemes () +{ + xcodebuild -list | awk '{if(found) print} /Schemes/{found=1}' | awk '{$1=$1};1' +} + +if [ ! -f $XCODE_WORKSPACE ] && [ -f Cartfile ] && type carthage > /dev/null +then + build_using_carthage +else + source script/.env + + if [ -z "$current_schemes" ] + then + echo "" + echo "ERROR: There are no schemes. Probably you forgot to share your schemes" + exit 1 + fi + + for scheme in $current_schemes + do + build_using_xcodebuild $scheme + done +fi diff --git a/script/certificates/.gitkeep b/script/certificates/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/script/cibuild b/script/cibuild index 33b5129..6dd943b 100755 --- a/script/cibuild +++ b/script/cibuild @@ -6,51 +6,8 @@ export SCRIPT_DIR=$(dirname "$0") ## Configuration Variables ## -SCHEMES="$@" - -config () -{ - # The workspace to build. - # - # If not set and no workspace is found, the -workspace flag will not be passed - # to `xctool`. - # - # Only one of `XCWORKSPACE` and `XCODEPROJ` needs to be set. The former will - # take precedence. - : ${XCWORKSPACE=$(find_pattern "*.xcworkspace")} - - # The project to build. - # - # If not set and no project is found, the -project flag will not be passed - # to `xctool`. - # - # Only one of `XCWORKSPACE` and `XCODEPROJ` needs to be set. The former will - # take precedence. - : ${XCODEPROJ=$(find_pattern "*.xcodeproj")} - - # A bootstrap script to run before building. - # - # If this file does not exist, it is not considered an error. - : ${BOOTSTRAP="$SCRIPT_DIR/bootstrap"} - - # Extra options to pass to xctool. - : ${XCTOOL_OPTIONS="RUN_CLANG_STATIC_ANALYZER=NO"} - - # A whitespace-separated list of default schemes to build. - # - # Individual names can be quoted to avoid word splitting. - : ${SCHEMES:=$(xcodebuild -list -project "$XCODEPROJ" 2>/dev/null | awk -f "$SCRIPT_DIR/schemes.awk")} - - # A whitespace-separated list of executables that must be present and locatable. - : ${REQUIRED_TOOLS="xctool"} - - export XCWORKSPACE - export XCODEPROJ - export BOOTSTRAP - export XCTOOL_OPTIONS - export SCHEMES - export REQUIRED_TOOLS -} +# The name of the keychain to create for iOS code signing. +KEYCHAIN=ios-build.keychain ## ## Build Process @@ -58,116 +15,85 @@ config () main () { - config - - if [ -n "$REQUIRED_TOOLS" ] + if [ -f Cartfile ] then - echo "*** Checking dependencies..." - check_deps + echo "" + echo "####### Importing Developer Certificates #######" + echo "" + import_certs fi - if [ -f "$BOOTSTRAP" ] - then - echo "*** Bootstrapping..." - "$BOOTSTRAP" || exit $? - fi - - echo "*** The following schemes will be built:" - echo "$SCHEMES" | xargs -n 1 echo " " - echo - - echo "$SCHEMES" | xargs -n 1 | ( - local status=0 - - while read scheme - do - build_scheme "$scheme" || status=1 - done + echo "" + echo "####### Bootstrap Phase #######" + echo "" + script/bootstrap + local status=$? - exit $status - ) -} - -check_deps () -{ - for tool in $REQUIRED_TOOLS - do - which -s "$tool" - if [ "$?" -ne "0" ] + if [ $status -eq 0 ] + then + echo "" + echo "" + echo "####### Build & Test Phase #######" + echo "" + script/test | tee /tmp/build.test-output.txt + status=$? + if [ ! $status -eq 0 ] + then + log_file_path=`cat /tmp/build.test-output.txt | perl -l -ne '/(\/var\/folders.*\/com\.apple\.dt\.XCTest-status.*)\)/ && print $1'` + if [ ! -z "$log_file_path" ] then - echo "*** Error: $tool not found. Please install it and cibuild again." - exit 1 + echo "" + echo " → The tests have failed. Printing output of log file '$log_file_path'." + cat $log_file_path + echo "" fi - done -} + fi + fi -find_pattern () -{ - ls -d $1 2>/dev/null | head -n 1 + delete_keychain + exit $status } -run_xctool () +import_certs () { - if [ -n "$XCWORKSPACE" ] - then - xctool -workspace "$XCWORKSPACE" $XCTOOL_OPTIONS "$@" 2>&1 - elif [ -n "$XCODEPROJ" ] + # If this environment variable is missing, we must not be running on Travis. + if [ -z "$KEY_PASSWORD" ] then - xctool -project "$XCODEPROJ" $XCTOOL_OPTIONS "$@" 2>&1 - else - echo "*** No workspace or project file found." - exit 1 + return 0 fi -} - -parse_build () -{ - awk -f "$SCRIPT_DIR/xctool.awk" 2>&1 >/dev/null -} - -build_scheme () -{ - local scheme=$1 - - echo "*** Building and testing $scheme..." - echo - - local sdkflag= - local action=test - # Determine whether we can run unit tests for this target. - run_xctool -scheme "$scheme" run-tests | parse_build + echo " → Setting up code signing..." + local password=cibuild - local awkstatus=$? + # Create a temporary keychain for code signing. + security create-keychain -p "$password" "$KEYCHAIN" + security default-keychain -s "$KEYCHAIN" + security unlock-keychain -p "$password" "$KEYCHAIN" + security set-keychain-settings -t 3600 -l "$KEYCHAIN" - if [ "$awkstatus" -eq "1" ] - then - # SDK not found, try for iphonesimulator. - sdkflag="-sdk iphonesimulator" - - # Determine whether the unit tests will run with iphonesimulator - run_xctool $sdkflag -scheme "$scheme" run-tests | parse_build + # Download the certificate for the Apple Worldwide Developer Relations + # Certificate Authority. + local certpath="$SCRIPT_DIR/apple_wwdr.cer" + curl -s 'https://developer.apple.com/certificationauthority/AppleWWDRCA.cer' > "$certpath" + security import "$certpath" -k "$KEYCHAIN" -T /usr/bin/codesign - awkstatus=$? - - if [ "$awkstatus" -ne "0" ] - then - # Unit tests will not run on iphonesimulator. - sdkflag="" - fi - fi + # Import our development certificate. + security import "$SCRIPT_DIR/certificates/cibot.p12" -k "$KEYCHAIN" -P "$KEY_PASSWORD" -T /usr/bin/codesign +} - if [ "$awkstatus" -ne "0" ] +delete_keychain () +{ + if [ -z "$KEY_PASSWORD" ] then - # Unit tests aren't supported. - action=build + return 0 fi - run_xctool $sdkflag -scheme "$scheme" $action + echo " → Removing temporary keychain" + security delete-keychain "$KEYCHAIN" + echo " ✔ Temporary keychain successfully removed." } -export -f build_scheme -export -f run_xctool -export -f parse_build +export -f import_certs +export -f delete_keychain main diff --git a/script/coverage b/script/coverage new file mode 100755 index 0000000..9dfc95b --- /dev/null +++ b/script/coverage @@ -0,0 +1,27 @@ +#!/bin/bash + +set -e + +source script/.env + +if type xcode-coveralls > /dev/null +then + if [ ! -f script/xcenv.sh ] + then + # Running the test generates the xcenv.sh + script/test + fi + + if [ -f script/xcenv.sh ] + then + source script/xcenv.sh + declare -r DIR_BUILD="${OBJECT_FILE_DIR_normal}/${CURRENT_ARCH}/" + xcode-coveralls --include $SRCROOT --exclude "$SRCROOT""Tests" --exclude Carthage --exclude Pods --token $COVERALLS_TOKEN "${DIR_BUILD}" + else + # TODO print instruction of how to add the generation of xcenv.sh + echo "" + echo " Error: script/xcenv.sh was not generated after running 'script/test'." + echo "" + exit 1 + fi +fi diff --git a/script/git_hooks/pre-push b/script/git_hooks/pre-push new file mode 100755 index 0000000..8f1423d --- /dev/null +++ b/script/git_hooks/pre-push @@ -0,0 +1,27 @@ +#!/bin/sh + +# An example hook script to verify what is about to be pushed. Called by "git +# push" after it has checked the remote status, but before anything has been +# pushed. If this script exits with a non-zero status nothing will be pushed. +# +# This hook is called with the following parameters: +# +# $1 -- Name of the remote to which the push is being done +# $2 -- URL to which the push is being done +# +# If pushing without using a named remote those arguments will be equal. +# +# Information about the commits which are being pushed is supplied as lines to +# the standard input in the form: +# +# +# +# This sample shows how to prevent push of commits where the log message starts +# with "WIP" (work in progress). + +set -e + +remote="$1" +url="$2" + +script/test diff --git a/script/schemes.awk b/script/schemes.awk deleted file mode 100644 index 4c94df9..0000000 --- a/script/schemes.awk +++ /dev/null @@ -1,10 +0,0 @@ -BEGIN { - FS = "\n"; -} - -/Schemes:/ { - while (getline && $0 != "") { - sub(/^ +/, ""); - print "'" $0 "'"; - } -} diff --git a/script/script_hooks/.gitkeep b/script/script_hooks/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/script/test b/script/test new file mode 100755 index 0000000..5b7951e --- /dev/null +++ b/script/test @@ -0,0 +1,77 @@ +#!/bin/bash + +set -e + +source script/.env + +schemes () +{ + xcodebuild -list | awk '{if(found) print} /Schemes/{found=1}' | awk '{$1=$1};1' +} + +run_tests () +{ + test_command="set -o pipefail && xcodebuild -scheme $1" + if [ -z "$XCODE_WORKSPACE" ] + then + test_command="$test_command -project $XCODE_PROJECT" + else + test_command="$test_command -workspace $XCODE_WORKSPACE" + fi + test_command="$test_command test" + + shopt -s nocasematch + + case "$1" in + + *iOS) test_command="$test_command -destination 'platform=iOS Simulator,name=iPhone 6'" + ;; + *OSX) test_command="$test_command -destination 'platform=OS X'" + ;; + +esac + if [ ! -z "$CIRCLE_ARTIFACTS" ] + then + test_command="$test_command | tee $CIRCLE_ARTIFACTS/xcode_raw.log" + fi + if type bundle > /dev/null && bundle show xcpretty > /dev/null + then + test_command="$test_command | bundle exec xcpretty -c" + if [ ! -z "$CIRCLE_TEST_REPORTS" ] + then + test_command="$test_command --report junit --output $CIRCLE_TEST_REPORTS/xcode/results.xml" + fi + fi + + echo "" + echo " → Running tests for scheme '$1'" + echo "" + eval $test_command +} + +current_schemes=$(schemes) +if [ -z "$current_schemes" ] +then + echo "" + echo "ERROR: There are no schemes. Probably you forgot to share your schemes" + exit 1 +fi + +for scheme in $current_schemes +do + run_tests $scheme +done + +if [ -f "$PROJECT_NAME.podspec" ] +then + echo "" + echo " → Linting $PROJECT_NAME.podspec" + echo "" + if type bundle > /dev/null && bundle show pod > /dev/null + then + bundle exec pod lib lint + elif type pod > /dev/null + then + pod lib lint + fi +fi diff --git a/script/update b/script/update new file mode 100755 index 0000000..751db3b --- /dev/null +++ b/script/update @@ -0,0 +1,27 @@ +#!/bin/bash + +set -e + +source script/.env + +if [ -f Cartfile ] && type carthage > /dev/null +then + carthage_cmd="carthage update --platform ios" + if [ "$USE_SSH" == "true" ] + then + carthage_cmd="$carthage_cmd --use-ssh" + fi + if [ "$USE_SUBMODULES" == "true" ] + then + carthage_cmd="$carthage_cmd --use-submodules --no-build" + fi + eval $carthage_cmd +elif [ -f Podfile ] +then + if type bundle > /dev/null && bundle show pod > /dev/null + then + bundle exec pod update + else + pod update + fi +fi diff --git a/script/xctool.awk b/script/xctool.awk deleted file mode 100644 index f613258..0000000 --- a/script/xctool.awk +++ /dev/null @@ -1,25 +0,0 @@ -# Exit statuses: -# -# 0 - No errors found. -# 1 - Wrong SDK. Retry with SDK `iphonesimulator`. -# 2 - Missing target. - -BEGIN { - status = 0; -} - -{ - print; -} - -/Testing with the '(.+)' SDK is not yet supported/ { - status = 1; -} - -/does not contain a target named/ { - status = 2; -} - -END { - exit status; -} From b41513e08d95b44651fff2ae41778fc569e4d3b1 Mon Sep 17 00:00:00 2001 From: Hernan Zalazar Date: Tue, 15 Sep 2015 18:53:42 -0400 Subject: [PATCH 12/12] Remove xctool install and add xcpretty --- .travis.yml | 1 - Gemfile | 3 +++ Gemfile.lock | 10 ++++++++++ 3 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 Gemfile create mode 100644 Gemfile.lock diff --git a/.travis.yml b/.travis.yml index 2ef54f1..6c97740 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,7 +6,6 @@ install: true git: submodules: false script: - - brew upgrade xctool --HEAD - script/cibuild branches: only: diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..14e1439 --- /dev/null +++ b/Gemfile @@ -0,0 +1,3 @@ +source 'https://rubygems.org' + +gem 'xcpretty' \ No newline at end of file diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..ae316f6 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,10 @@ +GEM + remote: https://rubygems.org/ + specs: + xcpretty (0.1.12) + +PLATFORMS + ruby + +DEPENDENCIES + xcpretty