341 lines
16 KiB
Objective-C
341 lines
16 KiB
Objective-C
/*
|
|
* 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
|