You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

340 lines
16 KiB

/*
* 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
instanceID:(NSString *)instanceID
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
instanceID:instanceID];
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
instanceID:(NSString *)instanceID
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
instanceID:instanceID
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)deleteAllTokensWithInstanceID:(NSString *)instanceID
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
instanceID:instanceID
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
instanceID: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
instanceID:(NSString *)instanceID {
FIRInstanceIDCheckinPreferences *checkinPreferences = self.authService.checkinPreferences;
FIRInstanceIDTokenFetchOperation *operation =
[[FIRInstanceIDTokenFetchOperation alloc] initWithAuthorizedEntity:authorizedEntity
scope:scope
options:options
checkinPreferences:checkinPreferences
instanceID:instanceID];
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
instanceID:(NSString *)instanceID
action:(FIRInstanceIDTokenAction)action {
FIRInstanceIDTokenDeleteOperation *operation =
[[FIRInstanceIDTokenDeleteOperation alloc] initWithAuthorizedEntity:authorizedEntity
scope:scope
checkinPreferences:checkinPreferences
instanceID:instanceID
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) {
if ([tokenInfo isFreshWithIID:IID]) {
// Token is fresh and in right format, do nothing
continue;
}
if ([tokenInfo isDefaultToken]) {
// 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