123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341 |
- /*
- * Copyright 2019 Google
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
- #import "FIRInstanceIDTokenManager.h"
-
- #import "FIRInstanceIDAuthKeyChain.h"
- #import "FIRInstanceIDAuthService.h"
- #import "FIRInstanceIDCheckinPreferences.h"
- #import "FIRInstanceIDConstants.h"
- #import "FIRInstanceIDDefines.h"
- #import "FIRInstanceIDLogger.h"
- #import "FIRInstanceIDStore.h"
- #import "FIRInstanceIDTokenDeleteOperation.h"
- #import "FIRInstanceIDTokenFetchOperation.h"
- #import "FIRInstanceIDTokenInfo.h"
- #import "FIRInstanceIDTokenOperation.h"
- #import "NSError+FIRInstanceID.h"
-
- @interface FIRInstanceIDTokenManager () <FIRInstanceIDStoreDelegate>
-
- @property(nonatomic, readwrite, strong) FIRInstanceIDStore *instanceIDStore;
- @property(nonatomic, readwrite, strong) FIRInstanceIDAuthService *authService;
- @property(nonatomic, readonly, strong) NSOperationQueue *tokenOperations;
-
- @property(nonatomic, readwrite, strong) FIRInstanceIDAPNSInfo *currentAPNSInfo;
-
- @end
-
- @implementation FIRInstanceIDTokenManager
-
- - (instancetype)init {
- self = [super init];
- if (self) {
- _instanceIDStore = [[FIRInstanceIDStore alloc] initWithDelegate:self];
- _authService = [[FIRInstanceIDAuthService alloc] initWithStore:_instanceIDStore];
- [self configureTokenOperations];
- }
- return self;
- }
-
- - (void)dealloc {
- [self stopAllTokenOperations];
- }
-
- - (void)configureTokenOperations {
- _tokenOperations = [[NSOperationQueue alloc] init];
- _tokenOperations.name = @"com.google.iid-token-operations";
- // For now, restrict the operations to be serial, because in some cases (like if the
- // authorized entity and scope are the same), order matters.
- // If we have to deal with several different token requests simultaneously, it would be a good
- // idea to add some better intelligence around this (performing unrelated token operations
- // simultaneously, etc.).
- _tokenOperations.maxConcurrentOperationCount = 1;
- if ([_tokenOperations respondsToSelector:@selector(qualityOfService)]) {
- _tokenOperations.qualityOfService = NSOperationQualityOfServiceUtility;
- }
- }
-
- - (void)fetchNewTokenWithAuthorizedEntity:(NSString *)authorizedEntity
- scope:(NSString *)scope
- keyPair:(FIRInstanceIDKeyPair *)keyPair
- options:(NSDictionary *)options
- handler:(FIRInstanceIDTokenHandler)handler {
- FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeTokenManager000,
- @"Fetch new token for authorizedEntity: %@, scope: %@", authorizedEntity,
- scope);
- FIRInstanceIDTokenFetchOperation *operation =
- [self createFetchOperationWithAuthorizedEntity:authorizedEntity
- scope:scope
- options:options
- keyPair:keyPair];
- FIRInstanceID_WEAKIFY(self);
- FIRInstanceIDTokenOperationCompletion completion =
- ^(FIRInstanceIDTokenOperationResult result, NSString *_Nullable token,
- NSError *_Nullable error) {
- FIRInstanceID_STRONGIFY(self);
- if (error) {
- handler(nil, error);
- return;
- }
- NSString *firebaseAppID = options[kFIRInstanceIDTokenOptionsFirebaseAppIDKey];
- FIRInstanceIDTokenInfo *tokenInfo = [[FIRInstanceIDTokenInfo alloc]
- initWithAuthorizedEntity:authorizedEntity
- scope:scope
- token:token
- appVersion:FIRInstanceIDCurrentAppVersion()
- firebaseAppID:firebaseAppID];
- tokenInfo.APNSInfo = [[FIRInstanceIDAPNSInfo alloc] initWithTokenOptionsDictionary:options];
-
- [self.instanceIDStore
- saveTokenInfo:tokenInfo
- handler:^(NSError *error) {
- if (!error) {
- // Do not send the token back in case the save was unsuccessful. Since with
- // the new asychronous fetch mechanism this can lead to infinite loops, for
- // example, we will return a valid token even though we weren't able to store
- // it in our cache. The first token will lead to a onTokenRefresh callback
- // wherein the user again calls `getToken` but since we weren't able to save
- // it we won't hit the cache but hit the server again leading to an infinite
- // loop.
- FIRInstanceIDLoggerDebug(
- kFIRInstanceIDMessageCodeTokenManager001,
- @"Token fetch successful, token: %@, authorizedEntity: %@, scope:%@",
- token, authorizedEntity, scope);
-
- if (handler) {
- handler(token, nil);
- }
- } else {
- if (handler) {
- handler(nil, error);
- }
- }
- }];
- };
- // Add completion handler, and ensure it's called on the main queue
- [operation addCompletionHandler:^(FIRInstanceIDTokenOperationResult result,
- NSString *_Nullable token, NSError *_Nullable error) {
- dispatch_async(dispatch_get_main_queue(), ^{
- completion(result, token, error);
- });
- }];
- [self.tokenOperations addOperation:operation];
- }
-
- - (FIRInstanceIDTokenInfo *)cachedTokenInfoWithAuthorizedEntity:(NSString *)authorizedEntity
- scope:(NSString *)scope {
- return [self.instanceIDStore tokenInfoWithAuthorizedEntity:authorizedEntity scope:scope];
- }
-
- - (void)deleteTokenWithAuthorizedEntity:(NSString *)authorizedEntity
- scope:(NSString *)scope
- keyPair:(FIRInstanceIDKeyPair *)keyPair
- handler:(FIRInstanceIDDeleteTokenHandler)handler {
- if ([self.instanceIDStore tokenInfoWithAuthorizedEntity:authorizedEntity scope:scope]) {
- [self.instanceIDStore removeCachedTokenWithAuthorizedEntity:authorizedEntity scope:scope];
- }
- // Does not matter if we cannot find it in the cache. Still make an effort to unregister
- // from the server.
- FIRInstanceIDCheckinPreferences *checkinPreferences = self.authService.checkinPreferences;
- FIRInstanceIDTokenDeleteOperation *operation =
- [self createDeleteOperationWithAuthorizedEntity:authorizedEntity
- scope:scope
- checkinPreferences:checkinPreferences
- keyPair:keyPair
- action:FIRInstanceIDTokenActionDeleteToken];
-
- if (handler) {
- [operation addCompletionHandler:^(FIRInstanceIDTokenOperationResult result,
- NSString *_Nullable token, NSError *_Nullable error) {
- dispatch_async(dispatch_get_main_queue(), ^{
- handler(error);
- });
- }];
- }
- [self.tokenOperations addOperation:operation];
- }
-
- - (void)deleteAllTokensWithKeyPair:(FIRInstanceIDKeyPair *)keyPair
- handler:(FIRInstanceIDDeleteHandler)handler {
- // delete all tokens
- FIRInstanceIDCheckinPreferences *checkinPreferences = self.authService.checkinPreferences;
- if (!checkinPreferences) {
- // The checkin is already deleted. No need to trigger the token delete operation as client no
- // longer has the checkin information for server to delete.
- dispatch_async(dispatch_get_main_queue(), ^{
- handler(nil);
- });
- return;
- }
- FIRInstanceIDTokenDeleteOperation *operation =
- [self createDeleteOperationWithAuthorizedEntity:kFIRInstanceIDKeychainWildcardIdentifier
- scope:kFIRInstanceIDKeychainWildcardIdentifier
- checkinPreferences:checkinPreferences
- keyPair:keyPair
- action:FIRInstanceIDTokenActionDeleteTokenAndIID];
- if (handler) {
- [operation addCompletionHandler:^(FIRInstanceIDTokenOperationResult result,
- NSString *_Nullable token, NSError *_Nullable error) {
- dispatch_async(dispatch_get_main_queue(), ^{
- handler(error);
- });
- }];
- }
- [self.tokenOperations addOperation:operation];
- }
-
- - (void)deleteAllTokensLocallyWithHandler:(void (^)(NSError *error))handler {
- [self.instanceIDStore removeAllCachedTokensWithHandler:handler];
- }
-
- - (void)stopAllTokenOperations {
- [self.authService stopCheckinRequest];
- [self.tokenOperations cancelAllOperations];
- }
-
- #pragma mark - FIRInstanceIDStoreDelegate
-
- - (void)store:(FIRInstanceIDStore *)store
- didDeleteFCMScopedTokensForCheckin:(FIRInstanceIDCheckinPreferences *)checkin {
- // Make a best effort try to delete the old client related state on the FCM server. This is
- // required to delete old pubusb registrations which weren't cleared when the app was deleted.
- //
- // This is only a one time effort. If this call fails the client would still receive duplicate
- // pubsub notifications if he is again subscribed to the same topic.
- //
- // The client state should be cleared on the server for the provided checkin preferences.
- FIRInstanceIDTokenDeleteOperation *operation =
- [self createDeleteOperationWithAuthorizedEntity:nil
- scope:nil
- checkinPreferences:checkin
- keyPair:nil
- action:FIRInstanceIDTokenActionDeleteToken];
- [operation addCompletionHandler:^(FIRInstanceIDTokenOperationResult result,
- NSString *_Nullable token, NSError *_Nullable error) {
- if (error) {
- FIRInstanceIDMessageCode code =
- kFIRInstanceIDMessageCodeTokenManagerErrorDeletingFCMTokensOnAppReset;
- FIRInstanceIDLoggerDebug(code, @"Failed to delete GCM server registrations on app reset.");
- } else {
- FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeTokenManagerDeletedFCMTokensOnAppReset,
- @"Successfully deleted GCM server registrations on app reset");
- }
- }];
-
- [self.tokenOperations addOperation:operation];
- }
-
- #pragma mark - Unit Testing Stub Helpers
- // We really have this method so that we can more easily stub it out for unit testing
- - (FIRInstanceIDTokenFetchOperation *)
- createFetchOperationWithAuthorizedEntity:(NSString *)authorizedEntity
- scope:(NSString *)scope
- options:(NSDictionary<NSString *, NSString *> *)options
- keyPair:(FIRInstanceIDKeyPair *)keyPair {
- FIRInstanceIDCheckinPreferences *checkinPreferences = self.authService.checkinPreferences;
- FIRInstanceIDTokenFetchOperation *operation =
- [[FIRInstanceIDTokenFetchOperation alloc] initWithAuthorizedEntity:authorizedEntity
- scope:scope
- options:options
- checkinPreferences:checkinPreferences
- keyPair:keyPair];
- return operation;
- }
-
- // We really have this method so that we can more easily stub it out for unit testing
- - (FIRInstanceIDTokenDeleteOperation *)
- createDeleteOperationWithAuthorizedEntity:(NSString *)authorizedEntity
- scope:(NSString *)scope
- checkinPreferences:(FIRInstanceIDCheckinPreferences *)checkinPreferences
- keyPair:(FIRInstanceIDKeyPair *)keyPair
- action:(FIRInstanceIDTokenAction)action {
- FIRInstanceIDTokenDeleteOperation *operation =
- [[FIRInstanceIDTokenDeleteOperation alloc] initWithAuthorizedEntity:authorizedEntity
- scope:scope
- checkinPreferences:checkinPreferences
- keyPair:keyPair
- action:action];
- return operation;
- }
-
- #pragma mark - Invalidating Cached Tokens
- - (BOOL)checkTokenRefreshPolicyWithIID:(NSString *)IID {
- // We know at least one cached token exists.
- BOOL shouldFetchDefaultToken = NO;
- NSArray<FIRInstanceIDTokenInfo *> *tokenInfos = [self.instanceIDStore cachedTokenInfos];
-
- NSMutableArray<FIRInstanceIDTokenInfo *> *tokenInfosToDelete =
- [NSMutableArray arrayWithCapacity:tokenInfos.count];
- for (FIRInstanceIDTokenInfo *tokenInfo in tokenInfos) {
- BOOL isTokenFresh = [tokenInfo isFresh];
- if (isTokenFresh && [tokenInfo.token hasPrefix:IID]) {
- // Token is fresh and in right format, do nothing
- continue;
- }
- if ([tokenInfo.scope isEqualToString:kFIRInstanceIDDefaultTokenScope]) {
- // Default token is expired, do not mark for deletion. Fetch directly from server to
- // replace the current one.
- shouldFetchDefaultToken = YES;
- } else {
- // Non-default token is expired, mark for deletion.
- [tokenInfosToDelete addObject:tokenInfo];
- }
- FIRInstanceIDLoggerDebug(
- kFIRInstanceIDMessageCodeTokenManagerInvalidateStaleToken,
- @"Invalidating cached token for %@ (%@) due to token is no longer fresh.",
- tokenInfo.authorizedEntity, tokenInfo.scope);
- }
- for (FIRInstanceIDTokenInfo *tokenInfoToDelete in tokenInfosToDelete) {
- [self.instanceIDStore removeCachedTokenWithAuthorizedEntity:tokenInfoToDelete.authorizedEntity
- scope:tokenInfoToDelete.scope];
- }
- return shouldFetchDefaultToken;
- }
-
- - (NSArray<FIRInstanceIDTokenInfo *> *)updateTokensToAPNSDeviceToken:(NSData *)deviceToken
- isSandbox:(BOOL)isSandbox {
- // Each cached IID token that is missing an APNSInfo, or has an APNSInfo associated should be
- // checked and invalidated if needed.
- FIRInstanceIDAPNSInfo *APNSInfo = [[FIRInstanceIDAPNSInfo alloc] initWithDeviceToken:deviceToken
- isSandbox:isSandbox];
- if ([self.currentAPNSInfo isEqualToAPNSInfo:APNSInfo]) {
- return @[];
- }
- self.currentAPNSInfo = APNSInfo;
-
- NSArray<FIRInstanceIDTokenInfo *> *tokenInfos = [self.instanceIDStore cachedTokenInfos];
- NSMutableArray<FIRInstanceIDTokenInfo *> *tokenInfosToDelete =
- [NSMutableArray arrayWithCapacity:tokenInfos.count];
- for (FIRInstanceIDTokenInfo *cachedTokenInfo in tokenInfos) {
- // Check if the cached APNSInfo is nil, or if it is an old APNSInfo.
- if (!cachedTokenInfo.APNSInfo ||
- ![cachedTokenInfo.APNSInfo isEqualToAPNSInfo:self.currentAPNSInfo]) {
- // Mark for invalidation.
- [tokenInfosToDelete addObject:cachedTokenInfo];
- }
- }
- for (FIRInstanceIDTokenInfo *tokenInfoToDelete in tokenInfosToDelete) {
- FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeTokenManagerAPNSChangedTokenInvalidated,
- @"Invalidating cached token for %@ (%@) due to APNs token change.",
- tokenInfoToDelete.authorizedEntity, tokenInfoToDelete.scope);
- [self.instanceIDStore removeCachedTokenWithAuthorizedEntity:tokenInfoToDelete.authorizedEntity
- scope:tokenInfoToDelete.scope];
- }
- return tokenInfosToDelete;
- }
-
- @end
|