No Description

FIRInstanceIDTokenManager.m 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341
  1. /*
  2. * Copyright 2019 Google
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. #import "FIRInstanceIDTokenManager.h"
  17. #import "FIRInstanceIDAuthKeyChain.h"
  18. #import "FIRInstanceIDAuthService.h"
  19. #import "FIRInstanceIDCheckinPreferences.h"
  20. #import "FIRInstanceIDConstants.h"
  21. #import "FIRInstanceIDDefines.h"
  22. #import "FIRInstanceIDLogger.h"
  23. #import "FIRInstanceIDStore.h"
  24. #import "FIRInstanceIDTokenDeleteOperation.h"
  25. #import "FIRInstanceIDTokenFetchOperation.h"
  26. #import "FIRInstanceIDTokenInfo.h"
  27. #import "FIRInstanceIDTokenOperation.h"
  28. #import "NSError+FIRInstanceID.h"
  29. @interface FIRInstanceIDTokenManager () <FIRInstanceIDStoreDelegate>
  30. @property(nonatomic, readwrite, strong) FIRInstanceIDStore *instanceIDStore;
  31. @property(nonatomic, readwrite, strong) FIRInstanceIDAuthService *authService;
  32. @property(nonatomic, readonly, strong) NSOperationQueue *tokenOperations;
  33. @property(nonatomic, readwrite, strong) FIRInstanceIDAPNSInfo *currentAPNSInfo;
  34. @end
  35. @implementation FIRInstanceIDTokenManager
  36. - (instancetype)init {
  37. self = [super init];
  38. if (self) {
  39. _instanceIDStore = [[FIRInstanceIDStore alloc] initWithDelegate:self];
  40. _authService = [[FIRInstanceIDAuthService alloc] initWithStore:_instanceIDStore];
  41. [self configureTokenOperations];
  42. }
  43. return self;
  44. }
  45. - (void)dealloc {
  46. [self stopAllTokenOperations];
  47. }
  48. - (void)configureTokenOperations {
  49. _tokenOperations = [[NSOperationQueue alloc] init];
  50. _tokenOperations.name = @"com.google.iid-token-operations";
  51. // For now, restrict the operations to be serial, because in some cases (like if the
  52. // authorized entity and scope are the same), order matters.
  53. // If we have to deal with several different token requests simultaneously, it would be a good
  54. // idea to add some better intelligence around this (performing unrelated token operations
  55. // simultaneously, etc.).
  56. _tokenOperations.maxConcurrentOperationCount = 1;
  57. if ([_tokenOperations respondsToSelector:@selector(qualityOfService)]) {
  58. _tokenOperations.qualityOfService = NSOperationQualityOfServiceUtility;
  59. }
  60. }
  61. - (void)fetchNewTokenWithAuthorizedEntity:(NSString *)authorizedEntity
  62. scope:(NSString *)scope
  63. keyPair:(FIRInstanceIDKeyPair *)keyPair
  64. options:(NSDictionary *)options
  65. handler:(FIRInstanceIDTokenHandler)handler {
  66. FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeTokenManager000,
  67. @"Fetch new token for authorizedEntity: %@, scope: %@", authorizedEntity,
  68. scope);
  69. FIRInstanceIDTokenFetchOperation *operation =
  70. [self createFetchOperationWithAuthorizedEntity:authorizedEntity
  71. scope:scope
  72. options:options
  73. keyPair:keyPair];
  74. FIRInstanceID_WEAKIFY(self);
  75. FIRInstanceIDTokenOperationCompletion completion =
  76. ^(FIRInstanceIDTokenOperationResult result, NSString *_Nullable token,
  77. NSError *_Nullable error) {
  78. FIRInstanceID_STRONGIFY(self);
  79. if (error) {
  80. handler(nil, error);
  81. return;
  82. }
  83. NSString *firebaseAppID = options[kFIRInstanceIDTokenOptionsFirebaseAppIDKey];
  84. FIRInstanceIDTokenInfo *tokenInfo = [[FIRInstanceIDTokenInfo alloc]
  85. initWithAuthorizedEntity:authorizedEntity
  86. scope:scope
  87. token:token
  88. appVersion:FIRInstanceIDCurrentAppVersion()
  89. firebaseAppID:firebaseAppID];
  90. tokenInfo.APNSInfo = [[FIRInstanceIDAPNSInfo alloc] initWithTokenOptionsDictionary:options];
  91. [self.instanceIDStore
  92. saveTokenInfo:tokenInfo
  93. handler:^(NSError *error) {
  94. if (!error) {
  95. // Do not send the token back in case the save was unsuccessful. Since with
  96. // the new asychronous fetch mechanism this can lead to infinite loops, for
  97. // example, we will return a valid token even though we weren't able to store
  98. // it in our cache. The first token will lead to a onTokenRefresh callback
  99. // wherein the user again calls `getToken` but since we weren't able to save
  100. // it we won't hit the cache but hit the server again leading to an infinite
  101. // loop.
  102. FIRInstanceIDLoggerDebug(
  103. kFIRInstanceIDMessageCodeTokenManager001,
  104. @"Token fetch successful, token: %@, authorizedEntity: %@, scope:%@",
  105. token, authorizedEntity, scope);
  106. if (handler) {
  107. handler(token, nil);
  108. }
  109. } else {
  110. if (handler) {
  111. handler(nil, error);
  112. }
  113. }
  114. }];
  115. };
  116. // Add completion handler, and ensure it's called on the main queue
  117. [operation addCompletionHandler:^(FIRInstanceIDTokenOperationResult result,
  118. NSString *_Nullable token, NSError *_Nullable error) {
  119. dispatch_async(dispatch_get_main_queue(), ^{
  120. completion(result, token, error);
  121. });
  122. }];
  123. [self.tokenOperations addOperation:operation];
  124. }
  125. - (FIRInstanceIDTokenInfo *)cachedTokenInfoWithAuthorizedEntity:(NSString *)authorizedEntity
  126. scope:(NSString *)scope {
  127. return [self.instanceIDStore tokenInfoWithAuthorizedEntity:authorizedEntity scope:scope];
  128. }
  129. - (void)deleteTokenWithAuthorizedEntity:(NSString *)authorizedEntity
  130. scope:(NSString *)scope
  131. keyPair:(FIRInstanceIDKeyPair *)keyPair
  132. handler:(FIRInstanceIDDeleteTokenHandler)handler {
  133. if ([self.instanceIDStore tokenInfoWithAuthorizedEntity:authorizedEntity scope:scope]) {
  134. [self.instanceIDStore removeCachedTokenWithAuthorizedEntity:authorizedEntity scope:scope];
  135. }
  136. // Does not matter if we cannot find it in the cache. Still make an effort to unregister
  137. // from the server.
  138. FIRInstanceIDCheckinPreferences *checkinPreferences = self.authService.checkinPreferences;
  139. FIRInstanceIDTokenDeleteOperation *operation =
  140. [self createDeleteOperationWithAuthorizedEntity:authorizedEntity
  141. scope:scope
  142. checkinPreferences:checkinPreferences
  143. keyPair:keyPair
  144. action:FIRInstanceIDTokenActionDeleteToken];
  145. if (handler) {
  146. [operation addCompletionHandler:^(FIRInstanceIDTokenOperationResult result,
  147. NSString *_Nullable token, NSError *_Nullable error) {
  148. dispatch_async(dispatch_get_main_queue(), ^{
  149. handler(error);
  150. });
  151. }];
  152. }
  153. [self.tokenOperations addOperation:operation];
  154. }
  155. - (void)deleteAllTokensWithKeyPair:(FIRInstanceIDKeyPair *)keyPair
  156. handler:(FIRInstanceIDDeleteHandler)handler {
  157. // delete all tokens
  158. FIRInstanceIDCheckinPreferences *checkinPreferences = self.authService.checkinPreferences;
  159. if (!checkinPreferences) {
  160. // The checkin is already deleted. No need to trigger the token delete operation as client no
  161. // longer has the checkin information for server to delete.
  162. dispatch_async(dispatch_get_main_queue(), ^{
  163. handler(nil);
  164. });
  165. return;
  166. }
  167. FIRInstanceIDTokenDeleteOperation *operation =
  168. [self createDeleteOperationWithAuthorizedEntity:kFIRInstanceIDKeychainWildcardIdentifier
  169. scope:kFIRInstanceIDKeychainWildcardIdentifier
  170. checkinPreferences:checkinPreferences
  171. keyPair:keyPair
  172. action:FIRInstanceIDTokenActionDeleteTokenAndIID];
  173. if (handler) {
  174. [operation addCompletionHandler:^(FIRInstanceIDTokenOperationResult result,
  175. NSString *_Nullable token, NSError *_Nullable error) {
  176. dispatch_async(dispatch_get_main_queue(), ^{
  177. handler(error);
  178. });
  179. }];
  180. }
  181. [self.tokenOperations addOperation:operation];
  182. }
  183. - (void)deleteAllTokensLocallyWithHandler:(void (^)(NSError *error))handler {
  184. [self.instanceIDStore removeAllCachedTokensWithHandler:handler];
  185. }
  186. - (void)stopAllTokenOperations {
  187. [self.authService stopCheckinRequest];
  188. [self.tokenOperations cancelAllOperations];
  189. }
  190. #pragma mark - FIRInstanceIDStoreDelegate
  191. - (void)store:(FIRInstanceIDStore *)store
  192. didDeleteFCMScopedTokensForCheckin:(FIRInstanceIDCheckinPreferences *)checkin {
  193. // Make a best effort try to delete the old client related state on the FCM server. This is
  194. // required to delete old pubusb registrations which weren't cleared when the app was deleted.
  195. //
  196. // This is only a one time effort. If this call fails the client would still receive duplicate
  197. // pubsub notifications if he is again subscribed to the same topic.
  198. //
  199. // The client state should be cleared on the server for the provided checkin preferences.
  200. FIRInstanceIDTokenDeleteOperation *operation =
  201. [self createDeleteOperationWithAuthorizedEntity:nil
  202. scope:nil
  203. checkinPreferences:checkin
  204. keyPair:nil
  205. action:FIRInstanceIDTokenActionDeleteToken];
  206. [operation addCompletionHandler:^(FIRInstanceIDTokenOperationResult result,
  207. NSString *_Nullable token, NSError *_Nullable error) {
  208. if (error) {
  209. FIRInstanceIDMessageCode code =
  210. kFIRInstanceIDMessageCodeTokenManagerErrorDeletingFCMTokensOnAppReset;
  211. FIRInstanceIDLoggerDebug(code, @"Failed to delete GCM server registrations on app reset.");
  212. } else {
  213. FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeTokenManagerDeletedFCMTokensOnAppReset,
  214. @"Successfully deleted GCM server registrations on app reset");
  215. }
  216. }];
  217. [self.tokenOperations addOperation:operation];
  218. }
  219. #pragma mark - Unit Testing Stub Helpers
  220. // We really have this method so that we can more easily stub it out for unit testing
  221. - (FIRInstanceIDTokenFetchOperation *)
  222. createFetchOperationWithAuthorizedEntity:(NSString *)authorizedEntity
  223. scope:(NSString *)scope
  224. options:(NSDictionary<NSString *, NSString *> *)options
  225. keyPair:(FIRInstanceIDKeyPair *)keyPair {
  226. FIRInstanceIDCheckinPreferences *checkinPreferences = self.authService.checkinPreferences;
  227. FIRInstanceIDTokenFetchOperation *operation =
  228. [[FIRInstanceIDTokenFetchOperation alloc] initWithAuthorizedEntity:authorizedEntity
  229. scope:scope
  230. options:options
  231. checkinPreferences:checkinPreferences
  232. keyPair:keyPair];
  233. return operation;
  234. }
  235. // We really have this method so that we can more easily stub it out for unit testing
  236. - (FIRInstanceIDTokenDeleteOperation *)
  237. createDeleteOperationWithAuthorizedEntity:(NSString *)authorizedEntity
  238. scope:(NSString *)scope
  239. checkinPreferences:(FIRInstanceIDCheckinPreferences *)checkinPreferences
  240. keyPair:(FIRInstanceIDKeyPair *)keyPair
  241. action:(FIRInstanceIDTokenAction)action {
  242. FIRInstanceIDTokenDeleteOperation *operation =
  243. [[FIRInstanceIDTokenDeleteOperation alloc] initWithAuthorizedEntity:authorizedEntity
  244. scope:scope
  245. checkinPreferences:checkinPreferences
  246. keyPair:keyPair
  247. action:action];
  248. return operation;
  249. }
  250. #pragma mark - Invalidating Cached Tokens
  251. - (BOOL)checkTokenRefreshPolicyWithIID:(NSString *)IID {
  252. // We know at least one cached token exists.
  253. BOOL shouldFetchDefaultToken = NO;
  254. NSArray<FIRInstanceIDTokenInfo *> *tokenInfos = [self.instanceIDStore cachedTokenInfos];
  255. NSMutableArray<FIRInstanceIDTokenInfo *> *tokenInfosToDelete =
  256. [NSMutableArray arrayWithCapacity:tokenInfos.count];
  257. for (FIRInstanceIDTokenInfo *tokenInfo in tokenInfos) {
  258. BOOL isTokenFresh = [tokenInfo isFresh];
  259. if (isTokenFresh && [tokenInfo.token hasPrefix:IID]) {
  260. // Token is fresh and in right format, do nothing
  261. continue;
  262. }
  263. if ([tokenInfo.scope isEqualToString:kFIRInstanceIDDefaultTokenScope]) {
  264. // Default token is expired, do not mark for deletion. Fetch directly from server to
  265. // replace the current one.
  266. shouldFetchDefaultToken = YES;
  267. } else {
  268. // Non-default token is expired, mark for deletion.
  269. [tokenInfosToDelete addObject:tokenInfo];
  270. }
  271. FIRInstanceIDLoggerDebug(
  272. kFIRInstanceIDMessageCodeTokenManagerInvalidateStaleToken,
  273. @"Invalidating cached token for %@ (%@) due to token is no longer fresh.",
  274. tokenInfo.authorizedEntity, tokenInfo.scope);
  275. }
  276. for (FIRInstanceIDTokenInfo *tokenInfoToDelete in tokenInfosToDelete) {
  277. [self.instanceIDStore removeCachedTokenWithAuthorizedEntity:tokenInfoToDelete.authorizedEntity
  278. scope:tokenInfoToDelete.scope];
  279. }
  280. return shouldFetchDefaultToken;
  281. }
  282. - (NSArray<FIRInstanceIDTokenInfo *> *)updateTokensToAPNSDeviceToken:(NSData *)deviceToken
  283. isSandbox:(BOOL)isSandbox {
  284. // Each cached IID token that is missing an APNSInfo, or has an APNSInfo associated should be
  285. // checked and invalidated if needed.
  286. FIRInstanceIDAPNSInfo *APNSInfo = [[FIRInstanceIDAPNSInfo alloc] initWithDeviceToken:deviceToken
  287. isSandbox:isSandbox];
  288. if ([self.currentAPNSInfo isEqualToAPNSInfo:APNSInfo]) {
  289. return @[];
  290. }
  291. self.currentAPNSInfo = APNSInfo;
  292. NSArray<FIRInstanceIDTokenInfo *> *tokenInfos = [self.instanceIDStore cachedTokenInfos];
  293. NSMutableArray<FIRInstanceIDTokenInfo *> *tokenInfosToDelete =
  294. [NSMutableArray arrayWithCapacity:tokenInfos.count];
  295. for (FIRInstanceIDTokenInfo *cachedTokenInfo in tokenInfos) {
  296. // Check if the cached APNSInfo is nil, or if it is an old APNSInfo.
  297. if (!cachedTokenInfo.APNSInfo ||
  298. ![cachedTokenInfo.APNSInfo isEqualToAPNSInfo:self.currentAPNSInfo]) {
  299. // Mark for invalidation.
  300. [tokenInfosToDelete addObject:cachedTokenInfo];
  301. }
  302. }
  303. for (FIRInstanceIDTokenInfo *tokenInfoToDelete in tokenInfosToDelete) {
  304. FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeTokenManagerAPNSChangedTokenInvalidated,
  305. @"Invalidating cached token for %@ (%@) due to APNs token change.",
  306. tokenInfoToDelete.authorizedEntity, tokenInfoToDelete.scope);
  307. [self.instanceIDStore removeCachedTokenWithAuthorizedEntity:tokenInfoToDelete.authorizedEntity
  308. scope:tokenInfoToDelete.scope];
  309. }
  310. return tokenInfosToDelete;
  311. }
  312. @end