From 7b645493476a25d923b5a22e17923ea66a3250ae Mon Sep 17 00:00:00 2001
From: Daniel Lazarenko <daniel.lazarenko@schibsted.com>
Date: Tue, 7 Mar 2017 16:33:36 +0100
Subject: [PATCH] IDMOB-10 Functionality for Apple account consolidation

When moving an app from one Apple account to another
it's not possible to keep the keychain data.
If the data is not there, a logged in user will not be logged in anymore.

To overcome this the move can be planned in 2 steps:
1. In the first step an app is upgraded on the old Apple account
  with a mode SPiDTokenStorageModeMigratePreITunesAccountMove to export keychain data to NSUserDefaults.
2. The next step is an app upgrade on to the new Apple account
  with a mode SPiDTokenStorageModeMigratePostITunesAccountMove to import data from NSUserDefaults.

Both versions should stay in the store for a while
until the most users are able to upgrade and run it.

Tests:
* SPiDTokenStorageTests
* SPiDTokenStorageUserDefaultsBackendTests
---
 SPiDExampleApp/SPiDExampleAppDelegate.m       |   3 +-
 SPiDFacebookApp/SPiDFacebookAppDelegate.m     |   3 +-
 SPiDHybridApp/SPiDHybridAppDelegate.m         |   3 +-
 SPiDNativeApp/SPiDNativeAppDelegate.m         |   3 +-
 SPiDSDK.xcodeproj/project.pbxproj             |  64 +++++--
 SPiDSDK/SPiDClient.h                          |  30 ++-
 SPiDSDK/SPiDClient.m                          |  47 ++++-
 SPiDSDK/SPiDSDK.h                             |   1 -
 SPiDSDK/SPiDTokenRequest.m                    |  11 +-
 SPiDSDK/SPiDTokenStorage.h                    |  43 +++++
 SPiDSDK/SPiDTokenStorage.m                    | 160 ++++++++++++++++
 SPiDSDK/SPiDTokenStorageKeychainBackend.h     |  13 ++
 SPiDSDK/SPiDTokenStorageKeychainBackend.m     |  33 ++++
 SPiDSDK/SPiDTokenStorageUserDefaultsBackend.h |  15 ++
 SPiDSDK/SPiDTokenStorageUserDefaultsBackend.m |  80 ++++++++
 SPiDSDKTests/SPiDTokenStorageTests.m          | 171 ++++++++++++++++++
 ...SPiDTokenStorageUserDefaultsBackendTests.m |  93 ++++++++++
 17 files changed, 737 insertions(+), 36 deletions(-)
 create mode 100644 SPiDSDK/SPiDTokenStorage.h
 create mode 100644 SPiDSDK/SPiDTokenStorage.m
 create mode 100644 SPiDSDK/SPiDTokenStorageKeychainBackend.h
 create mode 100644 SPiDSDK/SPiDTokenStorageKeychainBackend.m
 create mode 100644 SPiDSDK/SPiDTokenStorageUserDefaultsBackend.h
 create mode 100644 SPiDSDK/SPiDTokenStorageUserDefaultsBackend.m
 create mode 100644 SPiDSDKTests/SPiDTokenStorageTests.m
 create mode 100644 SPiDSDKTests/SPiDTokenStorageUserDefaultsBackendTests.m

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:@"<html><body>Loading SPiD login page</body></html>"];
 
     [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 = "<group>"; };
+		089318551E6EFAD6002CD789 /* SPiDTokenStorage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPiDTokenStorage.m; sourceTree = "<group>"; };
+		089318581E6EFD52002CD789 /* SPiDTokenStorageUserDefaultsBackend.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPiDTokenStorageUserDefaultsBackend.h; sourceTree = "<group>"; };
+		089318591E6EFD52002CD789 /* SPiDTokenStorageUserDefaultsBackend.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPiDTokenStorageUserDefaultsBackend.m; sourceTree = "<group>"; };
+		0893185C1E6EFEA9002CD789 /* SPiDTokenStorageKeychainBackend.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPiDTokenStorageKeychainBackend.h; sourceTree = "<group>"; };
+		0893185D1E6EFEA9002CD789 /* SPiDTokenStorageKeychainBackend.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPiDTokenStorageKeychainBackend.m; sourceTree = "<group>"; };
+		089318601E716AE6002CD789 /* SPiDTokenStorageTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPiDTokenStorageTests.m; sourceTree = "<group>"; };
+		089318631E7176DD002CD789 /* SPiDTokenStorageUserDefaultsBackendTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPiDTokenStorageUserDefaultsBackendTests.m; sourceTree = "<group>"; };
 		5306208716C082B8001B2A08 /* MainViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MainViewController.h; sourceTree = "<group>"; };
 		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 = "<group>";
@@ -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 = "<group>";
@@ -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 <Foundation/Foundation.h>
+
+@class SPiDAccessToken;
+
+@protocol SPiDTokenStorageBackend <NSObject>
+
+@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<NSNumber *> *)readBackendTypes
+                       writeBackendTypes:(NSArray<NSNumber *> *)writeBackendTypes;
+
+- (instancetype)initWithReadBackendTypes:(NSArray<NSNumber *> *)readBackendTypes
+                       writeBackendTypes:(NSArray<NSNumber *> *)writeBackendTypes
+                                backends:(NSDictionary<NSNumber *, id<SPiDTokenStorageBackend>> *)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<NSNumber *> *_readBackendTypes;
+    NSArray<NSNumber *> *_writeBackendTypes;
+    NSDictionary<NSNumber *, id<SPiDTokenStorageBackend>> *_backends;
+}
+
+@property (readonly) NSString *identifier;
+@property (readonly) NSArray<id<SPiDTokenStorageBackend>> *readBackends;
+@property (readonly) NSArray<id<SPiDTokenStorageBackend>> *writeBackends;
+
+@end
+
+@implementation SPiDTokenStorage
+
++ (id<SPiDTokenStorageBackend>)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<NSNumber *> *)readBackendTypes
+                       writeBackendTypes:(NSArray<NSNumber *> *)writeBackendTypes
+                                backends:(NSDictionary<NSNumber *, id<SPiDTokenStorageBackend>> *)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<NSNumber *> *)readBackendTypes
+                       writeBackendTypes:(NSArray<NSNumber *> *)writeBackendTypes
+{
+    NSSet<NSNumber *> *allBackendTypes = [[NSSet setWithArray:readBackendTypes] setByAddingObjectsFromArray:writeBackendTypes];
+    NSMutableDictionary<NSNumber *, id<SPiDTokenStorageBackend>> *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<id<SPiDTokenStorageBackend>> *)readBackends
+{
+    NSMutableArray *backends = [NSMutableArray new];
+    for (NSNumber *backendType in _readBackendTypes) {
+        [backends addObject:_backends[backendType]];
+    }
+    return backends;
+}
+
+- (NSArray<id<SPiDTokenStorageBackend>> *)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<SPiDTokenStorageBackend> 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<SPiDTokenStorageBackend> 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<SPiDTokenStorageBackend> backend = _backends[tokenBackendType];
+        [backend removeAccessTokenForIdentifier:self.identifier];
+    }
+
+    return token;
+}
+
+- (BOOL)storeAccessTokenWithValue:(SPiDAccessToken *)accessToken
+{
+    BOOL result = YES;
+    for (id<SPiDTokenStorageBackend> backend in self.writeBackends) {
+        result &= [backend storeAccessTokenWithValue:accessToken forIdentifier:self.identifier];
+    }
+    return result;
+}
+
+- (BOOL)updateAccessTokenWithValue:(SPiDAccessToken *)accessToken
+{
+    BOOL result = YES;
+    for (id<SPiDTokenStorageBackend> backend in self.writeBackends) {
+        result &= [backend updateAccessTokenWithValue:accessToken forIdentifier:self.identifier];
+    }
+    return result;
+}
+
+- (void)removeAccessToken
+{
+    for (id<SPiDTokenStorageBackend> 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 <Foundation/Foundation.h>
+#import "SPiDTokenStorage.h"
+
+@interface SPiDTokenStorageKeychainBackend : NSObject <SPiDTokenStorageBackend>
+
+@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 <Foundation/Foundation.h>
+#import "SPiDTokenStorage.h"
+
+@interface SPiDTokenStorageUserDefaultsBackend : NSObject <SPiDTokenStorageBackend>
+
+- (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<NSString *, id> *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<NSString *, id> *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 <XCTest/XCTest.h>
+#import "SPiDTokenStorageUserDefaultsBackend.h"
+#import "SPiDAccessToken.h"
+#import "SPiDTokenStorage.h"
+
+@interface SPiDTokenStorageTests : XCTestCase
+{
+    NSUserDefaults *_defaults1;
+    id<SPiDTokenStorageBackend> _backend1;
+    NSUserDefaults *_defaults2;
+    id<SPiDTokenStorageBackend> _backend2;
+    NSDictionary<NSNumber *, id<SPiDTokenStorageBackend>> *_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 <XCTest/XCTest.h>
+#import "SPiDTokenStorageUserDefaultsBackend.h"
+#import "SPiDAccessToken.h"
+
+@interface SPiDTokenStorageUserDefaultsBackendTests : XCTestCase
+{
+    NSUserDefaults *_defaults;
+    id<SPiDTokenStorageBackend> _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