diff --git a/SPiDExampleApp/SPiDExampleAppDelegate.m b/SPiDExampleApp/SPiDExampleAppDelegate.m index 55cf4c8..08fd824 100644 --- a/SPiDExampleApp/SPiDExampleAppDelegate.m +++ b/SPiDExampleApp/SPiDExampleAppDelegate.m @@ -25,7 +25,8 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( [SPiDClient setClientID:ClientID clientSecret:ClientSecret appURLScheme:AppURLScheme - serverURL:[NSURL URLWithString:ServerURL]]; + serverURL:[NSURL URLWithString:ServerURL] + tokenStorageMode:SPiDTokenStorageModeDefault]; [[SPiDClient sharedInstance] setWebViewInitialHTML:@"Loading SPiD login page"]; [self setUseWebView:YES]; // As default, logout as logged in through webview diff --git a/SPiDFacebookApp/SPiDFacebookAppDelegate.m b/SPiDFacebookApp/SPiDFacebookAppDelegate.m index 6084c08..94c6ea7 100644 --- a/SPiDFacebookApp/SPiDFacebookAppDelegate.m +++ b/SPiDFacebookApp/SPiDFacebookAppDelegate.m @@ -33,7 +33,8 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( [SPiDClient setClientID:ClientID clientSecret:ClientSecret appURLScheme:AppURLScheme - serverURL:[NSURL URLWithString:ServerURL]]; + serverURL:[NSURL URLWithString:ServerURL] + tokenStorageMode:SPiDTokenStorageModeDefault]; [[SPiDClient sharedInstance] setSignSecret:SignSecret]; MainViewController *mainViewController = [[MainViewController alloc] init]; diff --git a/SPiDHybridApp/SPiDHybridAppDelegate.m b/SPiDHybridApp/SPiDHybridAppDelegate.m index 170872e..d546fe7 100644 --- a/SPiDHybridApp/SPiDHybridAppDelegate.m +++ b/SPiDHybridApp/SPiDHybridAppDelegate.m @@ -27,7 +27,8 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( [SPiDClient setClientID:ClientID clientSecret:ClientSecret appURLScheme:AppURLScheme - serverURL:[NSURL URLWithString:ServerURL]]; + serverURL:[NSURL URLWithString:ServerURL] + tokenStorageMode:SPiDTokenStorageModeDefault]; [[SPiDClient sharedInstance] setServerClientID:ServerClientID]; [[SPiDClient sharedInstance] setServerRedirectUri:[NSURL URLWithString:ServerRedirectURI]]; diff --git a/SPiDNativeApp/SPiDNativeAppDelegate.m b/SPiDNativeApp/SPiDNativeAppDelegate.m index f917b8d..b5a6c72 100644 --- a/SPiDNativeApp/SPiDNativeAppDelegate.m +++ b/SPiDNativeApp/SPiDNativeAppDelegate.m @@ -27,7 +27,8 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( [SPiDClient setClientID:ClientID clientSecret:ClientSecret appURLScheme:AppURLScheme - serverURL:[NSURL URLWithString:ServerURL]]; + serverURL:[NSURL URLWithString:ServerURL] + tokenStorageMode:SPiDTokenStorageModeDefault]; MainViewController *mainViewController = [[MainViewController alloc] init]; self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; diff --git a/SPiDSDK.xcodeproj/project.pbxproj b/SPiDSDK.xcodeproj/project.pbxproj index 6291b3b..5c2c028 100644 --- a/SPiDSDK.xcodeproj/project.pbxproj +++ b/SPiDSDK.xcodeproj/project.pbxproj @@ -21,6 +21,19 @@ /* End PBXAggregateTarget section */ /* Begin PBXBuildFile section */ + 089318561E6EFAD6002CD789 /* SPiDTokenStorage.h in Headers */ = {isa = PBXBuildFile; fileRef = 089318541E6EFAD6002CD789 /* SPiDTokenStorage.h */; }; + 089318571E6EFAD6002CD789 /* SPiDTokenStorage.m in Sources */ = {isa = PBXBuildFile; fileRef = 089318551E6EFAD6002CD789 /* SPiDTokenStorage.m */; }; + 0893185A1E6EFD52002CD789 /* SPiDTokenStorageUserDefaultsBackend.h in Headers */ = {isa = PBXBuildFile; fileRef = 089318581E6EFD52002CD789 /* SPiDTokenStorageUserDefaultsBackend.h */; }; + 0893185B1E6EFD52002CD789 /* SPiDTokenStorageUserDefaultsBackend.m in Sources */ = {isa = PBXBuildFile; fileRef = 089318591E6EFD52002CD789 /* SPiDTokenStorageUserDefaultsBackend.m */; }; + 0893185E1E6EFEA9002CD789 /* SPiDTokenStorageKeychainBackend.h in Headers */ = {isa = PBXBuildFile; fileRef = 0893185C1E6EFEA9002CD789 /* SPiDTokenStorageKeychainBackend.h */; }; + 0893185F1E6EFEA9002CD789 /* SPiDTokenStorageKeychainBackend.m in Sources */ = {isa = PBXBuildFile; fileRef = 0893185D1E6EFEA9002CD789 /* SPiDTokenStorageKeychainBackend.m */; }; + 089318611E716AE6002CD789 /* SPiDTokenStorageTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 089318601E716AE6002CD789 /* SPiDTokenStorageTests.m */; }; + 089318621E717174002CD789 /* SPiDTokenStorageUserDefaultsBackend.m in Sources */ = {isa = PBXBuildFile; fileRef = 089318591E6EFD52002CD789 /* SPiDTokenStorageUserDefaultsBackend.m */; }; + 089318641E7176DD002CD789 /* SPiDTokenStorageUserDefaultsBackendTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 089318631E7176DD002CD789 /* SPiDTokenStorageUserDefaultsBackendTests.m */; }; + 089318651E717AB9002CD789 /* SPiDTokenStorage.m in Sources */ = {isa = PBXBuildFile; fileRef = 089318551E6EFAD6002CD789 /* SPiDTokenStorage.m */; }; + 089318661E717AE6002CD789 /* SPiDTokenStorageKeychainBackend.m in Sources */ = {isa = PBXBuildFile; fileRef = 0893185D1E6EFEA9002CD789 /* SPiDTokenStorageKeychainBackend.m */; }; + 089318671E717AF4002CD789 /* SPiDKeychainWrapper.m in Sources */ = {isa = PBXBuildFile; fileRef = E304E86B163571BA2FDCE5BD /* SPiDKeychainWrapper.m */; }; + 089318681E717B44002CD789 /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E304ECEC953C2E9C143FD9E2 /* Security.framework */; }; 5306208B16C12440001B2A08 /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E304ECEC953C2E9C143FD9E2 /* Security.framework */; }; 5306209716C1375D001B2A08 /* Social.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5306209616C1375D001B2A08 /* Social.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; 5306209B16C1376F001B2A08 /* Accounts.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5306209A16C1376F001B2A08 /* Accounts.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; @@ -153,6 +166,14 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 089318541E6EFAD6002CD789 /* SPiDTokenStorage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPiDTokenStorage.h; sourceTree = ""; }; + 089318551E6EFAD6002CD789 /* SPiDTokenStorage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPiDTokenStorage.m; sourceTree = ""; }; + 089318581E6EFD52002CD789 /* SPiDTokenStorageUserDefaultsBackend.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPiDTokenStorageUserDefaultsBackend.h; sourceTree = ""; }; + 089318591E6EFD52002CD789 /* SPiDTokenStorageUserDefaultsBackend.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPiDTokenStorageUserDefaultsBackend.m; sourceTree = ""; }; + 0893185C1E6EFEA9002CD789 /* SPiDTokenStorageKeychainBackend.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPiDTokenStorageKeychainBackend.h; sourceTree = ""; }; + 0893185D1E6EFEA9002CD789 /* SPiDTokenStorageKeychainBackend.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPiDTokenStorageKeychainBackend.m; sourceTree = ""; }; + 089318601E716AE6002CD789 /* SPiDTokenStorageTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPiDTokenStorageTests.m; sourceTree = ""; }; + 089318631E7176DD002CD789 /* SPiDTokenStorageUserDefaultsBackendTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPiDTokenStorageUserDefaultsBackendTests.m; sourceTree = ""; }; 5306208716C082B8001B2A08 /* MainViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MainViewController.h; sourceTree = ""; }; 5306209616C1375D001B2A08 /* Social.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Social.framework; path = System/Library/Frameworks/Social.framework; sourceTree = SDKROOT; }; 5306209816C13764001B2A08 /* AdSupport.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AdSupport.framework; path = System/Library/Frameworks/AdSupport.framework; sourceTree = SDKROOT; }; @@ -342,6 +363,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 089318681E717B44002CD789 /* Security.framework in Frameworks */, 537D220515FF224C000ABCA6 /* Foundation.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -534,6 +556,8 @@ 9698149C1E54942600439631 /* SPiDAccessTokenTests.m */, 969814A11E54972700439631 /* NSDictionary+Test.h */, 969814A21E54972700439631 /* NSDictionary+Test.m */, + 089318601E716AE6002CD789 /* SPiDTokenStorageTests.m */, + 089318631E7176DD002CD789 /* SPiDTokenStorageUserDefaultsBackendTests.m */, ); path = SPiDSDKTests; sourceTree = ""; @@ -637,6 +661,12 @@ DAFD374A1CD9F9B300BF0DE3 /* Info.plist */, 9665D1F31E0820C300759F60 /* SPiDAgreements.h */, 9665D1F41E0820C300759F60 /* SPiDAgreements.m */, + 089318541E6EFAD6002CD789 /* SPiDTokenStorage.h */, + 089318551E6EFAD6002CD789 /* SPiDTokenStorage.m */, + 089318581E6EFD52002CD789 /* SPiDTokenStorageUserDefaultsBackend.h */, + 089318591E6EFD52002CD789 /* SPiDTokenStorageUserDefaultsBackend.m */, + 0893185C1E6EFEA9002CD789 /* SPiDTokenStorageKeychainBackend.h */, + 0893185D1E6EFEA9002CD789 /* SPiDTokenStorageKeychainBackend.m */, ); path = SPiDSDK; sourceTree = ""; @@ -648,6 +678,7 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( + 0893185A1E6EFD52002CD789 /* SPiDTokenStorageUserDefaultsBackend.h in Headers */, DAFD37551CD9F9D700BF0DE3 /* SPiDResponse.h in Headers */, DAFD37611CD9F9D700BF0DE3 /* SPiDJwt.h in Headers */, DAFD375A1CD9F9D700BF0DE3 /* SPiDClient.h in Headers */, @@ -655,6 +686,7 @@ DAFD374E1CD9F9D700BF0DE3 /* NSError+SPiD.h in Headers */, DAF1DE401CDC78B8007B15B3 /* SPiDWebView.h in Headers */, DAFD37561CD9F9D700BF0DE3 /* SPiDRequest.h in Headers */, + 0893185E1E6EFEA9002CD789 /* SPiDTokenStorageKeychainBackend.h in Headers */, DAFD37491CD9F9B300BF0DE3 /* SPiDSDK.h in Headers */, DAFD37521CD9F9D700BF0DE3 /* SPiDAccessToken.h in Headers */, DAFD37651CD9F9D700BF0DE3 /* SPiDUser.h in Headers */, @@ -665,6 +697,7 @@ DAFD376A1CD9F9D700BF0DE3 /* NSURLRequest+SPiD.h in Headers */, DAFD37631CD9F9D700BF0DE3 /* NSString+Crypto.h in Headers */, DAC183B71CDA161600D08ABD /* NSCharacterSet+SPiD.h in Headers */, + 089318561E6EFAD6002CD789 /* SPiDTokenStorage.h in Headers */, DAFD375D1CD9F9D700BF0DE3 /* NSData+Base64.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; @@ -952,13 +985,19 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 089318661E717AE6002CD789 /* SPiDTokenStorageKeychainBackend.m in Sources */, DAF1DE3D1CDC706D007B15B3 /* NSCharacterSet+SPiD.m in Sources */, 537D221115FF224C000ABCA6 /* SPiDSDKTests.m in Sources */, 9665D1F91E092DCE00759F60 /* SPiDAgreements.m in Sources */, + 089318651E717AB9002CD789 /* SPiDTokenStorage.m in Sources */, + 089318671E717AF4002CD789 /* SPiDKeychainWrapper.m in Sources */, 969814A31E54972700439631 /* NSDictionary+Test.m in Sources */, 9665D1F81E092C0000759F60 /* SPiDAgreementsTests.m in Sources */, + 089318621E717174002CD789 /* SPiDTokenStorageUserDefaultsBackend.m in Sources */, + 089318641E7176DD002CD789 /* SPiDTokenStorageUserDefaultsBackendTests.m in Sources */, DAF1DE3C1CDC6F35007B15B3 /* SPiDUtils.m in Sources */, 9698149D1E54942600439631 /* SPiDAccessTokenTests.m in Sources */, + 089318611E716AE6002CD789 /* SPiDTokenStorageTests.m in Sources */, 969814A41E549AEE00439631 /* SPiDAccessToken.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -980,12 +1019,15 @@ files = ( DAFD376B1CD9F9D700BF0DE3 /* NSURLRequest+SPiD.m in Sources */, DAFD375B1CD9F9D700BF0DE3 /* SPiDClient.m in Sources */, + 0893185F1E6EFEA9002CD789 /* SPiDTokenStorageKeychainBackend.m in Sources */, DAFD37571CD9F9D700BF0DE3 /* SPiDRequest.m in Sources */, DAFD37541CD9F9D700BF0DE3 /* SPiDResponse.m in Sources */, DAFD37601CD9F9D700BF0DE3 /* SPiDJwt.m in Sources */, + 0893185B1E6EFD52002CD789 /* SPiDTokenStorageUserDefaultsBackend.m in Sources */, DAC183B91CDA161600D08ABD /* NSCharacterSet+SPiD.m in Sources */, DAFD375E1CD9F9D700BF0DE3 /* SPiDTokenRequest.m in Sources */, DAFD37621CD9F9D700BF0DE3 /* NSString+Crypto.m in Sources */, + 089318571E6EFAD6002CD789 /* SPiDTokenStorage.m in Sources */, 9665D1F61E0820C300759F60 /* SPiDAgreements.m in Sources */, DAFD374F1CD9F9D700BF0DE3 /* NSError+SPiD.m in Sources */, DAFD37531CD9F9D700BF0DE3 /* SPiDAccessToken.m in Sources */, @@ -1262,11 +1304,7 @@ 537D221815FF224C000ABCA6 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - FRAMEWORK_SEARCH_PATHS = ( - "\"$(SDKROOT)/Developer/Library/Frameworks\"", - "\"$(DEVELOPER_LIBRARY_DIR)/Frameworks\"", - "$(inherited)", - ); + FRAMEWORK_SEARCH_PATHS = "$(inherited)"; GCC_PRECOMPILE_PREFIX_HEADER = NO; GCC_PREFIX_HEADER = ""; INFOPLIST_FILE = "SPiDSDKTests/SPiDSDKTests-Info.plist"; @@ -1278,11 +1316,7 @@ 537D221915FF224C000ABCA6 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - FRAMEWORK_SEARCH_PATHS = ( - "\"$(SDKROOT)/Developer/Library/Frameworks\"", - "\"$(DEVELOPER_LIBRARY_DIR)/Frameworks\"", - "$(inherited)", - ); + FRAMEWORK_SEARCH_PATHS = "$(inherited)"; GCC_PRECOMPILE_PREFIX_HEADER = NO; GCC_PREFIX_HEADER = ""; INFOPLIST_FILE = "SPiDSDKTests/SPiDSDKTests-Info.plist"; @@ -1296,10 +1330,7 @@ buildSettings = { CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "\"$(SYSTEM_APPS_DIR)/Xcode.app/Contents/Developer/Library/Frameworks\"", - ); + FRAMEWORK_SEARCH_PATHS = "$(inherited)"; GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PREFIX_HEADER = "SPiDExampleApp/SPiDExampleApp-Prefix.pch"; INFOPLIST_FILE = "SPiDExampleApp/SPiDExampleApp-Info.plist"; @@ -1319,10 +1350,7 @@ buildSettings = { CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "\"$(SYSTEM_APPS_DIR)/Xcode.app/Contents/Developer/Library/Frameworks\"", - ); + FRAMEWORK_SEARCH_PATHS = "$(inherited)"; GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PREFIX_HEADER = "SPiDExampleApp/SPiDExampleApp-Prefix.pch"; INFOPLIST_FILE = "SPiDExampleApp/SPiDExampleApp-Info.plist"; diff --git a/SPiDSDK/SPiDClient.h b/SPiDSDK/SPiDClient.h index c248cfe..3c1843b 100644 --- a/SPiDSDK/SPiDClient.h +++ b/SPiDSDK/SPiDClient.h @@ -21,6 +21,32 @@ NS_ASSUME_NONNULL_BEGIN static NSString *const defaultAPIVersionSPiD = @"2"; static NSString *const AccessTokenKeychainIdentification = @"AccessToken"; +/** + Options for persisting user tokens. + + Normally an app should only use the keychain to store the tokens. + During app iTunes account migration the options here let + exporting or importing the data from NSUserDefaults. + This is needed to keep a user logged in after migration. + + The migration can be done in 2 steps (2 app updates): + 1. The first update is published to the old account with SPiDTokenStorageModeMigratePreITunesAccountMove + (and starts writing tokens to NSUserDefaults) + 2. The second update is published to the new account with SPiDTokenStorageModeMigratePostITunesAccountMove + (and starts reading tokens from NSUserDefaults) + Both versions should stay in the store for a while + until the most users are able to upgrade and run it. + Eventually a subsequent update can revert back to use SPiDTokenStorageModeDefault + */ +typedef NS_ENUM(NSUInteger, SPiDTokenStorageMode) { + /** Use only keychain to store and retrieve user tokens */ + SPiDTokenStorageModeDefault, + /** Retrieve user tokens from keychain, store to both keychain and NSUserDefaults */ + SPiDTokenStorageModeMigratePreITunesAccountMove, + /** Try retrieving user tokens from NSUserDefaults after keychain, store to keychain only */ + SPiDTokenStorageModeMigratePostITunesAccountMove, +}; + // debug print used by SPiDSDK #ifdef DEBUG # define SPiDDebugLog(fmt, ...) NSLog((@"%s [Line %d] " fmt), __PRETTY_FUNCTION__, __LINE__, ##__VA_ARGS__); @@ -148,11 +174,13 @@ static NSString *const AccessTokenKeychainIdentification = @"AccessToken"; @param clientSecret The client secret provided by SPiD @param appURLSchema The url schema for the app (eg spidtest://) @param serverURL The url to SPiD + @param tokenStorageMode See SPiDTokenStorageMode */ + (void)setClientID:(NSString *)clientID clientSecret:(NSString *)clientSecret appURLScheme:(NSString *)appURLSchema - serverURL:(NSURL *)serverURL; + serverURL:(NSURL *)serverURL + tokenStorageMode:(SPiDTokenStorageMode)tokenStorageMode; /** Redirects to safari for authorization diff --git a/SPiDSDK/SPiDClient.m b/SPiDSDK/SPiDClient.m index 7a8b08b..687e708 100644 --- a/SPiDSDK/SPiDClient.m +++ b/SPiDSDK/SPiDClient.m @@ -7,10 +7,11 @@ #import "SPiDClient.h" #import "SPiDRequest.h" -#import "SPiDKeychainWrapper.h" +#import "SPiDAccessToken.h" #import "SPiDResponse.h" #import "NSError+SPiD.h" #import "SPiDTokenRequest.h" +#import "SPiDTokenStorage.h" #import "SPiDStatus.h" #import "NSData+Base64.h" #import "SPiDAgreements.h" @@ -42,6 +43,7 @@ - (BOOL)doHandleOpenURL:(NSURL *)url; @property (nonatomic, strong, readwrite) NSMutableArray *waitingRequests; @property (nonatomic, strong) SPiDRequest *authorizationRequest; @property (nonatomic, copy) void (^completionHandler)(NSError *error); +@property (nonatomic, strong) SPiDTokenStorage *tokenStorage; @end @@ -57,7 +59,7 @@ + (SPiDClient *)sharedInstance { NSStringFromClass([self class]), NSStringFromSelector(_cmd), NSStringFromClass([self class]), - NSStringFromSelector(@selector(setClientID:clientSecret:appURLScheme:serverURL:))]; + NSStringFromSelector(@selector(setClientID:clientSecret:appURLScheme:serverURL:tokenStorageMode:))]; } return sharedSPiDClientInstance; @@ -66,7 +68,9 @@ + (SPiDClient *)sharedInstance { + (void)setClientID:(NSString *)clientID clientSecret:(NSString *)clientSecret appURLScheme:(NSString *)appURLSchema - serverURL:(NSURL *)serverURL { + serverURL:(NSURL *)serverURL + tokenStorageMode:(SPiDTokenStorageMode)tokenStorageMode +{ if (sharedSPiDClientInstance != nil) { [NSException raise:NSInternalInconsistencyException @@ -76,7 +80,8 @@ + (void)setClientID:(NSString *)clientID } static dispatch_once_t predicate; dispatch_once(&predicate, ^{ - sharedSPiDClientInstance = [[self alloc] init]; + SPiDTokenStorage *tokenStorage = [self createTokenStorageInMode:tokenStorageMode]; + sharedSPiDClientInstance = [[self alloc] initWithTokenStorage:tokenStorage]; }); [sharedSPiDClientInstance setClientID:clientID]; @@ -130,6 +135,21 @@ + (void)setClientID:(NSString *)clientID [SPiDStatus runStatusRequest]; } ++ (SPiDTokenStorage *)createTokenStorageInMode:(SPiDTokenStorageMode)mode +{ + switch (mode) { + case SPiDTokenStorageModeDefault: + return [[SPiDTokenStorage alloc] initWithReadBackendTypes:@[ @(SPiDTokenStorageBackendTypeKeychain) ] + writeBackendTypes:@[ @(SPiDTokenStorageBackendTypeKeychain) ]]; + case SPiDTokenStorageModeMigratePreITunesAccountMove: + return [[SPiDTokenStorage alloc] initWithReadBackendTypes:@[ @(SPiDTokenStorageBackendTypeKeychain) ] + writeBackendTypes:@[ @(SPiDTokenStorageBackendTypeKeychain), @(SPiDTokenStorageBackendTypeUserDefaults) ]]; + case SPiDTokenStorageModeMigratePostITunesAccountMove: + return [[SPiDTokenStorage alloc] initWithReadBackendTypes:@[ @(SPiDTokenStorageBackendTypeKeychain), @(SPiDTokenStorageBackendTypeUserDefaults) ] + writeBackendTypes:@[ @(SPiDTokenStorageBackendTypeKeychain) ]]; + } +} + - (void)browserRedirectAuthorizationWithCompletionHandler:(void (^)(NSError *response))completionHandler { #if !TARGET_OS_WATCH if (self.accessToken) { // we already have a access token @@ -353,9 +373,11 @@ - (void)emailStatusWithEmail:(NSString *)email completionHandler:(void (^)(SPiDR /// @name Private methods ///--------------------------------------------------------------------------------------- -- (id)init { +- (id)initWithTokenStorage:(SPiDTokenStorage *)tokenStorage +{ if (self = [super init]) { - self.accessToken = [SPiDKeychainWrapper accessTokenFromKeychainForIdentifier:AccessTokenKeychainIdentification]; + self.tokenStorage = tokenStorage; + self.accessToken = [self.tokenStorage loadAccessTokenAndReplicate]; if (![self apiVersionSPiD]) { [self setApiVersionSPiD:[NSString stringWithFormat:@"%@", defaultAPIVersionSPiD]]; } @@ -447,6 +469,15 @@ - (void)refreshAccessTokenAndRerunRequest:(SPiDRequest *)request { } } +- (void)setAndStoreAccessToken:(SPiDAccessToken *)accessToken { + self.accessToken = accessToken; + if (accessToken) { + [self.tokenStorage storeAccessTokenWithValue:accessToken]; + } else { + [self.tokenStorage removeAccessToken]; + } +} + - (void)clearAuthorizationRequest { @synchronized (self.authorizationRequest) { self.authorizationRequest = nil; @@ -468,9 +499,7 @@ - (void)authorizationComplete { - (void)logoutComplete { SPiDDebugLog(@"Logged out from SPiD"); - self.accessToken = nil; - - [SPiDKeychainWrapper removeAccessTokenFromKeychainForIdentifier:AccessTokenKeychainIdentification]; + [self setAndStoreAccessToken:nil]; [self clearAuthorizationRequest]; diff --git a/SPiDSDK/SPiDSDK.h b/SPiDSDK/SPiDSDK.h index eb53b94..5d57060 100644 --- a/SPiDSDK/SPiDSDK.h +++ b/SPiDSDK/SPiDSDK.h @@ -20,7 +20,6 @@ FOUNDATION_EXPORT const unsigned char SPiDVersionString[]; #import "SPiDAccessToken.h" #import "SPiDClient.h" #import "SPiDJwt.h" -#import "SPiDKeychainWrapper.h" #import "SPiDRequest.h" #import "SPiDResponse.h" #import "SPiDStatus.h" diff --git a/SPiDSDK/SPiDTokenRequest.m b/SPiDSDK/SPiDTokenRequest.m index 443c082..672483f 100644 --- a/SPiDSDK/SPiDTokenRequest.m +++ b/SPiDSDK/SPiDTokenRequest.m @@ -7,7 +7,7 @@ #import "SPiDTokenRequest.h" #import "NSError+SPiD.h" -#import "SPiDKeychainWrapper.h" +#import "SPiDAccessToken.h" #import "SPiDJwt.h" @interface SPiDTokenRequest () @@ -69,6 +69,12 @@ - (instancetype)initPostTokenRequestWithPath:(NSString *)requestPath body:(NSDic @end + +@interface SPiDClient (SPiDClientPrivateAccessTokenSetter) +- (void)setAndStoreAccessToken:(SPiDAccessToken *)accessToken; +@end + + @implementation SPiDTokenRequest + (instancetype)clientTokenRequestWithCompletionHandler:(void (^)(NSError *error))completionHandler { @@ -215,8 +221,7 @@ - (void)startWithRequest:(NSURLRequest *)request { self.tokenCompletionHandler(error); } else /*if (_receivedData)*/ { SPiDAccessToken *accessToken = [[SPiDAccessToken alloc] initWithDictionary:jsonObject]; - [SPiDKeychainWrapper storeInKeychainAccessTokenWithValue:accessToken forIdentifier:AccessTokenKeychainIdentification]; - [[SPiDClient sharedInstance] setAccessToken:accessToken]; + [[SPiDClient sharedInstance] setAndStoreAccessToken:accessToken]; [[SPiDClient sharedInstance] authorizationComplete]; self.tokenCompletionHandler(nil); } diff --git a/SPiDSDK/SPiDTokenStorage.h b/SPiDSDK/SPiDTokenStorage.h new file mode 100644 index 0000000..9911631 --- /dev/null +++ b/SPiDSDK/SPiDTokenStorage.h @@ -0,0 +1,43 @@ +// +// SPiDTokenStorage.h +// SPiDSDK +// +// Created by Daniel Lazarenko on 07/03/2017. +// + +#import + +@class SPiDAccessToken; + +@protocol SPiDTokenStorageBackend + +@required +- (SPiDAccessToken *)accessTokenForIdentifier:(NSString *)identifier; +- (BOOL)storeAccessTokenWithValue:(SPiDAccessToken *)accessToken forIdentifier:(NSString *)identifier; +- (BOOL)updateAccessTokenWithValue:(SPiDAccessToken *)accessToken forIdentifier:(NSString *)identifier; +- (void)removeAccessTokenForIdentifier:(NSString *)identifier; + +@end + + +typedef NS_ENUM(NSUInteger, SPiDTokenStorageBackendType) { + SPiDTokenStorageBackendTypeKeychain = 1, + SPiDTokenStorageBackendTypeUserDefaults, +}; + + +@interface SPiDTokenStorage : NSObject + +- (instancetype)initWithReadBackendTypes:(NSArray *)readBackendTypes + writeBackendTypes:(NSArray *)writeBackendTypes; + +- (instancetype)initWithReadBackendTypes:(NSArray *)readBackendTypes + writeBackendTypes:(NSArray *)writeBackendTypes + backends:(NSDictionary> *)backends; + +- (SPiDAccessToken *)loadAccessTokenAndReplicate; +- (BOOL)storeAccessTokenWithValue:(SPiDAccessToken *)accessToken; +- (BOOL)updateAccessTokenWithValue:(SPiDAccessToken *)accessToken; +- (void)removeAccessToken; + +@end diff --git a/SPiDSDK/SPiDTokenStorage.m b/SPiDSDK/SPiDTokenStorage.m new file mode 100644 index 0000000..90e216b --- /dev/null +++ b/SPiDSDK/SPiDTokenStorage.m @@ -0,0 +1,160 @@ +// +// SPiDTokenStorage.m +// SPiDSDK +// +// Created by Daniel Lazarenko on 07/03/2017. +// + +#import "SPiDTokenStorage.h" +#import "SPiDTokenStorageUserDefaultsBackend.h" +#import "SPiDTokenStorageKeychainBackend.h" + +static NSString *const AccessTokenKeychainIdentification = @"AccessToken"; + +@interface SPiDTokenStorage () +{ + NSArray *_readBackendTypes; + NSArray *_writeBackendTypes; + NSDictionary> *_backends; +} + +@property (readonly) NSString *identifier; +@property (readonly) NSArray> *readBackends; +@property (readonly) NSArray> *writeBackends; + +@end + +@implementation SPiDTokenStorage + ++ (id)createBackendOfType:(SPiDTokenStorageBackendType)type +{ + switch (type) { + case SPiDTokenStorageBackendTypeKeychain: + return [SPiDTokenStorageKeychainBackend new]; + case SPiDTokenStorageBackendTypeUserDefaults: + return [[SPiDTokenStorageUserDefaultsBackend alloc] initWithUserDefaults:[NSUserDefaults standardUserDefaults]]; + default: + NSAssert(NO, @"Unknown SPiDTokenStorageBackendType %d", (int)type); + } +} + +static BOOL haveCommonElementsInArrays(NSArray *array1, NSArray *array2) +{ + NSMutableSet *intersection = [NSMutableSet setWithArray:array1]; + [intersection intersectSet:[NSSet setWithArray:array2]]; + return (intersection.count > 0); +} + +- (instancetype)initWithReadBackendTypes:(NSArray *)readBackendTypes + writeBackendTypes:(NSArray *)writeBackendTypes + backends:(NSDictionary> *)backends +{ + __unused BOOL haveCommonReadAndWriteBackends = haveCommonElementsInArrays(readBackendTypes, writeBackendTypes); + NSAssert(haveCommonReadAndWriteBackends, @"At least one backend should be used for both reading and writing."); + + self = [super init]; + if (self == nil) return nil; + _readBackendTypes = readBackendTypes; + _writeBackendTypes = writeBackendTypes; + _backends = backends; + return self; +} + +- (instancetype)initWithReadBackendTypes:(NSArray *)readBackendTypes + writeBackendTypes:(NSArray *)writeBackendTypes +{ + NSSet *allBackendTypes = [[NSSet setWithArray:readBackendTypes] setByAddingObjectsFromArray:writeBackendTypes]; + NSMutableDictionary> *backends = [NSMutableDictionary new]; + for (NSNumber *type in allBackendTypes) { + backends[type] = [SPiDTokenStorage createBackendOfType:[type unsignedIntegerValue]]; + } + + return [self initWithReadBackendTypes:readBackendTypes writeBackendTypes:writeBackendTypes backends:backends]; +} + +- (NSString *)identifier +{ + return AccessTokenKeychainIdentification; +} + +- (NSArray> *)readBackends +{ + NSMutableArray *backends = [NSMutableArray new]; + for (NSNumber *backendType in _readBackendTypes) { + [backends addObject:_backends[backendType]]; + } + return backends; +} + +- (NSArray> *)writeBackends +{ + NSMutableArray *backends = [NSMutableArray new]; + for (NSNumber *backendType in _writeBackendTypes) { + [backends addObject:_backends[backendType]]; + } + return backends; +} + +- (SPiDAccessToken *)loadAccessTokenAndReplicate +{ + SPiDAccessToken *token = nil; + NSNumber *tokenBackendType = nil; + for (NSNumber *backendType in _readBackendTypes) { + id backend = _backends[backendType]; + token = [backend accessTokenForIdentifier:self.identifier]; + if (token != nil) { + tokenBackendType = backendType; + break; + } + } + + if (token == nil) { + return nil; + } + + // replicate + NSMutableSet *replicateBackendTypes = [NSMutableSet setWithArray:_writeBackendTypes]; + [replicateBackendTypes removeObject:tokenBackendType]; + for (NSNumber *backendType in replicateBackendTypes) { + id backend = _backends[backendType]; + [backend storeAccessTokenWithValue:token forIdentifier:self.identifier]; + } + + // If we've loaded the token from a backend which is not set up for writing, + // it's safe to delete it there, because it's already replicated. + // Note: This would remove unsafe read backend data (NSUserDefaults) + // after an upgrade to a safe write backend (keychain). + if (![_writeBackendTypes containsObject:tokenBackendType]) { + id backend = _backends[tokenBackendType]; + [backend removeAccessTokenForIdentifier:self.identifier]; + } + + return token; +} + +- (BOOL)storeAccessTokenWithValue:(SPiDAccessToken *)accessToken +{ + BOOL result = YES; + for (id backend in self.writeBackends) { + result &= [backend storeAccessTokenWithValue:accessToken forIdentifier:self.identifier]; + } + return result; +} + +- (BOOL)updateAccessTokenWithValue:(SPiDAccessToken *)accessToken +{ + BOOL result = YES; + for (id backend in self.writeBackends) { + result &= [backend updateAccessTokenWithValue:accessToken forIdentifier:self.identifier]; + } + return result; +} + +- (void)removeAccessToken +{ + for (id backend in _backends.allValues) { + [backend removeAccessTokenForIdentifier:self.identifier]; + } +} + +@end diff --git a/SPiDSDK/SPiDTokenStorageKeychainBackend.h b/SPiDSDK/SPiDTokenStorageKeychainBackend.h new file mode 100644 index 0000000..fbcb90a --- /dev/null +++ b/SPiDSDK/SPiDTokenStorageKeychainBackend.h @@ -0,0 +1,13 @@ +// +// SPiDTokenStorageKeychainBackend.h +// SPiDSDK +// +// Created by Daniel Lazarenko on 07/03/2017. +// + +#import +#import "SPiDTokenStorage.h" + +@interface SPiDTokenStorageKeychainBackend : NSObject + +@end diff --git a/SPiDSDK/SPiDTokenStorageKeychainBackend.m b/SPiDSDK/SPiDTokenStorageKeychainBackend.m new file mode 100644 index 0000000..5555767 --- /dev/null +++ b/SPiDSDK/SPiDTokenStorageKeychainBackend.m @@ -0,0 +1,33 @@ +// +// SPiDTokenStorageKeychainBackend.m +// SPiDSDK +// +// Created by Daniel Lazarenko on 07/03/2017. +// + +#import "SPiDTokenStorageKeychainBackend.h" +#import "SPiDKeychainWrapper.h" + +@implementation SPiDTokenStorageKeychainBackend + +- (SPiDAccessToken *)accessTokenForIdentifier:(NSString *)identifier +{ + return [SPiDKeychainWrapper accessTokenFromKeychainForIdentifier:identifier]; +} + +- (BOOL)storeAccessTokenWithValue:(SPiDAccessToken *)accessToken forIdentifier:(NSString *)identifier +{ + return [SPiDKeychainWrapper storeInKeychainAccessTokenWithValue:accessToken forIdentifier:identifier]; +} + +- (BOOL)updateAccessTokenWithValue:(SPiDAccessToken *)accessToken forIdentifier:(NSString *)identifier +{ + return [SPiDKeychainWrapper updateAccessTokenInKeychainWithValue:accessToken forIdentifier:identifier]; +} + +- (void)removeAccessTokenForIdentifier:(NSString *)identifier +{ + [SPiDKeychainWrapper removeAccessTokenFromKeychainForIdentifier:identifier]; +} + +@end diff --git a/SPiDSDK/SPiDTokenStorageUserDefaultsBackend.h b/SPiDSDK/SPiDTokenStorageUserDefaultsBackend.h new file mode 100644 index 0000000..017dec0 --- /dev/null +++ b/SPiDSDK/SPiDTokenStorageUserDefaultsBackend.h @@ -0,0 +1,15 @@ +// +// SPiDTokenStorageUserDefaultsBackend.h +// SPiDSDK +// +// Created by Daniel Lazarenko on 07/03/2017. +// + +#import +#import "SPiDTokenStorage.h" + +@interface SPiDTokenStorageUserDefaultsBackend : NSObject + +- (instancetype)initWithUserDefaults:(NSUserDefaults *)defaults; + +@end diff --git a/SPiDSDK/SPiDTokenStorageUserDefaultsBackend.m b/SPiDSDK/SPiDTokenStorageUserDefaultsBackend.m new file mode 100644 index 0000000..46810bf --- /dev/null +++ b/SPiDSDK/SPiDTokenStorageUserDefaultsBackend.m @@ -0,0 +1,80 @@ +// +// SPiDTokenStorageUserDefaultsBackend.m +// SPiDSDK +// +// Created by Daniel Lazarenko on 07/03/2017. +// + +#import "SPiDTokenStorageUserDefaultsBackend.h" +#import "SPiDTokenStorage.h" +#import "SPiDAccessToken.h" + +static NSString *const SPiDAccessTokenUserIdKey = @"user_id"; +static NSString *const SPiDAccessTokenKey = @"access_token"; +static NSString *const SPiDAccessTokenExpiresAtKey = @"expires_at"; +static NSString *const SPiDAccessTokenRefreshTokenKey = @"refresh_token"; + +static NSString *const SPiDAccessTokenUserDefaultsPrefix = @"SPiD."; + +#define DICT_KEY [SPiDAccessTokenUserDefaultsPrefix stringByAppendingString:identifier] + + +@implementation SPiDTokenStorageUserDefaultsBackend +{ + NSUserDefaults *_defaults; +} + +- (instancetype)initWithUserDefaults:(NSUserDefaults *)defaults +{ + self = [super init]; + if (self == nil) return nil; + _defaults = defaults; + return self; +} + +- (SPiDAccessToken *)accessTokenForIdentifier:(NSString *)identifier +{ + NSDictionary *dict = [_defaults dictionaryForKey:DICT_KEY]; + NSString *userID = dict[SPiDAccessTokenUserIdKey]; + NSString *accessToken = dict[SPiDAccessTokenKey]; + NSNumber *expiresAtNum = dict[SPiDAccessTokenExpiresAtKey]; + NSDate *expiresAt = nil; + if (expiresAtNum != nil) { + expiresAt = [NSDate dateWithTimeIntervalSince1970:[expiresAtNum integerValue]]; + } + NSString *refreshToken = dict[SPiDAccessTokenRefreshTokenKey]; + return [[SPiDAccessToken alloc] initWithUserID:userID accessToken:accessToken + expiresAt:expiresAt refreshToken:refreshToken]; +} + +- (BOOL)storeAccessTokenWithValue:(SPiDAccessToken *)accessToken forIdentifier:(NSString *)identifier +{ + return [self updateAccessTokenWithValue:accessToken forIdentifier:identifier]; +} + +- (BOOL)updateAccessTokenWithValue:(SPiDAccessToken *)accessToken forIdentifier:(NSString *)identifier +{ + NSMutableDictionary *dict = [NSMutableDictionary new]; + if (accessToken.userID) { + dict[SPiDAccessTokenUserIdKey] = accessToken.userID; + } + if (accessToken.accessToken) { + dict[SPiDAccessTokenKey] = accessToken.accessToken; + } + if (accessToken.expiresAt) { + NSInteger expiresAtInt = (NSInteger)[accessToken.expiresAt timeIntervalSince1970]; + dict[SPiDAccessTokenExpiresAtKey] = @(expiresAtInt); + } + if (accessToken.refreshToken) { + dict[SPiDAccessTokenRefreshTokenKey] = accessToken.refreshToken; + } + [_defaults setObject:dict forKey:DICT_KEY]; + return YES; +} + +- (void)removeAccessTokenForIdentifier:(NSString *)identifier +{ + [_defaults removeObjectForKey:DICT_KEY]; +} + +@end diff --git a/SPiDSDKTests/SPiDTokenStorageTests.m b/SPiDSDKTests/SPiDTokenStorageTests.m new file mode 100644 index 0000000..07e0be1 --- /dev/null +++ b/SPiDSDKTests/SPiDTokenStorageTests.m @@ -0,0 +1,171 @@ +// +// SPiDTokenStorageTests.m +// SPiDSDK +// +// Created by Daniel Lazarenko on 09/03/2017. +// + +#import +#import "SPiDTokenStorageUserDefaultsBackend.h" +#import "SPiDAccessToken.h" +#import "SPiDTokenStorage.h" + +@interface SPiDTokenStorageTests : XCTestCase +{ + NSUserDefaults *_defaults1; + id _backend1; + NSUserDefaults *_defaults2; + id _backend2; + NSDictionary> *_backends; +} + +@property (readonly) NSDate *expectedExpiresAt; +@property (readonly) NSString *tokenIdentifier; + +@end + +@implementation SPiDTokenStorageTests + +NSUserDefaults *SPiDSDKTestsCreateFreshUserDefaults(); + +- (void)setUp +{ + [super setUp]; + _defaults1 = SPiDSDKTestsCreateFreshUserDefaults(@"xctest1"); + _backend1 = [[SPiDTokenStorageUserDefaultsBackend alloc] initWithUserDefaults:_defaults1]; + _defaults2 = SPiDSDKTestsCreateFreshUserDefaults(@"xctest2"); + _backend2 = [[SPiDTokenStorageUserDefaultsBackend alloc] initWithUserDefaults:_defaults2]; + _backends = @{ + @(SPiDTokenStorageBackendTypeKeychain): _backend1, + @(SPiDTokenStorageBackendTypeUserDefaults): _backend2, + }; +} + +- (SPiDAccessToken *)createExpectedToken +{ + return [[SPiDAccessToken alloc] initWithUserID:@"uid123" accessToken:@"at234" + expiresAt:[NSDate dateWithTimeIntervalSinceReferenceDate:555] + refreshToken:@"rt345"]; +} + +- (NSString *)tokenIdentifier +{ + return @"AccessToken"; +} + +- (void)testBackendsAreCleanAfterSetUp +{ + XCTAssertNil([_backend1 accessTokenForIdentifier:self.tokenIdentifier]); + XCTAssertNil([_backend2 accessTokenForIdentifier:self.tokenIdentifier]); +} + +- (void)testLoadsSomeTokenAfterStoring +{ + SPiDTokenStorage *storage = [[SPiDTokenStorage alloc] initWithReadBackendTypes:@[ @(SPiDTokenStorageBackendTypeKeychain) ] + writeBackendTypes:@[ @(SPiDTokenStorageBackendTypeKeychain) ] + backends:_backends]; + [storage storeAccessTokenWithValue:[self createExpectedToken]]; + SPiDAccessToken *token = [storage loadAccessTokenAndReplicate]; + XCTAssertNotNil(token); +} + +- (void)testStoreToOnlyOneWriteBackend +{ + SPiDTokenStorage *storage = [[SPiDTokenStorage alloc] initWithReadBackendTypes:@[ @(SPiDTokenStorageBackendTypeKeychain) ] + writeBackendTypes:@[ @(SPiDTokenStorageBackendTypeKeychain) ] + backends:_backends]; + [storage storeAccessTokenWithValue:[self createExpectedToken]]; + XCTAssertNotNil([_backend1 accessTokenForIdentifier:self.tokenIdentifier]); + XCTAssertNil([_backend2 accessTokenForIdentifier:self.tokenIdentifier]); +} + +- (void)testWriteToBothBackends +{ + SPiDTokenStorage *storage = [[SPiDTokenStorage alloc] initWithReadBackendTypes:@[ @(SPiDTokenStorageBackendTypeKeychain) ] + writeBackendTypes:@[ @(SPiDTokenStorageBackendTypeKeychain), @(SPiDTokenStorageBackendTypeUserDefaults) ] + backends:_backends]; + [storage storeAccessTokenWithValue:[self createExpectedToken]]; + XCTAssertNotNil([_backend1 accessTokenForIdentifier:self.tokenIdentifier]); + XCTAssertNotNil([_backend2 accessTokenForIdentifier:self.tokenIdentifier]); + SPiDAccessToken *token = [storage loadAccessTokenAndReplicate]; + XCTAssertNotNil(token); +} + +- (void)testReplicateAfterReading +{ + SPiDTokenStorage *storageOnly1 = [[SPiDTokenStorage alloc] initWithReadBackendTypes:@[ @(SPiDTokenStorageBackendTypeKeychain) ] + writeBackendTypes:@[ @(SPiDTokenStorageBackendTypeKeychain) ] + backends:_backends]; + [storageOnly1 storeAccessTokenWithValue:[self createExpectedToken]]; + [storageOnly1 loadAccessTokenAndReplicate]; + XCTAssertNil([_backend2 accessTokenForIdentifier:self.tokenIdentifier]); + + SPiDTokenStorage *storageWriteBoth = [[SPiDTokenStorage alloc] initWithReadBackendTypes:@[ @(SPiDTokenStorageBackendTypeKeychain) ] + writeBackendTypes:@[ @(SPiDTokenStorageBackendTypeKeychain), @(SPiDTokenStorageBackendTypeUserDefaults) ] + backends:_backends]; + SPiDAccessToken *token = [storageWriteBoth loadAccessTokenAndReplicate]; + XCTAssertNotNil(token); + XCTAssertNotNil([_backend1 accessTokenForIdentifier:self.tokenIdentifier]); + XCTAssertNotNil([_backend2 accessTokenForIdentifier:self.tokenIdentifier]); +} + +- (void)testCleanupAfterReadingBackup +{ + SPiDTokenStorage *storageOnlyBackup = [[SPiDTokenStorage alloc] initWithReadBackendTypes:@[ @(SPiDTokenStorageBackendTypeUserDefaults) ] + writeBackendTypes:@[ @(SPiDTokenStorageBackendTypeUserDefaults) ] + backends:_backends]; + [storageOnlyBackup storeAccessTokenWithValue:[self createExpectedToken]]; + XCTAssertNil([_backend1 accessTokenForIdentifier:self.tokenIdentifier]); + XCTAssertNotNil([_backend2 accessTokenForIdentifier:self.tokenIdentifier]); + + SPiDTokenStorage *storageReadBackup = [[SPiDTokenStorage alloc] initWithReadBackendTypes:@[ @(SPiDTokenStorageBackendTypeKeychain), @(SPiDTokenStorageBackendTypeUserDefaults) ] + writeBackendTypes:@[ @(SPiDTokenStorageBackendTypeKeychain) ] + backends:_backends]; + SPiDAccessToken *token = [storageReadBackup loadAccessTokenAndReplicate]; + XCTAssertNotNil(token); + XCTAssertNotNil([_backend1 accessTokenForIdentifier:self.tokenIdentifier]); + XCTAssertNil([_backend2 accessTokenForIdentifier:self.tokenIdentifier]); +} + +- (void)testRemovalFromAllBackends +{ + SPiDTokenStorage *storage = [[SPiDTokenStorage alloc] initWithReadBackendTypes:@[ @(SPiDTokenStorageBackendTypeKeychain) ] + writeBackendTypes:@[ @(SPiDTokenStorageBackendTypeKeychain), @(SPiDTokenStorageBackendTypeUserDefaults) ] + backends:_backends]; + [storage storeAccessTokenWithValue:[self createExpectedToken]]; + [storage removeAccessToken]; + XCTAssertNil([_backend1 accessTokenForIdentifier:self.tokenIdentifier]); + XCTAssertNil([_backend2 accessTokenForIdentifier:self.tokenIdentifier]); + SPiDAccessToken *token = [storage loadAccessTokenAndReplicate]; + XCTAssertNil(token); +} + +- (void)testFullMigrationScenario +{ + // logged in app state + SPiDTokenStorage *storageOnly1 = [[SPiDTokenStorage alloc] initWithReadBackendTypes:@[ @(SPiDTokenStorageBackendTypeKeychain) ] + writeBackendTypes:@[ @(SPiDTokenStorageBackendTypeKeychain) ] + backends:@{ @(SPiDTokenStorageBackendTypeKeychain): _backend1 }]; + [storageOnly1 storeAccessTokenWithValue:[self createExpectedToken]]; + + // update 1: replicate to backup + SPiDTokenStorage *storageWriteBoth = [[SPiDTokenStorage alloc] initWithReadBackendTypes:@[ @(SPiDTokenStorageBackendTypeKeychain) ] + writeBackendTypes:@[ @(SPiDTokenStorageBackendTypeKeychain), @(SPiDTokenStorageBackendTypeUserDefaults) ] + backends:_backends]; + [storageWriteBoth loadAccessTokenAndReplicate]; + + // update 2: move to the new account + [storageOnly1 removeAccessToken]; + SPiDTokenStorage *storageReadBackup = [[SPiDTokenStorage alloc] initWithReadBackendTypes:@[ @(SPiDTokenStorageBackendTypeKeychain), @(SPiDTokenStorageBackendTypeUserDefaults) ] + writeBackendTypes:@[ @(SPiDTokenStorageBackendTypeKeychain) ] + backends:_backends]; + [storageReadBackup loadAccessTokenAndReplicate]; + + // update 3: back to keychain only + XCTAssertNotNil([_backend1 accessTokenForIdentifier:self.tokenIdentifier]); + XCTAssertNil([_backend2 accessTokenForIdentifier:self.tokenIdentifier]); + SPiDAccessToken *token = [storageOnly1 loadAccessTokenAndReplicate]; + XCTAssertNotNil(token); +} + +@end diff --git a/SPiDSDKTests/SPiDTokenStorageUserDefaultsBackendTests.m b/SPiDSDKTests/SPiDTokenStorageUserDefaultsBackendTests.m new file mode 100644 index 0000000..d1ad38b --- /dev/null +++ b/SPiDSDKTests/SPiDTokenStorageUserDefaultsBackendTests.m @@ -0,0 +1,93 @@ +// +// SPiDTokenStorageUserDefaultsBackendTests.m +// SPiDSDK +// +// Created by Daniel Lazarenko on 09/03/2017. +// + +#import +#import "SPiDTokenStorageUserDefaultsBackend.h" +#import "SPiDAccessToken.h" + +@interface SPiDTokenStorageUserDefaultsBackendTests : XCTestCase +{ + NSUserDefaults *_defaults; + id _backend; +} + +@property (readonly) NSDate *expectedExpiresAt; + +@end + +@implementation SPiDTokenStorageUserDefaultsBackendTests + +NSUserDefaults *SPiDSDKTestsCreateFreshUserDefaults(NSString *name) +{ + NSUserDefaults *defaults = [[NSUserDefaults alloc] initWithSuiteName:name]; + [defaults removePersistentDomainForName:name]; + [defaults setPersistentDomain:[NSDictionary new] forName:name]; + // force a read for clean reload + __unused id dict = defaults.dictionaryRepresentation; + return defaults; +} + +- (void)setUp +{ + [super setUp]; + _defaults = SPiDSDKTestsCreateFreshUserDefaults(@"xctest"); + _backend = [[SPiDTokenStorageUserDefaultsBackend alloc] initWithUserDefaults:_defaults]; +} + +- (NSDate *)expectedExpiresAt +{ + return [NSDate dateWithTimeIntervalSinceReferenceDate:555]; +} + +- (SPiDAccessToken *)createExpectedToken +{ + return [[SPiDAccessToken alloc] initWithUserID:@"uid123" accessToken:@"at234" + expiresAt:self.expectedExpiresAt refreshToken:@"rt345"]; +} + +- (void)testStoringAddsOnePreference +{ + NSUInteger initialPrefsCount = _defaults.dictionaryRepresentation.count; + SPiDAccessToken *expectedToken = [self createExpectedToken]; + [_backend storeAccessTokenWithValue:expectedToken forIdentifier:@"id"]; + XCTAssertEqual(_defaults.dictionaryRepresentation.count, initialPrefsCount + 1); +} + +- (void)testLoadedTokenIsSameAsStored +{ + SPiDAccessToken *expectedToken = [self createExpectedToken]; + [_backend storeAccessTokenWithValue:expectedToken forIdentifier:@"id"]; + SPiDAccessToken *token = [_backend accessTokenForIdentifier:@"id"]; + + XCTAssertNotNil(token); + XCTAssertEqualObjects(token.userID, @"uid123"); + XCTAssertEqualObjects(token.accessToken, @"at234"); + XCTAssertEqualObjects(token.refreshToken, @"rt345"); + + XCTAssertNotNil(token.expiresAt); + double expiresAtDiff = fabs([self.expectedExpiresAt timeIntervalSinceDate:token.expiresAt]); + XCTAssertEqualWithAccuracy(expiresAtDiff, 0, 0.1); +} + +- (void)testLoadFailsWithWrongIdentifierPrefix +{ + SPiDAccessToken *expectedToken = [self createExpectedToken]; + [_backend storeAccessTokenWithValue:expectedToken forIdentifier:@"id1"]; + SPiDAccessToken *token = [_backend accessTokenForIdentifier:@"id2"]; + XCTAssertNil(token); +} + +- (void)testNotFoundAfterRemoving +{ + SPiDAccessToken *expectedToken = [self createExpectedToken]; + [_backend storeAccessTokenWithValue:expectedToken forIdentifier:@"id"]; + [_backend removeAccessTokenForIdentifier:@"id"]; + SPiDAccessToken *token = [_backend accessTokenForIdentifier:@"id"]; + XCTAssertNil(token); +} + +@end