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