Files
app-check/AppCheck/Sources/Core/GACAppCheck.m
T
Andrew Heard d12ab82796 Remove Firebase-specific implementation from GACAppCheckAPIService (#11324)
- Removed the unused `appID` property from `GACAppCheckAPIService`.
- Removed heartbeat logging tests from `GACAppCheckAPIServiceTests`.
- Added request hook, additional headers and API key tests to `GACAppCheckAPIServiceTests`.
2023-07-20 10:35:11 -04:00

330 lines
13 KiB
Objective-C

/*
* Copyright 2020 Google LLC
*
* 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 "AppCheck/Sources/Public/AppCheck/GACAppCheck.h"
#if __has_include(<FBLPromises/FBLPromises.h>)
#import <FBLPromises/FBLPromises.h>
#else
#import "FBLPromises.h"
#endif
#import "AppCheck/Sources/Public/AppCheck/GACAppCheckErrors.h"
#import "AppCheck/Sources/Public/AppCheck/GACAppCheckProvider.h"
#import "AppCheck/Sources/Public/AppCheck/GACAppCheckSettings.h"
#import "AppCheck/Sources/Core/Errors/GACAppCheckErrorUtil.h"
#import "AppCheck/Sources/Core/GACAppCheckLogger.h"
#import "AppCheck/Sources/Core/GACAppCheckToken+Internal.h"
#import "AppCheck/Sources/Core/GACAppCheckTokenResult.h"
#import "AppCheck/Sources/Core/Storage/GACAppCheckStorage.h"
#import "AppCheck/Sources/Core/TokenRefresh/GACAppCheckTokenRefreshResult.h"
#import "AppCheck/Sources/Core/TokenRefresh/GACAppCheckTokenRefresher.h"
#import "AppCheck/Interop/GACAppCheckInterop.h"
#import "AppCheck/Interop/GACAppCheckTokenResultInterop.h"
#import "FirebaseCore/Extension/FirebaseCoreInternal.h"
NS_ASSUME_NONNULL_BEGIN
/// A notification with the specified name is sent to the default notification center
/// (`NotificationCenter.default`) each time a Firebase app check token is refreshed.
/// The user info dictionary contains `kGACAppCheckTokenNotificationKey` and
/// `kGACAppCheckAppNameNotificationKey` keys.
const NSNotificationName GACAppCheckAppCheckTokenDidChangeNotification =
@"GACAppCheckAppCheckTokenDidChangeNotification";
/// `userInfo` key for the `AppCheckToken` in `appCheckTokenRefreshNotification`.
NSString *const kGACAppCheckTokenNotificationKey = @"GACAppCheckTokenNotificationKey";
/// `userInfo` key for the `FirebaseApp.name` in `appCheckTokenRefreshNotification`.
NSString *const kGACAppCheckAppNameNotificationKey = @"GACAppCheckAppNameNotificationKey";
// TODO(andrewheard): Remove from generic App Check SDK.
// FIREBASE_APP_CHECK_ONLY_BEGIN
NSString *const kGACAppCheckTokenAutoRefreshEnabledUserDefaultsPrefix =
@"GACAppCheckTokenAutoRefreshEnabled_";
NSString *const kGACAppCheckTokenAutoRefreshEnabledInfoPlistKey =
@"FirebaseAppCheckTokenAutoRefreshEnabled";
// FIREBASE_APP_CHECK_ONLY_END
static const NSTimeInterval kTokenExpirationThreshold = 5 * 60; // 5 min.
static NSString *const kDummyFACTokenValue = @"eyJlcnJvciI6IlVOS05PV05fRVJST1IifQ==";
@interface GACAppCheck ()
@property(nonatomic, readonly) NSString *appName;
@property(nonatomic, readonly) id<GACAppCheckProvider> appCheckProvider;
@property(nonatomic, readonly) id<GACAppCheckStorageProtocol> storage;
@property(nonatomic, readonly) NSNotificationCenter *notificationCenter;
@property(nonatomic, readonly) id<GACAppCheckSettingsProtocol> settings;
@property(nonatomic, readonly, nullable) id<GACAppCheckTokenRefresherProtocol> tokenRefresher;
@property(nonatomic, nullable) FBLPromise<GACAppCheckToken *> *ongoingRetrieveOrRefreshTokenPromise;
@end
@implementation GACAppCheck
#pragma mark - Internal
- (instancetype)initWithAppName:(NSString *)appName
appCheckProvider:(id<GACAppCheckProvider>)appCheckProvider
storage:(id<GACAppCheckStorageProtocol>)storage
tokenRefresher:(id<GACAppCheckTokenRefresherProtocol>)tokenRefresher
notificationCenter:(NSNotificationCenter *)notificationCenter
settings:(id<GACAppCheckSettingsProtocol>)settings {
self = [super init];
if (self) {
_appName = appName;
_appCheckProvider = appCheckProvider;
_storage = storage;
_tokenRefresher = tokenRefresher;
_notificationCenter = notificationCenter;
_settings = settings;
__auto_type __weak weakSelf = self;
tokenRefresher.tokenRefreshHandler = ^(GACAppCheckTokenRefreshCompletion _Nonnull completion) {
__auto_type strongSelf = weakSelf;
[strongSelf periodicTokenRefreshWithCompletion:completion];
};
}
return self;
}
#pragma mark - Public
- (instancetype)initWithApp:(FIRApp *)app
appCheckProvider:(id<GACAppCheckProvider>)appCheckProvider
settings:(id<GACAppCheckSettingsProtocol>)settings {
GACAppCheckTokenRefreshResult *refreshResult =
[[GACAppCheckTokenRefreshResult alloc] initWithStatusNever];
GACAppCheckTokenRefresher *tokenRefresher =
[[GACAppCheckTokenRefresher alloc] initWithRefreshResult:refreshResult settings:settings];
// TODO(andrewheard): Remove from generic App Check SDK.
// FIREBASE_APP_CHECK_ONLY_BEGIN
NSString *tokenKey =
[NSString stringWithFormat:@"app_check_token.%@.%@", app.name, app.options.googleAppID];
// FIREBASE_APP_CHECK_ONLY_END
GACAppCheckStorage *storage =
[[GACAppCheckStorage alloc] initWithTokenKey:tokenKey accessGroup:app.options.appGroupID];
return [self initWithAppName:app.name
appCheckProvider:appCheckProvider
storage:storage
tokenRefresher:tokenRefresher
notificationCenter:NSNotificationCenter.defaultCenter
settings:settings];
}
- (void)tokenForcingRefresh:(BOOL)forcingRefresh
completion:(void (^)(GACAppCheckToken *_Nullable token,
NSError *_Nullable error))handler {
[self retrieveOrRefreshTokenForcingRefresh:forcingRefresh]
.then(^id _Nullable(GACAppCheckToken *token) {
handler(token, nil);
return token;
})
.catch(^(NSError *_Nonnull error) {
handler(nil, [GACAppCheckErrorUtil publicDomainErrorWithError:error]);
});
}
- (void)limitedUseTokenWithCompletion:(void (^)(GACAppCheckToken *_Nullable token,
NSError *_Nullable error))handler {
[self limitedUseToken]
.then(^id _Nullable(GACAppCheckToken *token) {
handler(token, nil);
return token;
})
.catch(^(NSError *_Nonnull error) {
handler(nil, [GACAppCheckErrorUtil publicDomainErrorWithError:error]);
});
}
- (void)setIsTokenAutoRefreshEnabled:(BOOL)isTokenAutoRefreshEnabled {
self.settings.isTokenAutoRefreshEnabled = isTokenAutoRefreshEnabled;
}
- (BOOL)isTokenAutoRefreshEnabled {
return self.settings.isTokenAutoRefreshEnabled;
}
#pragma mark - GACAppCheckInterop
- (void)getTokenForcingRefresh:(BOOL)forcingRefresh
completion:(GACAppCheckTokenHandlerInterop)handler {
[self retrieveOrRefreshTokenForcingRefresh:forcingRefresh]
.then(^id _Nullable(GACAppCheckToken *token) {
GACAppCheckTokenResult *result = [[GACAppCheckTokenResult alloc] initWithToken:token.token
error:nil];
handler(result);
return result;
})
.catch(^(NSError *_Nonnull error) {
GACAppCheckTokenResult *result =
[[GACAppCheckTokenResult alloc] initWithToken:kDummyFACTokenValue error:error];
handler(result);
});
}
- (void)getLimitedUseTokenWithCompletion:(GACAppCheckTokenHandlerInterop)handler {
[self limitedUseToken]
.then(^id _Nullable(GACAppCheckToken *token) {
GACAppCheckTokenResult *result = [[GACAppCheckTokenResult alloc] initWithToken:token.token
error:nil];
handler(result);
return result;
})
.catch(^(NSError *_Nonnull error) {
GACAppCheckTokenResult *result =
[[GACAppCheckTokenResult alloc] initWithToken:kDummyFACTokenValue error:error];
handler(result);
});
}
- (nonnull NSString *)tokenDidChangeNotificationName {
return GACAppCheckAppCheckTokenDidChangeNotification;
}
- (nonnull NSString *)notificationAppNameKey {
return kGACAppCheckAppNameNotificationKey;
}
- (nonnull NSString *)notificationTokenKey {
return kGACAppCheckTokenNotificationKey;
}
#pragma mark - FAA token cache
- (FBLPromise<GACAppCheckToken *> *)retrieveOrRefreshTokenForcingRefresh:(BOOL)forcingRefresh {
return [FBLPromise do:^id _Nullable {
if (self.ongoingRetrieveOrRefreshTokenPromise == nil) {
// Kick off a new operation only when there is not an ongoing one.
self.ongoingRetrieveOrRefreshTokenPromise =
[self createRetrieveOrRefreshTokenPromiseForcingRefresh:forcingRefresh]
// Release the ongoing operation promise on completion.
.then(^GACAppCheckToken *(GACAppCheckToken *token) {
self.ongoingRetrieveOrRefreshTokenPromise = nil;
return token;
})
.recover(^NSError *(NSError *error) {
self.ongoingRetrieveOrRefreshTokenPromise = nil;
return error;
});
}
return self.ongoingRetrieveOrRefreshTokenPromise;
}];
}
- (FBLPromise<GACAppCheckToken *> *)createRetrieveOrRefreshTokenPromiseForcingRefresh:
(BOOL)forcingRefresh {
return [self getCachedValidTokenForcingRefresh:forcingRefresh].recover(
^id _Nullable(NSError *_Nonnull error) {
return [self refreshToken];
});
}
- (FBLPromise<GACAppCheckToken *> *)getCachedValidTokenForcingRefresh:(BOOL)forcingRefresh {
if (forcingRefresh) {
FBLPromise *rejectedPromise = [FBLPromise pendingPromise];
[rejectedPromise reject:[GACAppCheckErrorUtil cachedTokenNotFound]];
return rejectedPromise;
}
return [self.storage getToken].then(^id(GACAppCheckToken *_Nullable token) {
if (token == nil) {
return [GACAppCheckErrorUtil cachedTokenNotFound];
}
BOOL isTokenExpiredOrExpiresSoon =
[token.expirationDate timeIntervalSinceNow] < kTokenExpirationThreshold;
if (isTokenExpiredOrExpiresSoon) {
return [GACAppCheckErrorUtil cachedTokenExpired];
}
return token;
});
}
- (FBLPromise<GACAppCheckToken *> *)refreshToken {
return [FBLPromise
wrapObjectOrErrorCompletion:^(FBLPromiseObjectOrErrorCompletion _Nonnull handler) {
[self.appCheckProvider getTokenWithCompletion:handler];
}]
.then(^id _Nullable(GACAppCheckToken *_Nullable token) {
return [self.storage setToken:token];
})
.then(^id _Nullable(GACAppCheckToken *_Nullable token) {
// TODO: Make sure the self.tokenRefresher is updated only once. Currently the timer will be
// updated twice in the case when the refresh triggered by self.tokenRefresher, but it
// should be fine for now as it is a relatively cheap operation.
__auto_type refreshResult = [[GACAppCheckTokenRefreshResult alloc]
initWithStatusSuccessAndExpirationDate:token.expirationDate
receivedAtDate:token.receivedAtDate];
[self.tokenRefresher updateWithRefreshResult:refreshResult];
[self postTokenUpdateNotificationWithToken:token];
return token;
});
}
- (FBLPromise<GACAppCheckToken *> *)limitedUseToken {
return
[FBLPromise wrapObjectOrErrorCompletion:^(
FBLPromiseObjectOrErrorCompletion _Nonnull handler) {
[self.appCheckProvider getTokenWithCompletion:handler];
}].then(^id _Nullable(GACAppCheckToken *_Nullable token) {
return token;
});
}
#pragma mark - Token auto refresh
- (void)periodicTokenRefreshWithCompletion:(GACAppCheckTokenRefreshCompletion)completion {
[self retrieveOrRefreshTokenForcingRefresh:NO]
.then(^id _Nullable(GACAppCheckToken *_Nullable token) {
__auto_type refreshResult = [[GACAppCheckTokenRefreshResult alloc]
initWithStatusSuccessAndExpirationDate:token.expirationDate
receivedAtDate:token.receivedAtDate];
completion(refreshResult);
return nil;
})
.catch(^(NSError *error) {
__auto_type refreshResult = [[GACAppCheckTokenRefreshResult alloc] initWithStatusFailure];
completion(refreshResult);
});
}
#pragma mark - Token update notification
- (void)postTokenUpdateNotificationWithToken:(GACAppCheckToken *)token {
[self.notificationCenter postNotificationName:GACAppCheckAppCheckTokenDidChangeNotification
object:self
userInfo:@{
kGACAppCheckTokenNotificationKey : token.token,
kGACAppCheckAppNameNotificationKey : self.appName
}];
}
@end
NS_ASSUME_NONNULL_END