From ff204f33b5b8bfa4786c7757a73d3824f7470175 Mon Sep 17 00:00:00 2001 From: Maksym Malyhin Date: Mon, 26 Apr 2021 18:39:10 -0400 Subject: [PATCH 01/37] App Attest provider: attestation sequence (#7971) * App Attest provider: attestation sequence (#761) * App Attest draft WIP * FIRAppAttestProvider initializers * ./scripts/style.sh * FIRAppAttestProvider implementation draft * Basic FIRAppAttestProviderTests and fixes * style * testGetTokenWhenAppAttestIsNotSupported * More FIRAppAttestProviderTests * Cleanup * Remove unused file * Availability annotations on DCAppAttestService category. * Guard FIRAppAttestProvider with #if TARGET_OS_IOS * Formatting * Fix SPM * app_check.yaml: Add diagnostics SPM builds * fix yaml * Fix Firebase-Package scheme bad merge * Fix typo * FIRAppAttestProvider: hide default init * FIRAppAttestKeyIDStorage: methods placeholders * Comments * Fix updated block definition --- .../API/FIRAppAttestAPIService.h | 45 ++ .../API/FIRAppAttestAPIService.m | 63 +++ .../DCAppAttestService+FIRAppAttestService.h | 34 ++ .../DCAppAttestService+FIRAppAttestService.m | 26 ++ .../AppAttestProvider/FIRAppAttestProvider.m | 216 ++++++++++ .../AppAttestProvider/FIRAppAttestService.h | 36 ++ .../Storage/FIRAppAttestKeyIDStorage.h | 47 +++ .../Storage/FIRAppAttestKeyIDStorage.m | 53 +++ .../Core/Errors/FIRAppCheckErrorUtil.h | 22 +- .../Core/Errors/FIRAppCheckErrorUtil.m | 10 + .../API/FIRDeviceCheckAPIService.m | 1 - .../FIRDeviceCheckProvider.m | 2 - .../FirebaseAppCheck/FIRAppAttestProvider.h | 36 ++ .../FIRAppAttestProviderTests.m | 388 ++++++++++++++++++ 14 files changed, 971 insertions(+), 8 deletions(-) create mode 100644 FirebaseAppCheck/Sources/AppAttestProvider/API/FIRAppAttestAPIService.h create mode 100644 FirebaseAppCheck/Sources/AppAttestProvider/API/FIRAppAttestAPIService.m create mode 100644 FirebaseAppCheck/Sources/AppAttestProvider/DCAppAttestService+FIRAppAttestService.h create mode 100644 FirebaseAppCheck/Sources/AppAttestProvider/DCAppAttestService+FIRAppAttestService.m create mode 100644 FirebaseAppCheck/Sources/AppAttestProvider/FIRAppAttestProvider.m create mode 100644 FirebaseAppCheck/Sources/AppAttestProvider/FIRAppAttestService.h create mode 100644 FirebaseAppCheck/Sources/AppAttestProvider/Storage/FIRAppAttestKeyIDStorage.h create mode 100644 FirebaseAppCheck/Sources/AppAttestProvider/Storage/FIRAppAttestKeyIDStorage.m create mode 100644 FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRAppAttestProvider.h create mode 100644 FirebaseAppCheck/Tests/Unit/AppAttestProvider/FIRAppAttestProviderTests.m diff --git a/FirebaseAppCheck/Sources/AppAttestProvider/API/FIRAppAttestAPIService.h b/FirebaseAppCheck/Sources/AppAttestProvider/API/FIRAppAttestAPIService.h new file mode 100644 index 00000000000..860b2224d0f --- /dev/null +++ b/FirebaseAppCheck/Sources/AppAttestProvider/API/FIRAppAttestAPIService.h @@ -0,0 +1,45 @@ +/* + * Copyright 2021 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 + +@class FBLPromise; +@class FIRAppCheckToken; +@protocol FIRAppCheckAPIServiceProtocol; + +NS_ASSUME_NONNULL_BEGIN + +@protocol FIRAppAttestAPIServiceProtocol + +/// Request a random challenge from server. +- (FBLPromise *)getRandomChallenge; + +/// Exchanges attestation data to FAC token. +- (FBLPromise *)appCheckTokenWithAttestation:(NSData *)attestation + keyID:(NSString *)keyID + challenge:(NSData *)challenge; + +@end + +@interface FIRAppAttestAPIService : NSObject + +- (instancetype)initWithAPIService:(id)APIService + projectID:(NSString *)projectID + appID:(NSString *)appID; + +@end + +NS_ASSUME_NONNULL_END diff --git a/FirebaseAppCheck/Sources/AppAttestProvider/API/FIRAppAttestAPIService.m b/FirebaseAppCheck/Sources/AppAttestProvider/API/FIRAppAttestAPIService.m new file mode 100644 index 00000000000..f05ae648bde --- /dev/null +++ b/FirebaseAppCheck/Sources/AppAttestProvider/API/FIRAppAttestAPIService.m @@ -0,0 +1,63 @@ +/* + * Copyright 2021 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 "FirebaseAppCheck/Sources/AppAttestProvider/API/FIRAppAttestAPIService.h" + +#if __has_include() +#import +#else +#import "FBLPromises.h" +#endif + +#import "FirebaseAppCheck/Sources/Core/APIService/FIRAppCheckAPIService.h" + +@interface FIRAppAttestAPIService () + +@property(nonatomic, readonly) id APIService; + +@property(nonatomic, readonly) NSString *projectID; +@property(nonatomic, readonly) NSString *appID; + +@end + +@implementation FIRAppAttestAPIService + +- (instancetype)initWithAPIService:(id)APIService + projectID:(NSString *)projectID + appID:(NSString *)appID { + self = [super init]; + if (self) { + _APIService = APIService; + _projectID = projectID; + _appID = appID; + } + return self; +} + +- (nonnull FBLPromise *) + appCheckTokenWithAttestation:(nonnull NSData *)attestation + keyID:(nonnull NSString *)keyID + challenge:(nonnull NSData *)challenge { + // TODO: Implement. + return [FBLPromise resolvedWith:nil]; +} + +- (nonnull FBLPromise *)getRandomChallenge { + // TODO: Implement. + return [FBLPromise resolvedWith:nil]; +} + +@end diff --git a/FirebaseAppCheck/Sources/AppAttestProvider/DCAppAttestService+FIRAppAttestService.h b/FirebaseAppCheck/Sources/AppAttestProvider/DCAppAttestService+FIRAppAttestService.h new file mode 100644 index 00000000000..92d147a4a1a --- /dev/null +++ b/FirebaseAppCheck/Sources/AppAttestProvider/DCAppAttestService+FIRAppAttestService.h @@ -0,0 +1,34 @@ +/* + * Copyright 2021 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. + */ + +// Currently DCAppAttestService is available on iOS only. +#if TARGET_OS_IOS + +#import + +#import "FirebaseAppCheck/Sources/AppAttestProvider/FIRAppAttestService.h" + +NS_ASSUME_NONNULL_BEGIN + +API_AVAILABLE(ios(14.0)) +API_UNAVAILABLE(macos, tvos, watchos) +@interface DCAppAttestService (FIRAppAttestService) + +@end + +NS_ASSUME_NONNULL_END + +#endif // TARGET_OS_IOS diff --git a/FirebaseAppCheck/Sources/AppAttestProvider/DCAppAttestService+FIRAppAttestService.m b/FirebaseAppCheck/Sources/AppAttestProvider/DCAppAttestService+FIRAppAttestService.m new file mode 100644 index 00000000000..412221d4b2b --- /dev/null +++ b/FirebaseAppCheck/Sources/AppAttestProvider/DCAppAttestService+FIRAppAttestService.m @@ -0,0 +1,26 @@ +/* + * Copyright 2021 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 "FirebaseAppCheck/Sources/AppAttestProvider/DCAppAttestService+FIRAppAttestService.h" + +// Currently DCAppAttestService is available on iOS only. +#if TARGET_OS_IOS + +@implementation DCAppAttestService (FIRAppAttestService) + +@end + +#endif // TARGET_OS_IOS diff --git a/FirebaseAppCheck/Sources/AppAttestProvider/FIRAppAttestProvider.m b/FirebaseAppCheck/Sources/AppAttestProvider/FIRAppAttestProvider.m new file mode 100644 index 00000000000..be4e4be35ea --- /dev/null +++ b/FirebaseAppCheck/Sources/AppAttestProvider/FIRAppAttestProvider.m @@ -0,0 +1,216 @@ +/* + * Copyright 2021 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 "FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRAppAttestProvider.h" + +#import "FirebaseAppCheck/Sources/AppAttestProvider/DCAppAttestService+FIRAppAttestService.h" + +#if __has_include() +#import +#else +#import "FBLPromises.h" +#endif + +#import "FirebaseAppCheck/Sources/AppAttestProvider/API/FIRAppAttestAPIService.h" +#import "FirebaseAppCheck/Sources/AppAttestProvider/FIRAppAttestService.h" +#import "FirebaseAppCheck/Sources/AppAttestProvider/Storage/FIRAppAttestKeyIDStorage.h" +#import "FirebaseAppCheck/Sources/Core/APIService/FIRAppCheckAPIService.h" +#import "FirebaseAppCheck/Sources/Core/Errors/FIRAppCheckErrorUtil.h" + +#import "FirebaseCore/Sources/Private/FirebaseCoreInternal.h" + +NS_ASSUME_NONNULL_BEGIN + +/// A data object that contains all key attest data required for FAC token exchange. +@interface FIRAppAttestKeyAttestationResult : NSObject + +@property(nonatomic, readonly) NSString *keyID; +@property(nonatomic, readonly) NSData *challenge; +@property(nonatomic, readonly) NSData *attestation; + +- (instancetype)initWithKeyID:(NSString *)keyID + challenge:(NSData *)challenge + attestation:(NSData *)attestation; + +@end + +@implementation FIRAppAttestKeyAttestationResult + +- (instancetype)initWithKeyID:(NSString *)keyID + challenge:(NSData *)challenge + attestation:(NSData *)attestation { + self = [super init]; + if (self) { + _keyID = keyID; + _challenge = challenge; + _attestation = attestation; + } + return self; +} + +@end + +@interface FIRAppAttestProvider () + +@property(nonatomic, readonly) id APIService; +@property(nonatomic, readonly) id appAttestService; +@property(nonatomic, readonly) id keyIDStorage; + +@property(nonatomic, readonly) dispatch_queue_t queue; + +@end + +@implementation FIRAppAttestProvider + +- (instancetype)initWithAppAttestService:(id)appAttestService + APIService:(id)APIService + keyIDStorage:(id)keyIDStorage { + self = [super init]; + if (self) { + _appAttestService = appAttestService; + _APIService = APIService; + _keyIDStorage = keyIDStorage; + _queue = dispatch_queue_create("com.firebase.FIRAppAttestProvider", DISPATCH_QUEUE_SERIAL); + } + return self; +} + +- (nullable instancetype)initWithApp:(FIRApp *)app { +#if TARGET_OS_IOS + NSURLSession *URLSession = [NSURLSession + sessionWithConfiguration:[NSURLSessionConfiguration ephemeralSessionConfiguration]]; + + FIRAppAttestKeyIDStorage *keyIDStorage = + [[FIRAppAttestKeyIDStorage alloc] initWithAppName:app.name appID:app.options.googleAppID]; + + FIRAppCheckAPIService *APIService = + [[FIRAppCheckAPIService alloc] initWithURLSession:URLSession + APIKey:app.options.APIKey + projectID:app.options.projectID + appID:app.options.googleAppID]; + + FIRAppAttestAPIService *appAttestAPIService = + [[FIRAppAttestAPIService alloc] initWithAPIService:APIService + projectID:app.options.projectID + appID:app.options.googleAppID]; + + return [self initWithAppAttestService:DCAppAttestService.sharedService + APIService:appAttestAPIService + keyIDStorage:keyIDStorage]; +#else // TARGET_OS_IOS + return nil; +#endif // TARGET_OS_IOS +} + +#pragma mark - FIRAppCheckProvider + +- (void)getTokenWithCompletion:(void (^)(FIRAppCheckToken *_Nullable, NSError *_Nullable))handler { + // 1. Check `DCAppAttestService.isSupported`. + [self isAppAttestSupported] + .thenOn(self.queue, + ^FBLPromise *(id result) { + return [FBLPromise onQueue:self.queue + all:@[ + // 2. Request random challenge. + [self.APIService getRandomChallenge], + // 3. Get App Attest key ID. + [self getAppAttestKeyIDGenerateIfNeeded] + ]]; + }) + .thenOn(self.queue, + ^FBLPromise *(NSArray *challengeAndKeyID) { + // 4. Attest the key. + NSData *challenge = challengeAndKeyID.firstObject; + NSString *keyID = challengeAndKeyID.lastObject; + + return [self attestKey:keyID challenge:challenge]; + }) + .thenOn(self.queue, + ^FBLPromise *(FIRAppAttestKeyAttestationResult *result) { + // 5. Exchange the attestation to FAC token. + return [self.APIService appCheckTokenWithAttestation:result.attestation + keyID:result.keyID + challenge:result.challenge]; + }) + // 6. Call the handler with the result. + .then(^FBLPromise *(FIRAppCheckToken *token) { + handler(token, nil); + return nil; + }) + .catch(^(NSError *error) { + handler(nil, error); + }); +} + +/// Returns a resolved promise if App Attest is supported and a rejected promise if it is not. +- (FBLPromise *)isAppAttestSupported { + if (self.appAttestService.isSupported) { + return [FBLPromise resolvedWith:[NSNull null]]; + } else { + NSError *error = [FIRAppCheckErrorUtil unsupportedAttestationProvider:@"AppAttestProvider"]; + FBLPromise *rejectedPromise = [FBLPromise pendingPromise]; + [rejectedPromise reject:error]; + return rejectedPromise; + } +} + +/// Retrieves or generates App Attest key associated with the Firebase app. +- (FBLPromise *)getAppAttestKeyIDGenerateIfNeeded { + return [self.keyIDStorage getAppAttestKeyID].recoverOn(self.queue, + ^FBLPromise *(NSError *error) { + return [self generateAppAttestKey]; + }); +} + +/// Generates and stores App Attest key associated with the Firebase app. +- (FBLPromise *)generateAppAttestKey { + return [FBLPromise onQueue:self.queue + wrapObjectOrErrorCompletion:^(FBLPromiseObjectOrErrorCompletion _Nonnull handler) { + [self.appAttestService generateKeyWithCompletionHandler:handler]; + }] + .thenOn(self.queue, ^FBLPromise *(NSString *keyID) { + return [self.keyIDStorage setAppAttestKeyID:keyID]; + }); +} + +- (FBLPromise *)attestKey:(NSString *)keyID + challenge:(NSData *)challenge { + return [FBLPromise onQueue:self.queue + do:^id _Nullable { + return [challenge base64EncodedDataWithOptions:0]; + }] + .thenOn( + self.queue, + ^FBLPromise *(NSData *challengeHash) { + return [FBLPromise onQueue:self.queue + wrapObjectOrErrorCompletion:^(FBLPromiseObjectOrErrorCompletion _Nonnull handler) { + [self.appAttestService attestKey:keyID + clientDataHash:challengeHash + completionHandler:handler]; + }]; + }) + .thenOn(self.queue, ^FBLPromise *(NSData *attestation) { + FIRAppAttestKeyAttestationResult *result = + [[FIRAppAttestKeyAttestationResult alloc] initWithKeyID:keyID + challenge:challenge + attestation:attestation]; + return [FBLPromise resolvedWith:result]; + }); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/FirebaseAppCheck/Sources/AppAttestProvider/FIRAppAttestService.h b/FirebaseAppCheck/Sources/AppAttestProvider/FIRAppAttestService.h new file mode 100644 index 00000000000..fd254aaacca --- /dev/null +++ b/FirebaseAppCheck/Sources/AppAttestProvider/FIRAppAttestService.h @@ -0,0 +1,36 @@ +/* + * Copyright 2021 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 + +NS_ASSUME_NONNULL_BEGIN + +/// See `DCAppAttestService` +/// https://developer.apple.com/documentation/devicecheck/dcappattestservice?language=objc +@protocol FIRAppAttestService + +@property(getter=isSupported, readonly) BOOL supported; + +- (void)generateKeyWithCompletionHandler:(void (^)(NSString *keyId, + NSError *error))completionHandler; + +- (void)attestKey:(NSString *)keyId + clientDataHash:(NSData *)clientDataHash + completionHandler:(void (^)(NSData *attestationObject, NSError *error))completionHandler; + +@end + +NS_ASSUME_NONNULL_END diff --git a/FirebaseAppCheck/Sources/AppAttestProvider/Storage/FIRAppAttestKeyIDStorage.h b/FirebaseAppCheck/Sources/AppAttestProvider/Storage/FIRAppAttestKeyIDStorage.h new file mode 100644 index 00000000000..a6caaf38d7e --- /dev/null +++ b/FirebaseAppCheck/Sources/AppAttestProvider/Storage/FIRAppAttestKeyIDStorage.h @@ -0,0 +1,47 @@ +/* + * Copyright 2021 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 + +@class FBLPromise; + +NS_ASSUME_NONNULL_BEGIN + +/// The protocol defines methods to store App Attest key IDs per Firebase app. +@protocol FIRAppAttestKeyIDStorageProtocol + +- (FBLPromise *)setAppAttestKeyID:(nullable NSString *)keyID; + +- (FBLPromise *)getAppAttestKeyID; + +@end + +/// The App Attest key ID storage implementation. +@interface FIRAppAttestKeyIDStorage : NSObject + +- (instancetype)init NS_UNAVAILABLE; + +/** Default convenience initializer. + * @param appName A Firebase App name (`FirebaseApp.name`). The app name will be used as a part of + * the key to store the token for the storage instance. + * @param appID A Firebase App identifier (`FirebaseOptions.googleAppID`). The app ID will be used + * as a part of the key to store the token for the storage instance. + */ +- (instancetype)initWithAppName:(NSString *)appName appID:(NSString *)appID; + +@end + +NS_ASSUME_NONNULL_END diff --git a/FirebaseAppCheck/Sources/AppAttestProvider/Storage/FIRAppAttestKeyIDStorage.m b/FirebaseAppCheck/Sources/AppAttestProvider/Storage/FIRAppAttestKeyIDStorage.m new file mode 100644 index 00000000000..a0e499606cb --- /dev/null +++ b/FirebaseAppCheck/Sources/AppAttestProvider/Storage/FIRAppAttestKeyIDStorage.m @@ -0,0 +1,53 @@ +/* + * Copyright 2021 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 "FirebaseAppCheck/Sources/AppAttestProvider/Storage/FIRAppAttestKeyIDStorage.h" + +#if __has_include() +#import +#else +#import "FBLPromises.h" +#endif + +@interface FIRAppAttestKeyIDStorage () + +@property(nonatomic, readonly) NSString *appName; +@property(nonatomic, readonly) NSString *appID; + +@end + +@implementation FIRAppAttestKeyIDStorage + +- (instancetype)initWithAppName:(NSString *)appName appID:(NSString *)appID { + self = [super init]; + if (self) { + _appName = [appName copy]; + _appID = [appID copy]; + } + return self; +} + +- (nonnull FBLPromise *)getAppAttestKeyID { + // TODO: Implement. + return [FBLPromise resolvedWith:nil]; +} + +- (nonnull FBLPromise *)setAppAttestKeyID:(nullable NSString *)keyID { + // TODO: Implement. + return [FBLPromise resolvedWith:nil]; +} + +@end diff --git a/FirebaseAppCheck/Sources/Core/Errors/FIRAppCheckErrorUtil.h b/FirebaseAppCheck/Sources/Core/Errors/FIRAppCheckErrorUtil.h index 77fe37b8159..f73a867891d 100644 --- a/FirebaseAppCheck/Sources/Core/Errors/FIRAppCheckErrorUtil.h +++ b/FirebaseAppCheck/Sources/Core/Errors/FIRAppCheckErrorUtil.h @@ -18,7 +18,7 @@ NS_ASSUME_NONNULL_BEGIN -FOUNDATION_EXTERN NSString *const kFIRAppCheckErrorDomain; +FOUNDATION_EXTERN NSErrorDomain const kFIRAppCheckErrorDomain NS_SWIFT_NAME(AppCheckErrorDomain); void FIRAppCheckSetErrorToPointer(NSError *error, NSError **pointer); @@ -40,12 +40,24 @@ void FIRAppCheckSetErrorToPointer(NSError *error, NSError **pointer); + (NSError *)errorWithFailureReason:(NSString *)failureReason; ++ (NSError *)unsupportedAttestationProvider:(NSString *)providerName; + @end -typedef NS_ENUM(NSInteger, FIRAppCheckErrorCode) { - FIRAppCheckErrorCodeUnknown = 0 +typedef NS_ERROR_ENUM(kFIRAppCheckErrorDomain, FIRAppCheckErrorCode){ + /// An unknown or non-actionable error. + FIRAppCheckErrorCodeUnknown = 0, + + /// A network connection error. + FIRAppCheckErrorCodeServerUnreachable = 1, + + /// Invalid configuration error. + FIRAppCheckErrorCodeInvalidConfiguration = 2, + + /// System keychain access error. + FIRAppCheckErrorCodeKeychain = 3, - // TODO: Add public error codes here. -}; + /// Selected app attestation provider is not supported on the current platform or OS version. + FIRAppCheckErrorCodeUnsupported = 4} NS_SWIFT_NAME(AppCheckErrorCode); NS_ASSUME_NONNULL_END diff --git a/FirebaseAppCheck/Sources/Core/Errors/FIRAppCheckErrorUtil.m b/FirebaseAppCheck/Sources/Core/Errors/FIRAppCheckErrorUtil.m index d59645226a6..c0871816d3f 100644 --- a/FirebaseAppCheck/Sources/Core/Errors/FIRAppCheckErrorUtil.m +++ b/FirebaseAppCheck/Sources/Core/Errors/FIRAppCheckErrorUtil.m @@ -68,6 +68,16 @@ + (NSError *)JSONSerializationError:(NSError *)error { underlyingError:error]; } ++ (NSError *)unsupportedAttestationProvider:(NSString *)providerName { + NSString *failureReason = [NSString + stringWithFormat: + @"The attestation provider %@ is not supported on current platform and OS version.", + providerName]; + return [self appCheckErrorWithCode:FIRAppCheckErrorCodeUnsupported + failureReason:failureReason + underlyingError:nil]; +} + + (NSError *)errorWithFailureReason:(NSString *)failureReason { return [self appCheckErrorWithCode:FIRAppCheckErrorCodeUnknown failureReason:failureReason diff --git a/FirebaseAppCheck/Sources/DeviceCheckProvider/API/FIRDeviceCheckAPIService.m b/FirebaseAppCheck/Sources/DeviceCheckProvider/API/FIRDeviceCheckAPIService.m index 8c9670791da..06512cac8e5 100644 --- a/FirebaseAppCheck/Sources/DeviceCheckProvider/API/FIRDeviceCheckAPIService.m +++ b/FirebaseAppCheck/Sources/DeviceCheckProvider/API/FIRDeviceCheckAPIService.m @@ -23,7 +23,6 @@ #endif #import -#import #import "FirebaseAppCheck/Sources/Core/APIService/FIRAppCheckAPIService.h" #import "FirebaseAppCheck/Sources/Core/APIService/FIRAppCheckToken+APIResponse.h" diff --git a/FirebaseAppCheck/Sources/DeviceCheckProvider/FIRDeviceCheckProvider.m b/FirebaseAppCheck/Sources/DeviceCheckProvider/FIRDeviceCheckProvider.m index 3f3407f7f15..c6c92f9fd41 100644 --- a/FirebaseAppCheck/Sources/DeviceCheckProvider/FIRDeviceCheckProvider.m +++ b/FirebaseAppCheck/Sources/DeviceCheckProvider/FIRDeviceCheckProvider.m @@ -16,8 +16,6 @@ #import -#import - #if __has_include() #import #else diff --git a/FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRAppAttestProvider.h b/FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRAppAttestProvider.h new file mode 100644 index 00000000000..af0a7a903b3 --- /dev/null +++ b/FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRAppAttestProvider.h @@ -0,0 +1,36 @@ +/* + * Copyright 2021 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 + +#import "FIRAppCheckProvider.h" + +@class FIRApp; + +NS_ASSUME_NONNULL_BEGIN + +API_AVAILABLE(ios(14.0)) +API_UNAVAILABLE(macos, tvos, watchos) +NS_SWIFT_NAME(AppAttestProvider) +@interface FIRAppAttestProvider : NSObject + +- (instancetype)init NS_UNAVAILABLE; + +- (nullable instancetype)initWithApp:(FIRApp *)app; + +@end + +NS_ASSUME_NONNULL_END diff --git a/FirebaseAppCheck/Tests/Unit/AppAttestProvider/FIRAppAttestProviderTests.m b/FirebaseAppCheck/Tests/Unit/AppAttestProvider/FIRAppAttestProviderTests.m new file mode 100644 index 00000000000..ead1c886161 --- /dev/null +++ b/FirebaseAppCheck/Tests/Unit/AppAttestProvider/FIRAppAttestProviderTests.m @@ -0,0 +1,388 @@ +/* + * Copyright 2021 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 + +#import "FBLPromise+Testing.h" +#import "OCMock.h" + +#import "FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRAppAttestProvider.h" + +#import "FirebaseAppCheck/Sources/AppAttestProvider/API/FIRAppAttestAPIService.h" +#import "FirebaseAppCheck/Sources/AppAttestProvider/FIRAppAttestService.h" +#import "FirebaseAppCheck/Sources/AppAttestProvider/Storage/FIRAppAttestKeyIDStorage.h" +#import "FirebaseAppCheck/Sources/Core/Errors/FIRAppCheckErrorUtil.h" +#import "FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRAppCheckToken.h" + +#import "FirebaseCore/Sources/Private/FirebaseCoreInternal.h" + +// Currently FIRAppAttestProvider is available only on iOS. +#if TARGET_OS_IOS + +@interface FIRAppAttestProvider (Tests) +- (instancetype)initWithAppAttestService:(id)appAttestService + APIService:(id)APIService + keyIDStorage:(id)keyIDStorage; +@end + +API_AVAILABLE(ios(14.0)) +@interface FIRAppAttestProviderTests : XCTestCase + +@property(nonatomic) FIRAppAttestProvider *provider; + +@property(nonatomic) OCMockObject *mockAppAttestService; +@property(nonatomic) OCMockObject *mockAPIService; +@property(nonatomic) OCMockObject *mockStorage; + +@end + +@implementation FIRAppAttestProviderTests + +- (void)setUp { + [super setUp]; + + self.mockAppAttestService = OCMProtocolMock(@protocol(FIRAppAttestService)); + self.mockAPIService = OCMProtocolMock(@protocol(FIRAppAttestAPIServiceProtocol)); + self.mockStorage = OCMProtocolMock(@protocol(FIRAppAttestKeyIDStorageProtocol)); + + self.provider = [[FIRAppAttestProvider alloc] initWithAppAttestService:self.mockAppAttestService + APIService:self.mockAPIService + keyIDStorage:self.mockStorage]; +} + +- (void)tearDown { + self.provider = nil; + self.mockStorage = nil; + self.mockAPIService = nil; + self.mockAppAttestService = nil; +} + +- (void)testInitWithValidApp { + FIROptions *options = [[FIROptions alloc] initWithGoogleAppID:@"app_id" GCMSenderID:@"sender_id"]; + options.APIKey = @"api_key"; + options.projectID = @"project_id"; + FIRApp *app = [[FIRApp alloc] initInstanceWithName:@"testInitWithValidApp" options:options]; + + XCTAssertNotNil([[FIRAppAttestProvider alloc] initWithApp:app]); +} + +- (void)testGetTokenWhenAppAttestIsNotSupported { + // 1. Expect FIRAppAttestService.isSupported. + [OCMExpect([self.mockAppAttestService isSupported]) andReturnValue:@(NO)]; + + // 2. Don't expect other operations. + OCMReject([self.mockStorage getAppAttestKeyID]); + OCMReject([self.mockAppAttestService generateKeyWithCompletionHandler:OCMOCK_ANY]); + OCMReject([self.mockAPIService getRandomChallenge]); + OCMReject([self.mockStorage setAppAttestKeyID:OCMOCK_ANY]); + OCMReject([self.mockAppAttestService attestKey:OCMOCK_ANY + clientDataHash:OCMOCK_ANY + completionHandler:OCMOCK_ANY]); + OCMReject([self.mockAPIService appCheckTokenWithAttestation:OCMOCK_ANY + keyID:OCMOCK_ANY + challenge:OCMOCK_ANY]); + + // 3. Call get token. + XCTestExpectation *completionExpectation = + [self expectationWithDescription:@"completionExpectation"]; + [self.provider + getTokenWithCompletion:^(FIRAppCheckToken *_Nullable token, NSError *_Nullable error) { + [completionExpectation fulfill]; + + XCTAssertNil(token); + XCTAssertEqualObjects( + error, [FIRAppCheckErrorUtil unsupportedAttestationProvider:@"AppAttestProvider"]); + }]; + + [self waitForExpectations:@[ completionExpectation ] timeout:0.5]; + + // 4. Verify mocks. + OCMVerifyAll(self.mockAppAttestService); + OCMVerifyAll(self.mockAPIService); + OCMVerifyAll(self.mockStorage); +} + +- (void)testGetToken_WhenNoExistingKey_Success { + // 1. Expect FIRAppAttestService.isSupported. + [OCMExpect([self.mockAppAttestService isSupported]) andReturnValue:@(YES)]; + + // 2. Expect storage getAppAttestKeyID. + FBLPromise *rejectedPromise = [FBLPromise pendingPromise]; + NSError *error = [NSError errorWithDomain:@"testGetToken_WhenNoExistingKey_Success" + code:NSNotFound + userInfo:nil]; + [rejectedPromise reject:error]; + OCMExpect([self.mockStorage getAppAttestKeyID]).andReturn(rejectedPromise); + + // 3. Expect App Attest key to be generated. + NSString *generatedKeyID = @"generatedKeyID"; + id completionArg = [OCMArg invokeBlockWithArgs:generatedKeyID, [NSNull null], nil]; + OCMExpect([self.mockAppAttestService generateKeyWithCompletionHandler:completionArg]); + + // 4. Expect the key ID to be stored. + OCMExpect([self.mockStorage setAppAttestKeyID:generatedKeyID]) + .andReturn([FBLPromise resolvedWith:generatedKeyID]); + + // 5. Expect random challenge to be requested. + NSData *randomChallenge = [@"random challenge" dataUsingEncoding:NSUTF8StringEncoding]; + OCMExpect([self.mockAPIService getRandomChallenge]) + .andReturn([FBLPromise resolvedWith:randomChallenge]); + + // 6. Expect the key to be attested with the challenge. + NSData *expectedChallengeHash = [randomChallenge base64EncodedDataWithOptions:0]; + NSData *attestationData = [@"attestation data" dataUsingEncoding:NSUTF8StringEncoding]; + id attestCompletionArg = [OCMArg invokeBlockWithArgs:attestationData, [NSNull null], nil]; + OCMExpect([self.mockAppAttestService attestKey:generatedKeyID + clientDataHash:expectedChallengeHash + completionHandler:attestCompletionArg]); + + // 7. Expect exchange request to be sent. + FIRAppCheckToken *FACToken = [[FIRAppCheckToken alloc] initWithToken:@"FAC token" + expirationDate:[NSDate date]]; + OCMExpect([self.mockAPIService appCheckTokenWithAttestation:attestationData + keyID:generatedKeyID + challenge:randomChallenge]) + .andReturn([FBLPromise resolvedWith:FACToken]); + + // 8. Call get token. + XCTestExpectation *completionExpectation = + [self expectationWithDescription:@"completionExpectation"]; + [self.provider + getTokenWithCompletion:^(FIRAppCheckToken *_Nullable token, NSError *_Nullable error) { + [completionExpectation fulfill]; + + XCTAssertEqualObjects(token.token, FACToken.token); + XCTAssertEqualObjects(token.expirationDate, FACToken.expirationDate); + XCTAssertNil(error); + }]; + + [self waitForExpectations:@[ completionExpectation ] timeout:0.5]; + + // 9. Verify mocks. + OCMVerifyAll(self.mockAppAttestService); + OCMVerifyAll(self.mockAPIService); + OCMVerifyAll(self.mockStorage); +} + +- (void)testGetToken_WhenExistingKey_Success { + // 1. Expect FIRAppAttestService.isSupported. + [OCMExpect([self.mockAppAttestService isSupported]) andReturnValue:@(YES)]; + + // 2. Expect storage getAppAttestKeyID. + NSString *existingKeyID = @"existingKeyID"; + OCMExpect([self.mockStorage getAppAttestKeyID]) + .andReturn([FBLPromise resolvedWith:existingKeyID]); + + // 3. Don't expect App Attest key to be generated. + OCMReject([self.mockAppAttestService generateKeyWithCompletionHandler:OCMOCK_ANY]); + + // 4. Don't expect the key ID to be stored. + OCMReject([self.mockStorage setAppAttestKeyID:OCMOCK_ANY]); + + // 5. Expect random challenge to be requested. + NSData *randomChallenge = [@"random challenge" dataUsingEncoding:NSUTF8StringEncoding]; + OCMExpect([self.mockAPIService getRandomChallenge]) + .andReturn([FBLPromise resolvedWith:randomChallenge]); + + // 6. Expect the key to be attested with the challenge. + NSData *expectedChallengeHash = [randomChallenge base64EncodedDataWithOptions:0]; + NSData *attestationData = [@"attestation data" dataUsingEncoding:NSUTF8StringEncoding]; + id attestCompletionArg = [OCMArg invokeBlockWithArgs:attestationData, [NSNull null], nil]; + OCMExpect([self.mockAppAttestService attestKey:existingKeyID + clientDataHash:expectedChallengeHash + completionHandler:attestCompletionArg]); + + // 7. Expect exchange request to be sent. + FIRAppCheckToken *FACToken = [[FIRAppCheckToken alloc] initWithToken:@"FAC token" + expirationDate:[NSDate date]]; + OCMExpect([self.mockAPIService appCheckTokenWithAttestation:attestationData + keyID:existingKeyID + challenge:randomChallenge]) + .andReturn([FBLPromise resolvedWith:FACToken]); + + // 8. Call get token. + XCTestExpectation *completionExpectation = + [self expectationWithDescription:@"completionExpectation"]; + [self.provider + getTokenWithCompletion:^(FIRAppCheckToken *_Nullable token, NSError *_Nullable error) { + [completionExpectation fulfill]; + + XCTAssertEqualObjects(token.token, FACToken.token); + XCTAssertEqualObjects(token.expirationDate, FACToken.expirationDate); + XCTAssertNil(error); + }]; + + [self waitForExpectations:@[ completionExpectation ] timeout:0.5]; + + // 9. Verify mocks. + OCMVerifyAll(self.mockAppAttestService); + OCMVerifyAll(self.mockAPIService); + OCMVerifyAll(self.mockStorage); +} + +- (void)testGetToken_WhenRandomChallengeError { + // 1. Expect FIRAppAttestService.isSupported. + [OCMExpect([self.mockAppAttestService isSupported]) andReturnValue:@(YES)]; + + // 2. Expect storage getAppAttestKeyID. + NSString *existingKeyID = @"existingKeyID"; + OCMExpect([self.mockStorage getAppAttestKeyID]) + .andReturn([FBLPromise resolvedWith:existingKeyID]); + + // 3. Expect random challenge to be requested. + NSError *challengeError = [NSError errorWithDomain:@"testGetToken_WhenRandomChallengeError" + code:NSNotFound + userInfo:nil]; + OCMExpect([self.mockAPIService getRandomChallenge]) + .andReturn([self rejectedPromiseWithError:challengeError]); + + // 4. Don't expect other steps. + OCMReject([self.mockStorage setAppAttestKeyID:OCMOCK_ANY]); + OCMReject([self.mockAppAttestService attestKey:OCMOCK_ANY + clientDataHash:OCMOCK_ANY + completionHandler:OCMOCK_ANY]); + OCMReject([self.mockAPIService appCheckTokenWithAttestation:OCMOCK_ANY + keyID:OCMOCK_ANY + challenge:OCMOCK_ANY]); + + // 5. Call get token. + XCTestExpectation *completionExpectation = + [self expectationWithDescription:@"completionExpectation"]; + [self.provider + getTokenWithCompletion:^(FIRAppCheckToken *_Nullable token, NSError *_Nullable error) { + [completionExpectation fulfill]; + + XCTAssertNil(token); + XCTAssertEqualObjects(error, challengeError); + }]; + + [self waitForExpectations:@[ completionExpectation ] timeout:0.5]; + + // 6. Verify mocks. + OCMVerifyAll(self.mockAppAttestService); + OCMVerifyAll(self.mockAPIService); + OCMVerifyAll(self.mockStorage); +} + +- (void)testGetTokenWhenKeyAttestationError { + // 1. Expect FIRAppAttestService.isSupported. + [OCMExpect([self.mockAppAttestService isSupported]) andReturnValue:@(YES)]; + + // 2. Expect storage getAppAttestKeyID. + NSString *existingKeyID = @"existingKeyID"; + OCMExpect([self.mockStorage getAppAttestKeyID]) + .andReturn([FBLPromise resolvedWith:existingKeyID]); + + // 3. Expect random challenge to be requested. + NSData *randomChallenge = [@"random challenge" dataUsingEncoding:NSUTF8StringEncoding]; + OCMExpect([self.mockAPIService getRandomChallenge]) + .andReturn([FBLPromise resolvedWith:randomChallenge]); + + // 4. Expect the key to be attested with the challenge. + NSData *expectedChallengeHash = [randomChallenge base64EncodedDataWithOptions:0]; + NSError *attestationError = [NSError errorWithDomain:@"testGetTokenWhenKeyAttestationError" + code:0 + userInfo:nil]; + id attestCompletionArg = [OCMArg invokeBlockWithArgs:[NSNull null], attestationError, nil]; + OCMExpect([self.mockAppAttestService attestKey:existingKeyID + clientDataHash:expectedChallengeHash + completionHandler:attestCompletionArg]); + + // 5. Don't exchange API request. + OCMReject([self.mockAPIService appCheckTokenWithAttestation:OCMOCK_ANY + keyID:OCMOCK_ANY + challenge:OCMOCK_ANY]); + + // 6. Call get token. + XCTestExpectation *completionExpectation = + [self expectationWithDescription:@"completionExpectation"]; + [self.provider + getTokenWithCompletion:^(FIRAppCheckToken *_Nullable token, NSError *_Nullable error) { + [completionExpectation fulfill]; + + XCTAssertNil(token); + XCTAssertEqualObjects(error, attestationError); + }]; + + [self waitForExpectations:@[ completionExpectation ] timeout:0.5]; + + // 7. Verify mocks. + OCMVerifyAll(self.mockAppAttestService); + OCMVerifyAll(self.mockAPIService); + OCMVerifyAll(self.mockStorage); +} + +- (void)testGetTokenWhenKeyAttestationExchangeError { + // 1. Expect FIRAppAttestService.isSupported. + [OCMExpect([self.mockAppAttestService isSupported]) andReturnValue:@(YES)]; + + // 2. Expect storage getAppAttestKeyID. + NSString *existingKeyID = @"existingKeyID"; + OCMExpect([self.mockStorage getAppAttestKeyID]) + .andReturn([FBLPromise resolvedWith:existingKeyID]); + + // 3. Expect random challenge to be requested. + NSData *randomChallenge = [@"random challenge" dataUsingEncoding:NSUTF8StringEncoding]; + OCMExpect([self.mockAPIService getRandomChallenge]) + .andReturn([FBLPromise resolvedWith:randomChallenge]); + + // 4. Expect the key to be attested with the challenge. + NSData *expectedChallengeHash = [randomChallenge base64EncodedDataWithOptions:0]; + NSData *attestationData = [@"attestation data" dataUsingEncoding:NSUTF8StringEncoding]; + id attestCompletionArg = [OCMArg invokeBlockWithArgs:attestationData, [NSNull null], nil]; + OCMExpect([self.mockAppAttestService attestKey:existingKeyID + clientDataHash:expectedChallengeHash + completionHandler:attestCompletionArg]); + + // 7. Expect exchange request to be sent. + NSError *exchangeError = [NSError errorWithDomain:@"testGetTokenWhenKeyAttestationExchangeError" + code:0 + userInfo:nil]; + OCMExpect([self.mockAPIService appCheckTokenWithAttestation:attestationData + keyID:existingKeyID + challenge:randomChallenge]) + .andReturn([self rejectedPromiseWithError:exchangeError]); + + // 5. Call get token. + XCTestExpectation *completionExpectation = + [self expectationWithDescription:@"completionExpectation"]; + [self.provider + getTokenWithCompletion:^(FIRAppCheckToken *_Nullable token, NSError *_Nullable error) { + [completionExpectation fulfill]; + + XCTAssertNil(token); + XCTAssertEqualObjects(error, exchangeError); + }]; + + [self waitForExpectations:@[ completionExpectation ] timeout:0.5]; + + // 6. Verify mocks. + OCMVerifyAll(self.mockAppAttestService); + OCMVerifyAll(self.mockAPIService); + OCMVerifyAll(self.mockStorage); +} + +#pragma mark - Helpers + +- (FBLPromise *)rejectedPromiseWithError:(NSError *)error { + FBLPromise *rejectedPromise = [FBLPromise pendingPromise]; + [rejectedPromise reject:error]; + return rejectedPromise; +} + +@end + +#endif // TARGET_OS_IOS From 869f87c0d2b530bd132aef37c0fba16a78c45da6 Mon Sep 17 00:00:00 2001 From: Nick Cooke <36927374+ncooke3@users.noreply.github.com> Date: Mon, 3 May 2021 12:48:40 -0400 Subject: [PATCH 02/37] Implement app attest key ID storage (#8014) * Implement FIRAppAttestKeyIDStorage * Add FIRAppAttestKeyIDStorageTests * Review [Draft] * Style * Docs updates * Docs updates 2 * Review [Draft] 2 * Improve tests * Improve test readability * Improve test readability 2 --- .../Storage/FIRAppAttestKeyIDStorage.h | 13 ++ .../Storage/FIRAppAttestKeyIDStorage.m | 50 +++++- .../Core/Errors/FIRAppCheckErrorUtil.h | 2 + .../Core/Errors/FIRAppCheckErrorUtil.m | 7 + .../Sources/Core/Storage/FIRAppCheckStorage.h | 2 +- .../FIRAppAttestKeyIDStorageTests.m | 145 ++++++++++++++++++ 6 files changed, 213 insertions(+), 6 deletions(-) create mode 100644 FirebaseAppCheck/Tests/Unit/AppAttestProvider/FIRAppAttestKeyIDStorageTests.m diff --git a/FirebaseAppCheck/Sources/AppAttestProvider/Storage/FIRAppAttestKeyIDStorage.h b/FirebaseAppCheck/Sources/AppAttestProvider/Storage/FIRAppAttestKeyIDStorage.h index a6caaf38d7e..13833a874bf 100644 --- a/FirebaseAppCheck/Sources/AppAttestProvider/Storage/FIRAppAttestKeyIDStorage.h +++ b/FirebaseAppCheck/Sources/AppAttestProvider/Storage/FIRAppAttestKeyIDStorage.h @@ -23,13 +23,26 @@ NS_ASSUME_NONNULL_BEGIN /// The protocol defines methods to store App Attest key IDs per Firebase app. @protocol FIRAppAttestKeyIDStorageProtocol +/** Manages storage of an app attest key ID. + * @param keyID The app attest key ID to store or `nil` to remove the existing app attest key ID. + * @returns A promise that is resolved with a stored app attest key ID or `nil` if the existing app + * attest key ID has been removed. + */ - (FBLPromise *)setAppAttestKeyID:(nullable NSString *)keyID; +/** Reads a stored app attest key ID. + * @returns A promise that is resolved with a stored app attest key ID or `nil` if there is not a + * stored app attest key ID. The promise is rejected with an error in the case of a missing app + * attest key ID . + */ - (FBLPromise *)getAppAttestKeyID; @end /// The App Attest key ID storage implementation. +/// This class is designed for use by `FIRAppAttestProvider`. It's operations are managed by +/// `FIRAppAttestProvider`'s internal serial queue. It is not considered thread safe and should not +/// be used by other classes at this time. @interface FIRAppAttestKeyIDStorage : NSObject - (instancetype)init NS_UNAVAILABLE; diff --git a/FirebaseAppCheck/Sources/AppAttestProvider/Storage/FIRAppAttestKeyIDStorage.m b/FirebaseAppCheck/Sources/AppAttestProvider/Storage/FIRAppAttestKeyIDStorage.m index a0e499606cb..5eb8ffd88f1 100644 --- a/FirebaseAppCheck/Sources/AppAttestProvider/Storage/FIRAppAttestKeyIDStorage.m +++ b/FirebaseAppCheck/Sources/AppAttestProvider/Storage/FIRAppAttestKeyIDStorage.m @@ -22,11 +22,19 @@ #import "FBLPromises.h" #endif +#import "FirebaseAppCheck/Sources/Core/Errors/FIRAppCheckErrorUtil.h" + +/// The `NSUserDefaults` suite name for the storage location of the app attest key ID. +static NSString *const kKeyIDStorageDefaultsSuiteName = @"com.firebase.FIRAppAttestKeyIDStorage"; + @interface FIRAppAttestKeyIDStorage () @property(nonatomic, readonly) NSString *appName; @property(nonatomic, readonly) NSString *appID; +/// The app attest key ID is stored using `NSUserDefaults` . +@property(nonatomic, readonly) NSUserDefaults *userDefaults; + @end @implementation FIRAppAttestKeyIDStorage @@ -36,18 +44,50 @@ - (instancetype)initWithAppName:(NSString *)appName appID:(NSString *)appID { if (self) { _appName = [appName copy]; _appID = [appID copy]; + _userDefaults = [[NSUserDefaults alloc] initWithSuiteName:kKeyIDStorageDefaultsSuiteName]; } return self; } +- (nonnull FBLPromise *)setAppAttestKeyID:(nullable NSString *)keyID { + [self storeAppAttestKeyID:keyID]; + return [FBLPromise resolvedWith:keyID]; +} + - (nonnull FBLPromise *)getAppAttestKeyID { - // TODO: Implement. - return [FBLPromise resolvedWith:nil]; + NSString *appAttestKeyID = [self appAttestKeyIDFromStorage]; + if (appAttestKeyID) { + return [FBLPromise resolvedWith:appAttestKeyID]; + } else { + NSError *error = [FIRAppCheckErrorUtil appAttestKeyIDNotFound]; + FBLPromise *rejectedPromise = [FBLPromise pendingPromise]; + [rejectedPromise reject:error]; + return rejectedPromise; + } } -- (nonnull FBLPromise *)setAppAttestKeyID:(nullable NSString *)keyID { - // TODO: Implement. - return [FBLPromise resolvedWith:nil]; +#pragma mark - Helpers + +- (void)storeAppAttestKeyID:(nullable NSString *)keyID { + if (keyID) { + [self.userDefaults setObject:keyID forKey:[self keyIDStorageKey]]; + } else { + [self.userDefaults removeObjectForKey:[self keyIDStorageKey]]; + } +} + +- (nullable NSString *)appAttestKeyIDFromStorage { + NSString *appAttestKeyID = nil; + appAttestKeyID = [self.userDefaults objectForKey:[self keyIDStorageKey]]; + return appAttestKeyID; +} + +- (NSString *)keyIDStorageKey { + return [[self class] keyIDStorageKeyForAppName:self.appName appID:self.appID]; +} + ++ (NSString *)keyIDStorageKeyForAppName:(NSString *)appName appID:(NSString *)appID { + return [NSString stringWithFormat:@"app_attest_keyID.%@.%@", appName, appID]; } @end diff --git a/FirebaseAppCheck/Sources/Core/Errors/FIRAppCheckErrorUtil.h b/FirebaseAppCheck/Sources/Core/Errors/FIRAppCheckErrorUtil.h index f73a867891d..077d3387b28 100644 --- a/FirebaseAppCheck/Sources/Core/Errors/FIRAppCheckErrorUtil.h +++ b/FirebaseAppCheck/Sources/Core/Errors/FIRAppCheckErrorUtil.h @@ -42,6 +42,8 @@ void FIRAppCheckSetErrorToPointer(NSError *error, NSError **pointer); + (NSError *)unsupportedAttestationProvider:(NSString *)providerName; ++ (NSError *)appAttestKeyIDNotFound; + @end typedef NS_ERROR_ENUM(kFIRAppCheckErrorDomain, FIRAppCheckErrorCode){ diff --git a/FirebaseAppCheck/Sources/Core/Errors/FIRAppCheckErrorUtil.m b/FirebaseAppCheck/Sources/Core/Errors/FIRAppCheckErrorUtil.m index c0871816d3f..93f62e22558 100644 --- a/FirebaseAppCheck/Sources/Core/Errors/FIRAppCheckErrorUtil.m +++ b/FirebaseAppCheck/Sources/Core/Errors/FIRAppCheckErrorUtil.m @@ -78,6 +78,13 @@ + (NSError *)unsupportedAttestationProvider:(NSString *)providerName { underlyingError:nil]; } ++ (NSError *)appAttestKeyIDNotFound { + NSString *failureReason = @"App attest key ID not found."; + return [self appCheckErrorWithCode:FIRAppCheckErrorCodeUnknown + failureReason:failureReason + underlyingError:nil]; +} + + (NSError *)errorWithFailureReason:(NSString *)failureReason { return [self appCheckErrorWithCode:FIRAppCheckErrorCodeUnknown failureReason:failureReason diff --git a/FirebaseAppCheck/Sources/Core/Storage/FIRAppCheckStorage.h b/FirebaseAppCheck/Sources/Core/Storage/FIRAppCheckStorage.h index 9180b73c409..29486f20536 100644 --- a/FirebaseAppCheck/Sources/Core/Storage/FIRAppCheckStorage.h +++ b/FirebaseAppCheck/Sources/Core/Storage/FIRAppCheckStorage.h @@ -32,7 +32,7 @@ NS_ASSUME_NONNULL_BEGIN - (FBLPromise *)setToken:(nullable FIRAppCheckToken *)token; /** Reads a stored FAA token. - * @returns A promise that is resolved with a stored token or `nil` if there is no a stored token. + * @returns A promise that is resolved with a stored token or `nil` if there is not a stored token. * The promise is rejected with an error in the case of a failure. */ - (FBLPromise *)getToken; diff --git a/FirebaseAppCheck/Tests/Unit/AppAttestProvider/FIRAppAttestKeyIDStorageTests.m b/FirebaseAppCheck/Tests/Unit/AppAttestProvider/FIRAppAttestKeyIDStorageTests.m new file mode 100644 index 00000000000..f2db101a5e8 --- /dev/null +++ b/FirebaseAppCheck/Tests/Unit/AppAttestProvider/FIRAppAttestKeyIDStorageTests.m @@ -0,0 +1,145 @@ +/* + * Copyright 2021 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 + +#import "FBLPromise+Testing.h" + +#import "FirebaseAppCheck/Sources/AppAttestProvider/Storage/FIRAppAttestKeyIDStorage.h" + +#import "FirebaseAppCheck/Sources/Core/Errors/FIRAppCheckErrorUtil.h" + +@interface FIRAppAttestKeyIDStorageTests : XCTestCase +@property(nonatomic) NSString *appName; +@property(nonatomic) NSString *appID; +@property(nonatomic) FIRAppAttestKeyIDStorage *storage; +@end + +@implementation FIRAppAttestKeyIDStorageTests + +- (void)setUp { + [super setUp]; + + self.appName = @"FIRAppAttestKeyIDStorageTestsApp"; + self.appID = @"app_id"; + self.storage = [[FIRAppAttestKeyIDStorage alloc] initWithAppName:self.appName appID:self.appID]; +} + +- (void)tearDown { + // Remove the app attest key ID from storage. + [self.storage setAppAttestKeyID:nil]; + FBLWaitForPromisesWithTimeout(1.0); + self.storage = nil; + + [super tearDown]; +} + +- (void)testInitWithApp { + XCTAssertNotNil([[FIRAppAttestKeyIDStorage alloc] initWithAppName:self.appName appID:self.appID]); +} + +- (void)testSetAndGetAppAttestKeyID { + NSString *appAttestKeyID = @"app_attest_key_ID"; + + FBLPromise *setPromise = [self.storage setAppAttestKeyID:appAttestKeyID]; + XCTAssert(FBLWaitForPromisesWithTimeout(0.5)); + XCTAssertEqualObjects(setPromise.value, appAttestKeyID); + XCTAssertNil(setPromise.error); + + __auto_type getPromise = [self.storage getAppAttestKeyID]; + XCTAssert(FBLWaitForPromisesWithTimeout(0.5)); + XCTAssertEqualObjects(getPromise.value, appAttestKeyID); + XCTAssertNil(getPromise.error); +} + +- (void)testRemoveAppAttestKeyID { + FBLPromise *setPromise = [self.storage setAppAttestKeyID:nil]; + XCTAssert(FBLWaitForPromisesWithTimeout(0.5)); + XCTAssertEqualObjects(setPromise.value, nil); + XCTAssertNil(setPromise.error); +} + +- (void)testGetAppAttestKeyID_WhenAppAttestKeyIDNotFoundError { + __auto_type getPromise = [self.storage getAppAttestKeyID]; + XCTAssert(FBLWaitForPromisesWithTimeout(0.5)); + XCTAssertNotNil(getPromise.error); + XCTAssertEqualObjects(getPromise.error, [FIRAppCheckErrorUtil appAttestKeyIDNotFound]); +} + +- (void)testSetGetAppAttestKeyIDPerApp { + // Assert storages for apps with the same name can independently set/get app attest key ID. + [self assertIndependentSetGetForStoragesWithAppName1:self.appName + appID1:@"app_id" + appName2:self.appName + appID2:@"app_id_2"]; + // Assert storages for apps with the same app ID can independently set/get app attest key ID. + [self assertIndependentSetGetForStoragesWithAppName1:@"app_1" + appID1:self.appID + appName2:@"app_2" + appID2:self.appID]; + // Assert storages for apps with different info can independently set/get app attest key ID. + [self assertIndependentSetGetForStoragesWithAppName1:@"app_1" + appID1:@"app_id_1" + appName2:@"app_2" + appID2:@"app_id_2"]; +} + +#pragma mark - Helpers + +- (void)assertIndependentSetGetForStoragesWithAppName1:(NSString *)appName1 + appID1:(NSString *)appID1 + appName2:(NSString *)appName2 + appID2:(NSString *)appID2 { + // Create two storages. + FIRAppAttestKeyIDStorage *storage1 = [[FIRAppAttestKeyIDStorage alloc] initWithAppName:appName1 + appID:appID1]; + FIRAppAttestKeyIDStorage *storage2 = [[FIRAppAttestKeyIDStorage alloc] initWithAppName:appName2 + appID:appID2]; + // 1. Independently set app attest key IDs for the two storages. + NSString *appAttestKeyID1 = @"app_attest_key_ID1"; + FBLPromise *setPromise1 = [storage1 setAppAttestKeyID:appAttestKeyID1]; + XCTAssert(FBLWaitForPromisesWithTimeout(0.5)); + XCTAssertEqualObjects(setPromise1.value, appAttestKeyID1); + XCTAssertNil(setPromise1.error); + + NSString *appAttestKeyID2 = @"app_attest_key_ID2"; + __auto_type setPromise2 = [storage2 setAppAttestKeyID:appAttestKeyID2]; + XCTAssert(FBLWaitForPromisesWithTimeout(0.5)); + XCTAssertEqualObjects(setPromise2.value, appAttestKeyID2); + XCTAssertNil(setPromise2.error); + + // 2. Get app attest key IDs for the two storages. + __auto_type getPromise1 = [storage1 getAppAttestKeyID]; + XCTAssert(FBLWaitForPromisesWithTimeout(0.5)); + XCTAssertEqualObjects(getPromise1.value, appAttestKeyID1); + XCTAssertNil(getPromise1.error); + + __auto_type getPromise2 = [storage2 getAppAttestKeyID]; + XCTAssert(FBLWaitForPromisesWithTimeout(0.5)); + XCTAssertEqualObjects(getPromise2.value, appAttestKeyID2); + XCTAssertNil(getPromise2.error); + + // 3. Assert that the app attest key IDs were set and retrieved independently of one another. + XCTAssertNotEqualObjects(getPromise1.value, getPromise2.value); + + // Cleanup storages. + [storage1 setAppAttestKeyID:nil]; + XCTAssert(FBLWaitForPromisesWithTimeout(0.5)); + [storage2 setAppAttestKeyID:nil]; + XCTAssert(FBLWaitForPromisesWithTimeout(0.5)); +} + +@end From 39ae2d35fbd4d43b833235ccad7464b57b0f3588 Mon Sep 17 00:00:00 2001 From: Maksym Malyhin Date: Wed, 5 May 2021 14:09:50 -0400 Subject: [PATCH 03/37] App Check App Attest workflow updates: initial handshake (#8032) * Handshake adjustments (WIP) * Introduce FIRAppAttestProviderState * WIP: calculate attestation state * WIP: calculate attestation state 2 * formatting * Comments and moving code around * Fix init in tests * Fix state calculation flow * Cleanup state calculation and fix tests. * Cleanup and fixes. * Comments * formatting * Fix import * Typo fixes and additional comments * FIRAppAttestInitialHandshakeResponse API * Cleanup state calculation using FBLPromiseAwait * Cleanup * style --- .../FIRAppAttestInitialHandshakeResponse.h | 38 +++++ .../FIRAppAttestInitialHandshakeResponse.m | 30 ++++ .../AppAttestProvider/FIRAppAttestProvider.m | 141 ++++++++++++++---- .../FIRAppAttestProviderState.h | 73 +++++++++ .../FIRAppAttestProviderState.m | 57 +++++++ .../Storage/FIRAppAttestArtifactStorage.h | 40 +++++ .../Storage/FIRAppAttestArtifactStorage.m | 37 +++++ .../FIRAppAttestProviderTests.m | 122 +++++++++------ 8 files changed, 467 insertions(+), 71 deletions(-) create mode 100644 FirebaseAppCheck/Sources/AppAttestProvider/API/FIRAppAttestInitialHandshakeResponse.h create mode 100644 FirebaseAppCheck/Sources/AppAttestProvider/API/FIRAppAttestInitialHandshakeResponse.m create mode 100644 FirebaseAppCheck/Sources/AppAttestProvider/FIRAppAttestProviderState.h create mode 100644 FirebaseAppCheck/Sources/AppAttestProvider/FIRAppAttestProviderState.m create mode 100644 FirebaseAppCheck/Sources/AppAttestProvider/Storage/FIRAppAttestArtifactStorage.h create mode 100644 FirebaseAppCheck/Sources/AppAttestProvider/Storage/FIRAppAttestArtifactStorage.m diff --git a/FirebaseAppCheck/Sources/AppAttestProvider/API/FIRAppAttestInitialHandshakeResponse.h b/FirebaseAppCheck/Sources/AppAttestProvider/API/FIRAppAttestInitialHandshakeResponse.h new file mode 100644 index 00000000000..448d15e8c30 --- /dev/null +++ b/FirebaseAppCheck/Sources/AppAttestProvider/API/FIRAppAttestInitialHandshakeResponse.h @@ -0,0 +1,38 @@ +/* + * Copyright 2021 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 + +@class FIRAppCheckToken; + +NS_ASSUME_NONNULL_BEGIN + +@interface FIRAppAttestInitialHandshakeResponse : NSObject + +/// App Attest attestation artifact required to refresh Firebase App Check token. +@property(nonatomic, readonly) NSData *artifact; + +/// Firebase App Check token. +@property(nonatomic, readonly) FIRAppCheckToken *token; + +- (instancetype)init NS_UNAVAILABLE; + +- (instancetype)initWithArtifact:(NSData *)artifact + token:(FIRAppCheckToken *)token NS_DESIGNATED_INITIALIZER; + +@end + +NS_ASSUME_NONNULL_END diff --git a/FirebaseAppCheck/Sources/AppAttestProvider/API/FIRAppAttestInitialHandshakeResponse.m b/FirebaseAppCheck/Sources/AppAttestProvider/API/FIRAppAttestInitialHandshakeResponse.m new file mode 100644 index 00000000000..b217632979c --- /dev/null +++ b/FirebaseAppCheck/Sources/AppAttestProvider/API/FIRAppAttestInitialHandshakeResponse.m @@ -0,0 +1,30 @@ +/* + * Copyright 2021 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 "FirebaseAppCheck/Sources/AppAttestProvider/API/FIRAppAttestInitialHandshakeResponse.h" + +@implementation FIRAppAttestInitialHandshakeResponse + +- (instancetype)initWithArtifact:(NSData *)artifact token:(FIRAppCheckToken *)token { + self = [super init]; + if (self) { + _artifact = artifact; + _token = token; + } + return self; +} + +@end diff --git a/FirebaseAppCheck/Sources/AppAttestProvider/FIRAppAttestProvider.m b/FirebaseAppCheck/Sources/AppAttestProvider/FIRAppAttestProvider.m index be4e4be35ea..14fd833a3f9 100644 --- a/FirebaseAppCheck/Sources/AppAttestProvider/FIRAppAttestProvider.m +++ b/FirebaseAppCheck/Sources/AppAttestProvider/FIRAppAttestProvider.m @@ -25,7 +25,9 @@ #endif #import "FirebaseAppCheck/Sources/AppAttestProvider/API/FIRAppAttestAPIService.h" +#import "FirebaseAppCheck/Sources/AppAttestProvider/FIRAppAttestProviderState.h" #import "FirebaseAppCheck/Sources/AppAttestProvider/FIRAppAttestService.h" +#import "FirebaseAppCheck/Sources/AppAttestProvider/Storage/FIRAppAttestArtifactStorage.h" #import "FirebaseAppCheck/Sources/AppAttestProvider/Storage/FIRAppAttestKeyIDStorage.h" #import "FirebaseAppCheck/Sources/Core/APIService/FIRAppCheckAPIService.h" #import "FirebaseAppCheck/Sources/Core/Errors/FIRAppCheckErrorUtil.h" @@ -68,6 +70,7 @@ @interface FIRAppAttestProvider () @property(nonatomic, readonly) id APIService; @property(nonatomic, readonly) id appAttestService; @property(nonatomic, readonly) id keyIDStorage; +@property(nonatomic, readonly) id artifactStorage; @property(nonatomic, readonly) dispatch_queue_t queue; @@ -77,12 +80,14 @@ @implementation FIRAppAttestProvider - (instancetype)initWithAppAttestService:(id)appAttestService APIService:(id)APIService - keyIDStorage:(id)keyIDStorage { + keyIDStorage:(id)keyIDStorage + artifactStorage:(id)artifactStorage { self = [super init]; if (self) { _appAttestService = appAttestService; _APIService = APIService; _keyIDStorage = keyIDStorage; + _artifactStorage = artifactStorage; _queue = dispatch_queue_create("com.firebase.FIRAppAttestProvider", DISPATCH_QUEUE_SERIAL); } return self; @@ -107,9 +112,12 @@ - (nullable instancetype)initWithApp:(FIRApp *)app { projectID:app.options.projectID appID:app.options.googleAppID]; + FIRAppAttestArtifactStorage *artifactStorage = [[FIRAppAttestArtifactStorage alloc] init]; + return [self initWithAppAttestService:DCAppAttestService.sharedService APIService:appAttestAPIService - keyIDStorage:keyIDStorage]; + keyIDStorage:keyIDStorage + artifactStorage:artifactStorage]; #else // TARGET_OS_IOS return nil; #endif // TARGET_OS_IOS @@ -118,18 +126,51 @@ - (nullable instancetype)initWithApp:(FIRApp *)app { #pragma mark - FIRAppCheckProvider - (void)getTokenWithCompletion:(void (^)(FIRAppCheckToken *_Nullable, NSError *_Nullable))handler { + [self getToken] + // Call the handler with the result. + .then(^FBLPromise *(FIRAppCheckToken *token) { + handler(token, nil); + return nil; + }) + .catch(^(NSError *error) { + handler(nil, error); + }); +} + +- (FBLPromise *)getToken { + // Check attestation state to decide on the next steps. + return [self attestationState].thenOn(self.queue, ^id(FIRAppAttestProviderState *attestState) { + switch (attestState.state) { + case FIRAppAttestAttestationStateUnsupported: + return attestState.appAttestUnsupportedError; + break; + + case FIRAppAttestAttestationStateSupportedInitial: + case FIRAppAttestAttestationStateKeyGenerated: + // Initial handshake is required for both the "initial" and the "key generated" states. + return [self initialHandshakeWithKeyID:attestState.appAttestKeyID]; + break; + + case FIRAppAttestAttestationStateKeyRegistered: + // Refresh FAC token using the existing registerred App Attest key pair. + return [self refreshTokenWithKeyID:attestState.appAttestKeyID + artifact:attestState.attestationArtifact]; + break; + } + }); +} + +#pragma mark - Initial handshake sequence + +- (FBLPromise *)initialHandshakeWithKeyID:(nullable NSString *)keyID { // 1. Check `DCAppAttestService.isSupported`. - [self isAppAttestSupported] - .thenOn(self.queue, - ^FBLPromise *(id result) { - return [FBLPromise onQueue:self.queue - all:@[ - // 2. Request random challenge. - [self.APIService getRandomChallenge], - // 3. Get App Attest key ID. - [self getAppAttestKeyIDGenerateIfNeeded] - ]]; - }) + return [FBLPromise onQueue:self.queue + all:@[ + // 2. Request random challenge. + [self.APIService getRandomChallenge], + // 3. Get App Attest key ID. + [self generateAppAttestKeyIDIfNeeded:keyID] + ]] .thenOn(self.queue, ^FBLPromise *(NSArray *challengeAndKeyID) { // 4. Attest the key. @@ -138,23 +179,64 @@ - (void)getTokenWithCompletion:(void (^)(FIRAppCheckToken *_Nullable, NSError *_ return [self attestKey:keyID challenge:challenge]; }) + // TODO: Handle a possible key rejection - generate another key. .thenOn(self.queue, ^FBLPromise *(FIRAppAttestKeyAttestationResult *result) { // 5. Exchange the attestation to FAC token. return [self.APIService appCheckTokenWithAttestation:result.attestation keyID:result.keyID challenge:result.challenge]; - }) - // 6. Call the handler with the result. - .then(^FBLPromise *(FIRAppCheckToken *token) { - handler(token, nil); - return nil; - }) - .catch(^(NSError *error) { - handler(nil, error); - }); + }); } +#pragma mark - Token refresh sequence + +- (FBLPromise *)refreshTokenWithKeyID:(NSString *)keyID + artifact:(NSData *)artifact { + // TODO: Implement (b/186438346). + return [FBLPromise resolvedWith:nil]; +} + +#pragma mark - State handling + +- (FBLPromise *)attestationState { + dispatch_queue_t stateQueue = + dispatch_queue_create("FIRAppAttestProvider.state", DISPATCH_QUEUE_SERIAL); + + return [FBLPromise + onQueue:stateQueue + do:^id _Nullable { + NSError *error; + + // 1. Check if App Attest is supported. + id isSupportedResult = FBLPromiseAwait([self isAppAttestSupported], &error); + if (isSupportedResult == nil) { + return [[FIRAppAttestProviderState alloc] initUnsupportedWithError:error]; + } + + // 2. Check for stored key ID of the generated App Attest key pair. + NSString *appAttestKeyID = + FBLPromiseAwait([self.keyIDStorage getAppAttestKeyID], &error); + if (appAttestKeyID == nil) { + return [[FIRAppAttestProviderState alloc] initWithSupportedInitialState]; + } + + // 3. Check for stored attestation artifact received from Firebase backend. + NSData *attestationArtifact = + FBLPromiseAwait([self.artifactStorage getArtifact], &error); + if (attestationArtifact == nil) { + return [[FIRAppAttestProviderState alloc] initWithGeneratedKeyID:appAttestKeyID]; + } + + // 4. A valid App Attest key pair was generated and registered with Firebase + // backend. Return the corresponding state. + return [[FIRAppAttestProviderState alloc] initWithRegisteredKeyID:appAttestKeyID + artifact:attestationArtifact]; + }]; +} + +#pragma mark - Helpers + /// Returns a resolved promise if App Attest is supported and a rejected promise if it is not. - (FBLPromise *)isAppAttestSupported { if (self.appAttestService.isSupported) { @@ -167,12 +249,15 @@ - (void)getTokenWithCompletion:(void (^)(FIRAppCheckToken *_Nullable, NSError *_ } } -/// Retrieves or generates App Attest key associated with the Firebase app. -- (FBLPromise *)getAppAttestKeyIDGenerateIfNeeded { - return [self.keyIDStorage getAppAttestKeyID].recoverOn(self.queue, - ^FBLPromise *(NSError *error) { - return [self generateAppAttestKey]; - }); +/// Generates a new App Attest key associated with the Firebase app if `storedKeyID == nil`. +- (FBLPromise *)generateAppAttestKeyIDIfNeeded:(nullable NSString *)storedKeyID { + if (storedKeyID) { + // The key ID has been fetched already, just return it. + return [FBLPromise resolvedWith:storedKeyID]; + } else { + // Generate and save a new key otherwise. + return [self generateAppAttestKey]; + } } /// Generates and stores App Attest key associated with the Firebase app. diff --git a/FirebaseAppCheck/Sources/AppAttestProvider/FIRAppAttestProviderState.h b/FirebaseAppCheck/Sources/AppAttestProvider/FIRAppAttestProviderState.h new file mode 100644 index 00000000000..4d82b02b9d2 --- /dev/null +++ b/FirebaseAppCheck/Sources/AppAttestProvider/FIRAppAttestProviderState.h @@ -0,0 +1,73 @@ +/* + * Copyright 2021 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 + +NS_ASSUME_NONNULL_BEGIN + +/// Represents different stages of App Attest attestation. +typedef NS_ENUM(NSInteger, FIRAppAttestAttestationState) { + /// App Attest is not supported on the current device. + FIRAppAttestAttestationStateUnsupported, + + /// App Attest is supported, the App Attest key pair has been generated. + FIRAppAttestAttestationStateSupportedInitial, + + /// App Attest key pair has been generated but has not been attested and registered with Firebase + /// backend. + FIRAppAttestAttestationStateKeyGenerated, + + /// App Attest key has been generated, attested with Apple backend and registered with Firebase + /// backend. An encrypted artifact required to refresh FAC token is stored on the device. + FIRAppAttestAttestationStateKeyRegistered, +}; + +/// Represents attestation stages of App Attest. The class is designed to be used exclusively by +/// `FIRAppAttestProvider`. +@interface FIRAppAttestProviderState : NSObject + +/// App Attest attestation state. +@property(nonatomic, readonly) FIRAppAttestAttestationState state; + +/// An error object when state is FIRAppAttestAttestationStateUnsupported. +@property(nonatomic, nullable, readonly) NSError *appAttestUnsupportedError; + +/// An App Attest key ID when state is FIRAppAttestAttestationStateKeyGenerated or +/// FIRAppAttestAttestationStateKeyRegistered. +@property(nonatomic, nullable, readonly) NSString *appAttestKeyID; + +/// An attestation artifact received from Firebase backend when state is +/// FIRAppAttestAttestationStateKeyRegistered. +@property(nonatomic, nullable, readonly) NSData *attestationArtifact; + +- (instancetype)init NS_UNAVAILABLE; + +/// Init with FIRAppAttestAttestationStateUnsupported and an error describing issue. +- (instancetype)initUnsupportedWithError:(NSError *)error; + +/// Init with FIRAppAttestAttestationStateSupportedInitial. +- (instancetype)initWithSupportedInitialState; + +/// Init with FIRAppAttestAttestationStateKeyGenerated and the key ID. +- (instancetype)initWithGeneratedKeyID:(NSString *)keyID; + +/// Init with FIRAppAttestAttestationStateKeyRegistered, the key ID and the attestation artifact +/// received from Firebase backend. +- (instancetype)initWithRegisteredKeyID:(NSString *)keyID artifact:(NSData *)artifact; + +@end + +NS_ASSUME_NONNULL_END diff --git a/FirebaseAppCheck/Sources/AppAttestProvider/FIRAppAttestProviderState.m b/FirebaseAppCheck/Sources/AppAttestProvider/FIRAppAttestProviderState.m new file mode 100644 index 00000000000..c71be31c00d --- /dev/null +++ b/FirebaseAppCheck/Sources/AppAttestProvider/FIRAppAttestProviderState.m @@ -0,0 +1,57 @@ +/* + * Copyright 2021 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 "FirebaseAppCheck/Sources/AppAttestProvider/FIRAppAttestProviderState.h" + +@implementation FIRAppAttestProviderState + +- (instancetype)initUnsupportedWithError:(NSError *)error { + self = [super init]; + if (self) { + _state = FIRAppAttestAttestationStateUnsupported; + _appAttestUnsupportedError = error; + } + return self; +} + +- (instancetype)initWithSupportedInitialState { + self = [super init]; + if (self) { + _state = FIRAppAttestAttestationStateSupportedInitial; + } + return self; +} + +- (instancetype)initWithGeneratedKeyID:(NSString *)keyID { + self = [super init]; + if (self) { + _state = FIRAppAttestAttestationStateKeyGenerated; + _appAttestKeyID = keyID; + } + return self; +} + +- (instancetype)initWithRegisteredKeyID:(NSString *)keyID artifact:(NSData *)artifact { + self = [super init]; + if (self) { + _state = FIRAppAttestAttestationStateKeyRegistered; + _appAttestKeyID = keyID; + _attestationArtifact = artifact; + } + return self; +} + +@end diff --git a/FirebaseAppCheck/Sources/AppAttestProvider/Storage/FIRAppAttestArtifactStorage.h b/FirebaseAppCheck/Sources/AppAttestProvider/Storage/FIRAppAttestArtifactStorage.h new file mode 100644 index 00000000000..00be919454d --- /dev/null +++ b/FirebaseAppCheck/Sources/AppAttestProvider/Storage/FIRAppAttestArtifactStorage.h @@ -0,0 +1,40 @@ +/* + * Copyright 2021 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 + +@class FBLPromise; + +NS_ASSUME_NONNULL_BEGIN + +/// Defines API of a storage capable to store an encrypted artifact required to refresh Firebase App +/// Check token obtained with App Attest provider. +@protocol FIRAppAttestArtifactStorageProtocol + +/// Set the artifact. +- (FBLPromise *)setArtifact:(NSData *)artifact; + +/// Get the artifact. +- (FBLPromise *)getArtifact; + +@end + +/// An implementation of FIRAppAttestArtifactStorageProtocol. +@interface FIRAppAttestArtifactStorage : NSObject + +@end + +NS_ASSUME_NONNULL_END diff --git a/FirebaseAppCheck/Sources/AppAttestProvider/Storage/FIRAppAttestArtifactStorage.m b/FirebaseAppCheck/Sources/AppAttestProvider/Storage/FIRAppAttestArtifactStorage.m new file mode 100644 index 00000000000..64accd472ae --- /dev/null +++ b/FirebaseAppCheck/Sources/AppAttestProvider/Storage/FIRAppAttestArtifactStorage.m @@ -0,0 +1,37 @@ +/* + * Copyright 2021 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 "FirebaseAppCheck/Sources/AppAttestProvider/Storage/FIRAppAttestArtifactStorage.h" + +#if __has_include() +#import +#else +#import "FBLPromises.h" +#endif + +@implementation FIRAppAttestArtifactStorage + +- (nonnull FBLPromise *)getArtifact { + // TODO: Implement. + return [FBLPromise resolvedWith:nil]; +} + +- (nonnull FBLPromise *)setArtifact:(nonnull NSData *)artifact { + // TODO: Implement. + return [FBLPromise resolvedWith:nil]; +} + +@end diff --git a/FirebaseAppCheck/Tests/Unit/AppAttestProvider/FIRAppAttestProviderTests.m b/FirebaseAppCheck/Tests/Unit/AppAttestProvider/FIRAppAttestProviderTests.m index ead1c886161..9ad57e7bcdd 100644 --- a/FirebaseAppCheck/Tests/Unit/AppAttestProvider/FIRAppAttestProviderTests.m +++ b/FirebaseAppCheck/Tests/Unit/AppAttestProvider/FIRAppAttestProviderTests.m @@ -23,6 +23,7 @@ #import "FirebaseAppCheck/Sources/AppAttestProvider/API/FIRAppAttestAPIService.h" #import "FirebaseAppCheck/Sources/AppAttestProvider/FIRAppAttestService.h" +#import "FirebaseAppCheck/Sources/AppAttestProvider/Storage/FIRAppAttestArtifactStorage.h" #import "FirebaseAppCheck/Sources/AppAttestProvider/Storage/FIRAppAttestKeyIDStorage.h" #import "FirebaseAppCheck/Sources/Core/Errors/FIRAppCheckErrorUtil.h" #import "FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRAppCheckToken.h" @@ -35,7 +36,8 @@ @interface FIRAppAttestProvider (Tests) - (instancetype)initWithAppAttestService:(id)appAttestService APIService:(id)APIService - keyIDStorage:(id)keyIDStorage; + keyIDStorage:(id)keyIDStorage + artifactStorage:(id)artifactStorage; @end API_AVAILABLE(ios(14.0)) @@ -46,6 +48,7 @@ @interface FIRAppAttestProviderTests : XCTestCase @property(nonatomic) OCMockObject *mockAppAttestService; @property(nonatomic) OCMockObject *mockAPIService; @property(nonatomic) OCMockObject *mockStorage; +@property(nonatomic) OCMockObject *mockArtifactStorage; @end @@ -57,14 +60,17 @@ - (void)setUp { self.mockAppAttestService = OCMProtocolMock(@protocol(FIRAppAttestService)); self.mockAPIService = OCMProtocolMock(@protocol(FIRAppAttestAPIServiceProtocol)); self.mockStorage = OCMProtocolMock(@protocol(FIRAppAttestKeyIDStorageProtocol)); + self.mockArtifactStorage = OCMProtocolMock(@protocol(FIRAppAttestArtifactStorageProtocol)); self.provider = [[FIRAppAttestProvider alloc] initWithAppAttestService:self.mockAppAttestService APIService:self.mockAPIService - keyIDStorage:self.mockStorage]; + keyIDStorage:self.mockStorage + artifactStorage:self.mockArtifactStorage]; } - (void)tearDown { self.provider = nil; + self.mockArtifactStorage = nil; self.mockStorage = nil; self.mockAPIService = nil; self.mockAppAttestService = nil; @@ -86,6 +92,7 @@ - (void)testGetTokenWhenAppAttestIsNotSupported { // 2. Don't expect other operations. OCMReject([self.mockStorage getAppAttestKeyID]); OCMReject([self.mockAppAttestService generateKeyWithCompletionHandler:OCMOCK_ANY]); + OCMReject([self.mockArtifactStorage getArtifact]); OCMReject([self.mockAPIService getRandomChallenge]); OCMReject([self.mockStorage setAppAttestKeyID:OCMOCK_ANY]); OCMReject([self.mockAppAttestService attestKey:OCMOCK_ANY @@ -110,9 +117,7 @@ - (void)testGetTokenWhenAppAttestIsNotSupported { [self waitForExpectations:@[ completionExpectation ] timeout:0.5]; // 4. Verify mocks. - OCMVerifyAll(self.mockAppAttestService); - OCMVerifyAll(self.mockAPIService); - OCMVerifyAll(self.mockStorage); + [self verifyAllMocks]; } - (void)testGetToken_WhenNoExistingKey_Success { @@ -172,12 +177,10 @@ - (void)testGetToken_WhenNoExistingKey_Success { [self waitForExpectations:@[ completionExpectation ] timeout:0.5]; // 9. Verify mocks. - OCMVerifyAll(self.mockAppAttestService); - OCMVerifyAll(self.mockAPIService); - OCMVerifyAll(self.mockStorage); + [self verifyAllMocks]; } -- (void)testGetToken_WhenExistingKey_Success { +- (void)testGetToken_WhenExistingUnregisteredKey_Success { // 1. Expect FIRAppAttestService.isSupported. [OCMExpect([self.mockAppAttestService isSupported]) andReturnValue:@(YES)]; @@ -192,12 +195,20 @@ - (void)testGetToken_WhenExistingKey_Success { // 4. Don't expect the key ID to be stored. OCMReject([self.mockStorage setAppAttestKeyID:OCMOCK_ANY]); - // 5. Expect random challenge to be requested. + // 5. Expect a stored artifact to be requested. + __auto_type rejectedPromise = + [self rejectedPromiseWithError: + [NSError errorWithDomain:@"testGetToken_WhenExistingUnregisteredKey_Success" + code:NSNotFound + userInfo:nil]]; + OCMExpect([self.mockArtifactStorage getArtifact]).andReturn(rejectedPromise); + + // 6. Expect random challenge to be requested. NSData *randomChallenge = [@"random challenge" dataUsingEncoding:NSUTF8StringEncoding]; OCMExpect([self.mockAPIService getRandomChallenge]) .andReturn([FBLPromise resolvedWith:randomChallenge]); - // 6. Expect the key to be attested with the challenge. + // 7. Expect the key to be attested with the challenge. NSData *expectedChallengeHash = [randomChallenge base64EncodedDataWithOptions:0]; NSData *attestationData = [@"attestation data" dataUsingEncoding:NSUTF8StringEncoding]; id attestCompletionArg = [OCMArg invokeBlockWithArgs:attestationData, [NSNull null], nil]; @@ -205,7 +216,7 @@ - (void)testGetToken_WhenExistingKey_Success { clientDataHash:expectedChallengeHash completionHandler:attestCompletionArg]); - // 7. Expect exchange request to be sent. + // 8. Expect exchange request to be sent. FIRAppCheckToken *FACToken = [[FIRAppCheckToken alloc] initWithToken:@"FAC token" expirationDate:[NSDate date]]; OCMExpect([self.mockAPIService appCheckTokenWithAttestation:attestationData @@ -213,7 +224,7 @@ - (void)testGetToken_WhenExistingKey_Success { challenge:randomChallenge]) .andReturn([FBLPromise resolvedWith:FACToken]); - // 8. Call get token. + // 9. Call get token. XCTestExpectation *completionExpectation = [self expectationWithDescription:@"completionExpectation"]; [self.provider @@ -227,13 +238,11 @@ - (void)testGetToken_WhenExistingKey_Success { [self waitForExpectations:@[ completionExpectation ] timeout:0.5]; - // 9. Verify mocks. - OCMVerifyAll(self.mockAppAttestService); - OCMVerifyAll(self.mockAPIService); - OCMVerifyAll(self.mockStorage); + // 10. Verify mocks. + [self verifyAllMocks]; } -- (void)testGetToken_WhenRandomChallengeError { +- (void)testGetToken_WhenUnregisteredKeyAndRandomChallengeError { // 1. Expect FIRAppAttestService.isSupported. [OCMExpect([self.mockAppAttestService isSupported]) andReturnValue:@(YES)]; @@ -242,14 +251,22 @@ - (void)testGetToken_WhenRandomChallengeError { OCMExpect([self.mockStorage getAppAttestKeyID]) .andReturn([FBLPromise resolvedWith:existingKeyID]); - // 3. Expect random challenge to be requested. + // 3. Expect a stored artifact to be requested. + __auto_type rejectedPromise = + [self rejectedPromiseWithError: + [NSError errorWithDomain:@"testGetToken_WhenExistingUnregisteredKey_Success" + code:NSNotFound + userInfo:nil]]; + OCMExpect([self.mockArtifactStorage getArtifact]).andReturn(rejectedPromise); + + // 4. Expect random challenge to be requested. NSError *challengeError = [NSError errorWithDomain:@"testGetToken_WhenRandomChallengeError" code:NSNotFound userInfo:nil]; OCMExpect([self.mockAPIService getRandomChallenge]) .andReturn([self rejectedPromiseWithError:challengeError]); - // 4. Don't expect other steps. + // 5. Don't expect other steps. OCMReject([self.mockStorage setAppAttestKeyID:OCMOCK_ANY]); OCMReject([self.mockAppAttestService attestKey:OCMOCK_ANY clientDataHash:OCMOCK_ANY @@ -258,7 +275,7 @@ - (void)testGetToken_WhenRandomChallengeError { keyID:OCMOCK_ANY challenge:OCMOCK_ANY]); - // 5. Call get token. + // 6. Call get token. XCTestExpectation *completionExpectation = [self expectationWithDescription:@"completionExpectation"]; [self.provider @@ -271,13 +288,11 @@ - (void)testGetToken_WhenRandomChallengeError { [self waitForExpectations:@[ completionExpectation ] timeout:0.5]; - // 6. Verify mocks. - OCMVerifyAll(self.mockAppAttestService); - OCMVerifyAll(self.mockAPIService); - OCMVerifyAll(self.mockStorage); + // 7. Verify mocks. + [self verifyAllMocks]; } -- (void)testGetTokenWhenKeyAttestationError { +- (void)testGetToken_WhenUnregisteredKeyAndKeyAttestationError { // 1. Expect FIRAppAttestService.isSupported. [OCMExpect([self.mockAppAttestService isSupported]) andReturnValue:@(YES)]; @@ -286,12 +301,20 @@ - (void)testGetTokenWhenKeyAttestationError { OCMExpect([self.mockStorage getAppAttestKeyID]) .andReturn([FBLPromise resolvedWith:existingKeyID]); - // 3. Expect random challenge to be requested. + // 3. Expect a stored artifact to be requested. + __auto_type rejectedPromise = + [self rejectedPromiseWithError: + [NSError errorWithDomain:@"testGetToken_WhenExistingUnregisteredKey_Success" + code:NSNotFound + userInfo:nil]]; + OCMExpect([self.mockArtifactStorage getArtifact]).andReturn(rejectedPromise); + + // 4. Expect random challenge to be requested. NSData *randomChallenge = [@"random challenge" dataUsingEncoding:NSUTF8StringEncoding]; OCMExpect([self.mockAPIService getRandomChallenge]) .andReturn([FBLPromise resolvedWith:randomChallenge]); - // 4. Expect the key to be attested with the challenge. + // 5. Expect the key to be attested with the challenge. NSData *expectedChallengeHash = [randomChallenge base64EncodedDataWithOptions:0]; NSError *attestationError = [NSError errorWithDomain:@"testGetTokenWhenKeyAttestationError" code:0 @@ -301,12 +324,12 @@ - (void)testGetTokenWhenKeyAttestationError { clientDataHash:expectedChallengeHash completionHandler:attestCompletionArg]); - // 5. Don't exchange API request. + // 6. Don't exchange API request. OCMReject([self.mockAPIService appCheckTokenWithAttestation:OCMOCK_ANY keyID:OCMOCK_ANY challenge:OCMOCK_ANY]); - // 6. Call get token. + // 7. Call get token. XCTestExpectation *completionExpectation = [self expectationWithDescription:@"completionExpectation"]; [self.provider @@ -319,13 +342,11 @@ - (void)testGetTokenWhenKeyAttestationError { [self waitForExpectations:@[ completionExpectation ] timeout:0.5]; - // 7. Verify mocks. - OCMVerifyAll(self.mockAppAttestService); - OCMVerifyAll(self.mockAPIService); - OCMVerifyAll(self.mockStorage); + // 8. Verify mocks. + [self verifyAllMocks]; } -- (void)testGetTokenWhenKeyAttestationExchangeError { +- (void)testGetToken_WhenUnregisteredKeyAndKeyAttestationExchangeError { // 1. Expect FIRAppAttestService.isSupported. [OCMExpect([self.mockAppAttestService isSupported]) andReturnValue:@(YES)]; @@ -334,12 +355,20 @@ - (void)testGetTokenWhenKeyAttestationExchangeError { OCMExpect([self.mockStorage getAppAttestKeyID]) .andReturn([FBLPromise resolvedWith:existingKeyID]); - // 3. Expect random challenge to be requested. + // 3. Expect a stored artifact to be requested. + __auto_type rejectedPromise = + [self rejectedPromiseWithError: + [NSError errorWithDomain:@"testGetToken_WhenExistingUnregisteredKey_Success" + code:NSNotFound + userInfo:nil]]; + OCMExpect([self.mockArtifactStorage getArtifact]).andReturn(rejectedPromise); + + // 4. Expect random challenge to be requested. NSData *randomChallenge = [@"random challenge" dataUsingEncoding:NSUTF8StringEncoding]; OCMExpect([self.mockAPIService getRandomChallenge]) .andReturn([FBLPromise resolvedWith:randomChallenge]); - // 4. Expect the key to be attested with the challenge. + // 5. Expect the key to be attested with the challenge. NSData *expectedChallengeHash = [randomChallenge base64EncodedDataWithOptions:0]; NSData *attestationData = [@"attestation data" dataUsingEncoding:NSUTF8StringEncoding]; id attestCompletionArg = [OCMArg invokeBlockWithArgs:attestationData, [NSNull null], nil]; @@ -347,7 +376,7 @@ - (void)testGetTokenWhenKeyAttestationExchangeError { clientDataHash:expectedChallengeHash completionHandler:attestCompletionArg]); - // 7. Expect exchange request to be sent. + // 6. Expect exchange request to be sent. NSError *exchangeError = [NSError errorWithDomain:@"testGetTokenWhenKeyAttestationExchangeError" code:0 userInfo:nil]; @@ -356,7 +385,7 @@ - (void)testGetTokenWhenKeyAttestationExchangeError { challenge:randomChallenge]) .andReturn([self rejectedPromiseWithError:exchangeError]); - // 5. Call get token. + // 7. Call get token. XCTestExpectation *completionExpectation = [self expectationWithDescription:@"completionExpectation"]; [self.provider @@ -369,12 +398,12 @@ - (void)testGetTokenWhenKeyAttestationExchangeError { [self waitForExpectations:@[ completionExpectation ] timeout:0.5]; - // 6. Verify mocks. - OCMVerifyAll(self.mockAppAttestService); - OCMVerifyAll(self.mockAPIService); - OCMVerifyAll(self.mockStorage); + // 8. Verify mocks. + [self verifyAllMocks]; } +// TODO: FAC token refresh tests (b/186438346). + #pragma mark - Helpers - (FBLPromise *)rejectedPromiseWithError:(NSError *)error { @@ -383,6 +412,13 @@ - (FBLPromise *)rejectedPromiseWithError:(NSError *)error { return rejectedPromise; } +- (void)verifyAllMocks { + OCMVerifyAll(self.mockAppAttestService); + OCMVerifyAll(self.mockAPIService); + OCMVerifyAll(self.mockStorage); + OCMVerifyAll(self.mockArtifactStorage); +} + @end #endif // TARGET_OS_IOS From ccac49caf1695f0a0ab95ac54022c034540159df Mon Sep 17 00:00:00 2001 From: Maksym Malyhin Date: Wed, 5 May 2021 18:18:58 -0400 Subject: [PATCH 04/37] FIRAppAttestArtifactStorage implementation and tests (#8041) * Update comments * FIRAppAttestArtifactStorage implementation and tests * Fix init * API docs * Clean up storage in tests * Comments * Disable Keychain dependent tests for SPM --- .../AppAttestProvider/FIRAppAttestProvider.m | 15 +- .../Storage/FIRAppAttestArtifactStorage.h | 18 ++- .../Storage/FIRAppAttestArtifactStorage.m | 78 ++++++++- .../FIRAppAttestArtifactStorageTests.m | 148 ++++++++++++++++++ .../FIRAppAttestKeyIDStorageTests.m | 0 Package.swift | 1 + 6 files changed, 247 insertions(+), 13 deletions(-) create mode 100644 FirebaseAppCheck/Tests/Unit/AppAttestProvider/Storage/FIRAppAttestArtifactStorageTests.m rename FirebaseAppCheck/Tests/Unit/AppAttestProvider/{ => Storage}/FIRAppAttestKeyIDStorageTests.m (100%) diff --git a/FirebaseAppCheck/Sources/AppAttestProvider/FIRAppAttestProvider.m b/FirebaseAppCheck/Sources/AppAttestProvider/FIRAppAttestProvider.m index 14fd833a3f9..9a1346f9829 100644 --- a/FirebaseAppCheck/Sources/AppAttestProvider/FIRAppAttestProvider.m +++ b/FirebaseAppCheck/Sources/AppAttestProvider/FIRAppAttestProvider.m @@ -112,7 +112,10 @@ - (nullable instancetype)initWithApp:(FIRApp *)app { projectID:app.options.projectID appID:app.options.googleAppID]; - FIRAppAttestArtifactStorage *artifactStorage = [[FIRAppAttestArtifactStorage alloc] init]; + FIRAppAttestArtifactStorage *artifactStorage = + [[FIRAppAttestArtifactStorage alloc] initWithAppName:app.name + appID:app.options.googleAppID + accessGroup:app.options.appGroupID]; return [self initWithAppAttestService:DCAppAttestService.sharedService APIService:appAttestAPIService @@ -163,17 +166,17 @@ - (void)getTokenWithCompletion:(void (^)(FIRAppCheckToken *_Nullable, NSError *_ #pragma mark - Initial handshake sequence - (FBLPromise *)initialHandshakeWithKeyID:(nullable NSString *)keyID { - // 1. Check `DCAppAttestService.isSupported`. + // 1. Request a random challenge and get App Attest key ID concurrently. return [FBLPromise onQueue:self.queue all:@[ - // 2. Request random challenge. + // 1.1. Request random challenge. [self.APIService getRandomChallenge], - // 3. Get App Attest key ID. + // 1.2. Get App Attest key ID. [self generateAppAttestKeyIDIfNeeded:keyID] ]] .thenOn(self.queue, ^FBLPromise *(NSArray *challengeAndKeyID) { - // 4. Attest the key. + // 2. Attest the key. NSData *challenge = challengeAndKeyID.firstObject; NSString *keyID = challengeAndKeyID.lastObject; @@ -182,7 +185,7 @@ - (void)getTokenWithCompletion:(void (^)(FIRAppCheckToken *_Nullable, NSError *_ // TODO: Handle a possible key rejection - generate another key. .thenOn(self.queue, ^FBLPromise *(FIRAppAttestKeyAttestationResult *result) { - // 5. Exchange the attestation to FAC token. + // 3. Exchange the attestation to FAC token. return [self.APIService appCheckTokenWithAttestation:result.attestation keyID:result.keyID challenge:result.challenge]; diff --git a/FirebaseAppCheck/Sources/AppAttestProvider/Storage/FIRAppAttestArtifactStorage.h b/FirebaseAppCheck/Sources/AppAttestProvider/Storage/FIRAppAttestArtifactStorage.h index 00be919454d..91fe25e1085 100644 --- a/FirebaseAppCheck/Sources/AppAttestProvider/Storage/FIRAppAttestArtifactStorage.h +++ b/FirebaseAppCheck/Sources/AppAttestProvider/Storage/FIRAppAttestArtifactStorage.h @@ -25,9 +25,14 @@ NS_ASSUME_NONNULL_BEGIN @protocol FIRAppAttestArtifactStorageProtocol /// Set the artifact. -- (FBLPromise *)setArtifact:(NSData *)artifact; +/// @param artifact The artifact data to store. Pass `nil` to remove the stored artifact. +/// @return An artifact that is resolved with the artifact data passed into the method in case of +/// success or is rejected with an error. +- (FBLPromise *)setArtifact:(nullable NSData *)artifact; /// Get the artifact. +/// @return A promise that is resolved with the artifact data if artifact exists, is resolved with +/// `nil` if no artifact found or is rejected with an error. - (FBLPromise *)getArtifact; @end @@ -35,6 +40,17 @@ NS_ASSUME_NONNULL_BEGIN /// An implementation of FIRAppAttestArtifactStorageProtocol. @interface FIRAppAttestArtifactStorage : NSObject +- (instancetype)init NS_UNAVAILABLE; + +/// A default initializer. +/// @param appName A Firebase App name (`FirebaseApp.name`). The app name will be used as a part of +/// the key to store the token for the storage instance. +/// @param appID A Firebase App identifier (`FirebaseOptions.googleAppID`). The app ID will be used +/// as a part of the key to store the token for the storage instance. +/// @param accessGroup The Keychain Access Group. +- (instancetype)initWithAppName:(NSString *)appName + appID:(NSString *)appID + accessGroup:(nullable NSString *)accessGroup; @end NS_ASSUME_NONNULL_END diff --git a/FirebaseAppCheck/Sources/AppAttestProvider/Storage/FIRAppAttestArtifactStorage.m b/FirebaseAppCheck/Sources/AppAttestProvider/Storage/FIRAppAttestArtifactStorage.m index 64accd472ae..744370647f6 100644 --- a/FirebaseAppCheck/Sources/AppAttestProvider/Storage/FIRAppAttestArtifactStorage.m +++ b/FirebaseAppCheck/Sources/AppAttestProvider/Storage/FIRAppAttestArtifactStorage.m @@ -22,16 +22,82 @@ #import "FBLPromises.h" #endif +#import + +NS_ASSUME_NONNULL_BEGIN + +static NSString *const kKeychainService = @"com.firebase.app_check.app_attest_artifact_storage"; + +@interface FIRAppAttestArtifactStorage () + +@property(nonatomic, readonly) NSString *appName; +@property(nonatomic, readonly) NSString *appID; +@property(nonatomic, readonly) GULKeychainStorage *keychainStorage; +@property(nonatomic, readonly, nullable) NSString *accessGroup; + +@end + @implementation FIRAppAttestArtifactStorage -- (nonnull FBLPromise *)getArtifact { - // TODO: Implement. - return [FBLPromise resolvedWith:nil]; +- (instancetype)initWithAppName:(NSString *)appName + appID:(NSString *)appID + keychainStorage:(GULKeychainStorage *)keychainStorage + accessGroup:(nullable NSString *)accessGroup { + self = [super init]; + if (self) { + _appName = [appName copy]; + _appID = [appID copy]; + _keychainStorage = keychainStorage; + _accessGroup = [accessGroup copy]; + } + return self; } -- (nonnull FBLPromise *)setArtifact:(nonnull NSData *)artifact { - // TODO: Implement. - return [FBLPromise resolvedWith:nil]; +- (instancetype)initWithAppName:(NSString *)appName + appID:(NSString *)appID + accessGroup:(nullable NSString *)accessGroup { + GULKeychainStorage *keychainStorage = + [[GULKeychainStorage alloc] initWithService:kKeychainService]; + return [self initWithAppName:appName + appID:appID + keychainStorage:keychainStorage + accessGroup:accessGroup]; +} + +- (FBLPromise *)getArtifact { + return [self.keychainStorage getObjectForKey:[self artifactKey] + objectClass:[NSData class] + accessGroup:self.accessGroup] + .then(^NSData *(id storedArtifact) { + if ([(NSObject *)storedArtifact isKindOfClass:[NSData class]]) { + return (NSData *)storedArtifact; + } else { + return nil; + } + }); +} + +- (FBLPromise *)setArtifact:(nullable NSData *)artifact { + if (artifact) { + return [self.keychainStorage setObject:artifact + forKey:[self artifactKey] + accessGroup:self.accessGroup] + .then(^id _Nullable(NSNull *_Nullable value) { + return artifact; + }); + } else { + return [self.keychainStorage removeObjectForKey:[self artifactKey] accessGroup:self.accessGroup] + .then(^id _Nullable(NSNull *_Nullable value) { + return nil; + }); + } +} + +- (NSString *)artifactKey { + return + [NSString stringWithFormat:@"app_check_app_attest_artifact.%@.%@", self.appName, self.appID]; } @end + +NS_ASSUME_NONNULL_END diff --git a/FirebaseAppCheck/Tests/Unit/AppAttestProvider/Storage/FIRAppAttestArtifactStorageTests.m b/FirebaseAppCheck/Tests/Unit/AppAttestProvider/Storage/FIRAppAttestArtifactStorageTests.m new file mode 100644 index 00000000000..4a9beb429e3 --- /dev/null +++ b/FirebaseAppCheck/Tests/Unit/AppAttestProvider/Storage/FIRAppAttestArtifactStorageTests.m @@ -0,0 +1,148 @@ +/* + * Copyright 2021 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 + +#import "FBLPromise+Testing.h" + +#import "FirebaseAppCheck/Sources/AppAttestProvider/Storage/FIRAppAttestArtifactStorage.h" + +@interface FIRAppAttestArtifactStorageTests : XCTestCase + +@property(nonatomic) NSString *appName; +@property(nonatomic) NSString *appID; +@property(nonatomic) FIRAppAttestArtifactStorage *storage; + +@end + +@implementation FIRAppAttestArtifactStorageTests + +- (void)setUp { + [super setUp]; + + self.appName = @"FIRAppAttestArtifactStorageTests"; + self.appID = @"1:100000000000:ios:aaaaaaaaaaaaaaaaaaaaaaaa"; + + self.storage = [[FIRAppAttestArtifactStorage alloc] initWithAppName:self.appName + appID:self.appID + accessGroup:nil]; +} + +- (void)tearDown { + // Cleanup the storage. + [self.storage setArtifact:nil]; + self.storage = nil; + [super tearDown]; +} + +- (void)testSetAndGetArtifact { + [self assertSetGetForStorage]; +} + +- (void)testRemoveArtifact { + // 1. Save an artifact to storage and check it is stored. + [self assertSetGetForStorage]; + + // 2. Remove artifact. + __auto_type setPromise = [self.storage setArtifact:nil]; + XCTAssert(FBLWaitForPromisesWithTimeout(0.5)); + XCTAssertNil(setPromise.value); + XCTAssertNil(setPromise.error); + + // 3. Check it has been removed. + __auto_type getPromise = [self.storage getArtifact]; + XCTAssert(FBLWaitForPromisesWithTimeout(0.5)); + XCTAssertNil(getPromise.value); + XCTAssertNil(getPromise.error); +} + +- (void)testSetAndGetPerApp { + // Assert storages for apps with the same name can independently set/get artifact. + [self assertIndependentSetGetForStoragesWithAppName1:self.appName + appID1:@"app_id" + appName2:self.appName + appID2:@"app_id_2"]; + // Assert storages for apps with the same app ID can independently set/get artifact. + [self assertIndependentSetGetForStoragesWithAppName1:@"app_1" + appID1:self.appID + appName2:@"app_2" + appID2:self.appID]; + // Assert storages for apps with different info can independently set/get artifact. + [self assertIndependentSetGetForStoragesWithAppName1:@"app_1" + appID1:@"app_id_1" + appName2:@"app_2" + appID2:@"app_id_2"]; +} + +#pragma mark - Helpers + +- (void)assertSetGetForStorage { + NSData *artifactToSet = [[NSUUID UUID].UUIDString dataUsingEncoding:NSUTF8StringEncoding]; + + __auto_type setPromise = [self.storage setArtifact:artifactToSet]; + XCTAssert(FBLWaitForPromisesWithTimeout(0.5)); + XCTAssertEqualObjects(setPromise.value, artifactToSet); + XCTAssertNil(setPromise.error); + + __auto_type getPromise = [self.storage getArtifact]; + XCTAssert(FBLWaitForPromisesWithTimeout(0.5)); + XCTAssertEqualObjects(getPromise.value, artifactToSet); + XCTAssertNil(getPromise.error); +} + +- (void)assertIndependentSetGetForStoragesWithAppName1:(NSString *)appName1 + appID1:(NSString *)appID1 + appName2:(NSString *)appName2 + appID2:(NSString *)appID2 { + // Create two storages. + FIRAppAttestArtifactStorage *storage1 = + [[FIRAppAttestArtifactStorage alloc] initWithAppName:appName1 appID:appID1 accessGroup:nil]; + FIRAppAttestArtifactStorage *storage2 = + [[FIRAppAttestArtifactStorage alloc] initWithAppName:appName2 appID:appID2 accessGroup:nil]; + // 1. Independently set artifacts for the two storages. + NSData *artifact1 = [@"app_attest_artifact1" dataUsingEncoding:NSUTF8StringEncoding]; + FBLPromise *setPromise1 = [storage1 setArtifact:artifact1]; + XCTAssert(FBLWaitForPromisesWithTimeout(0.5)); + XCTAssertEqualObjects(setPromise1.value, artifact1); + XCTAssertNil(setPromise1.error); + + NSData *artifact2 = [@"app_attest_artifact2" dataUsingEncoding:NSUTF8StringEncoding]; + __auto_type setPromise2 = [storage2 setArtifact:artifact2]; + XCTAssert(FBLWaitForPromisesWithTimeout(0.5)); + XCTAssertEqualObjects(setPromise2.value, artifact2); + XCTAssertNil(setPromise2.error); + + // 2. Get artifacts for the two storages. + __auto_type getPromise1 = [storage1 getArtifact]; + XCTAssert(FBLWaitForPromisesWithTimeout(0.5)); + XCTAssertEqualObjects(getPromise1.value, artifact1); + XCTAssertNil(getPromise1.error); + + __auto_type getPromise2 = [storage2 getArtifact]; + XCTAssert(FBLWaitForPromisesWithTimeout(0.5)); + XCTAssertEqualObjects(getPromise2.value, artifact2); + XCTAssertNil(getPromise2.error); + + // 3. Assert that artifacts were set and retrieved independently of one another. + XCTAssertNotEqualObjects(getPromise1.value, getPromise2.value); + + // Cleanup storages. + [storage1 setArtifact:nil]; + [storage2 setArtifact:nil]; + XCTAssert(FBLWaitForPromisesWithTimeout(0.5)); +} + +@end diff --git a/FirebaseAppCheck/Tests/Unit/AppAttestProvider/FIRAppAttestKeyIDStorageTests.m b/FirebaseAppCheck/Tests/Unit/AppAttestProvider/Storage/FIRAppAttestKeyIDStorageTests.m similarity index 100% rename from FirebaseAppCheck/Tests/Unit/AppAttestProvider/FIRAppAttestKeyIDStorageTests.m rename to FirebaseAppCheck/Tests/Unit/AppAttestProvider/Storage/FIRAppAttestKeyIDStorageTests.m diff --git a/Package.swift b/Package.swift index 0fd153d8df9..c2d5aaa9ffb 100644 --- a/Package.swift +++ b/Package.swift @@ -949,6 +949,7 @@ let package = Package( // Disable Keychain dependent tests as they require a host application on iOS. "Integration", + "Unit/AppAttestProvider/Storage/FIRAppAttestArtifactStorageTests.m", "Unit/Core/FIRAppCheckIntegrationTests.m", "Unit/Core/FIRAppCheckStorageTests.m", ], From c7558b58e318410d731f8b8e50f328e889026918 Mon Sep 17 00:00:00 2001 From: Nick Cooke <36927374+ncooke3@users.noreply.github.com> Date: Thu, 6 May 2021 09:54:56 -0400 Subject: [PATCH 05/37] Implement App Attest `getRandomChallenge` (#8033) * Initial implementation * Parse response body for challenge and stub test cases * Review [Draft] * Avoid encoding challenge again * Add tests * Revert "Avoid encoding challenge again" and add TODO This reverts commit 69eb00dfe2b5bc25a03f2e971bfc7ae6531bbeee. * Document tests; Add test * Tests: Add URL validation check * Review --- .../API/FIRAppAttestAPIService.m | 67 ++++- .../AppAttestProvider/FIRAppAttestProvider.m | 1 + .../Core/APIService/FIRAppCheckAPIService.h | 2 +- .../Core/APIService/FIRAppCheckAPIService.m | 10 +- .../API/FIRAppCheckDebugProviderAPIService.m | 2 +- .../AppAttestResponseMissingChallenge.json | 3 + .../Fixture/AppAttestResponseSuccess.json | 4 + .../FIRAppAttestAPIServiceTests.m | 233 ++++++++++++++++++ 8 files changed, 315 insertions(+), 7 deletions(-) create mode 100644 FirebaseAppCheck/Tests/Fixture/AppAttestResponseMissingChallenge.json create mode 100644 FirebaseAppCheck/Tests/Fixture/AppAttestResponseSuccess.json create mode 100644 FirebaseAppCheck/Tests/Unit/AppAttestProvider/FIRAppAttestAPIServiceTests.m diff --git a/FirebaseAppCheck/Sources/AppAttestProvider/API/FIRAppAttestAPIService.m b/FirebaseAppCheck/Sources/AppAttestProvider/API/FIRAppAttestAPIService.m index f05ae648bde..b8ed562213f 100644 --- a/FirebaseAppCheck/Sources/AppAttestProvider/API/FIRAppAttestAPIService.m +++ b/FirebaseAppCheck/Sources/AppAttestProvider/API/FIRAppAttestAPIService.m @@ -24,6 +24,9 @@ #import "FirebaseAppCheck/Sources/Core/APIService/FIRAppCheckAPIService.h" +#import +#import "FirebaseAppCheck/Sources/Core/Errors/FIRAppCheckErrorUtil.h" + @interface FIRAppAttestAPIService () @property(nonatomic, readonly) id APIService; @@ -56,8 +59,68 @@ - (instancetype)initWithAPIService:(id)APIService } - (nonnull FBLPromise *)getRandomChallenge { - // TODO: Implement. - return [FBLPromise resolvedWith:nil]; + NSString *URLString = + [NSString stringWithFormat:@"%@/projects/%@/apps/%@:generateAppAttestChallenge", + self.APIService.baseURL, self.projectID, self.appID]; + NSURL *URL = [NSURL URLWithString:URLString]; + + return [FBLPromise onQueue:[self defaultQueue] + do:^id _Nullable { + return [self.APIService sendRequestWithURL:URL + HTTPMethod:@"POST" + body:nil + additionalHeaders:nil]; + }] + .then(^id _Nullable(GULURLSessionDataResponse *_Nullable response) { + return [self randomChallengeWithAPIResponse:response]; + }); +} + +#pragma mark - Challenge response parsing + +- (FBLPromise *)randomChallengeWithAPIResponse:(GULURLSessionDataResponse *)response { + return [FBLPromise onQueue:[self defaultQueue] + do:^id _Nullable { + NSError *error; + + NSData *randomChallenge = + [self randomChallengeFromResponseBody:response.HTTPBody + error:&error]; + + return randomChallenge ?: error; + }]; +} + +- (NSData *)randomChallengeFromResponseBody:(NSData *)response error:(NSError **)outError { + if (response.length <= 0) { + FIRAppCheckSetErrorToPointer( + [FIRAppCheckErrorUtil errorWithFailureReason:@"Empty server response body."], outError); + return nil; + } + + NSError *JSONError; + NSDictionary *responseDict = [NSJSONSerialization JSONObjectWithData:response + options:0 + error:&JSONError]; + + if (![responseDict isKindOfClass:[NSDictionary class]]) { + FIRAppCheckSetErrorToPointer([FIRAppCheckErrorUtil JSONSerializationError:JSONError], outError); + return nil; + } + + NSString *challenge = responseDict[@"challenge"]; + if (![challenge isKindOfClass:[NSString class]]) { + FIRAppCheckSetErrorToPointer( + [FIRAppCheckErrorUtil appCheckTokenResponseErrorWithMissingField:@"challenge"], outError); + return nil; + } + + NSData *randomChallenge = [[NSData alloc] initWithBase64EncodedString:challenge options:0]; + return randomChallenge; +} + +- (dispatch_queue_t)defaultQueue { + return dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0); } @end diff --git a/FirebaseAppCheck/Sources/AppAttestProvider/FIRAppAttestProvider.m b/FirebaseAppCheck/Sources/AppAttestProvider/FIRAppAttestProvider.m index 9a1346f9829..9526dfa9085 100644 --- a/FirebaseAppCheck/Sources/AppAttestProvider/FIRAppAttestProvider.m +++ b/FirebaseAppCheck/Sources/AppAttestProvider/FIRAppAttestProvider.m @@ -278,6 +278,7 @@ - (void)getTokenWithCompletion:(void (^)(FIRAppCheckToken *_Nullable, NSError *_ challenge:(NSData *)challenge { return [FBLPromise onQueue:self.queue do:^id _Nullable { + // TODO: Hash challenge. return [challenge base64EncodedDataWithOptions:0]; }] .thenOn( diff --git a/FirebaseAppCheck/Sources/Core/APIService/FIRAppCheckAPIService.h b/FirebaseAppCheck/Sources/Core/APIService/FIRAppCheckAPIService.h index 53be0740e56..c8446cd1723 100644 --- a/FirebaseAppCheck/Sources/Core/APIService/FIRAppCheckAPIService.h +++ b/FirebaseAppCheck/Sources/Core/APIService/FIRAppCheckAPIService.h @@ -29,7 +29,7 @@ NS_ASSUME_NONNULL_BEGIN - (FBLPromise *) sendRequestWithURL:(NSURL *)requestURL HTTPMethod:(NSString *)HTTPMethod - body:(NSData *)body + body:(nullable NSData *)body additionalHeaders:(nullable NSDictionary *)additionalHeaders; - (FBLPromise *)appCheckTokenWithAPIResponse: diff --git a/FirebaseAppCheck/Sources/Core/APIService/FIRAppCheckAPIService.m b/FirebaseAppCheck/Sources/Core/APIService/FIRAppCheckAPIService.m index 3e349d74b58..a5bd36c5579 100644 --- a/FirebaseAppCheck/Sources/Core/APIService/FIRAppCheckAPIService.m +++ b/FirebaseAppCheck/Sources/Core/APIService/FIRAppCheckAPIService.m @@ -83,7 +83,7 @@ - (instancetype)initWithURLSession:(NSURLSession *)session - (FBLPromise *) sendRequestWithURL:(NSURL *)requestURL HTTPMethod:(NSString *)HTTPMethod - body:(NSData *)body + body:(nullable NSData *)body additionalHeaders:(nullable NSDictionary *)additionalHeaders { return [self requestWithURL:requestURL HTTPMethod:HTTPMethod @@ -103,7 +103,7 @@ - (instancetype)initWithURLSession:(NSURLSession *)session additionalHeaders:(nullable NSDictionary *) additionalHeaders { return [FBLPromise - onQueue:dispatch_get_global_queue(QOS_CLASS_UTILITY, 0) + onQueue:[self defaultQueue] do:^id _Nullable { __block NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:requestURL]; request.HTTPMethod = HTTPMethod; @@ -155,7 +155,7 @@ - (instancetype)initWithURLSession:(NSURLSession *)session - (FBLPromise *)appCheckTokenWithAPIResponse: (GULURLSessionDataResponse *)response { - return [FBLPromise onQueue:dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0) + return [FBLPromise onQueue:[self defaultQueue] do:^id _Nullable { NSError *error; @@ -167,6 +167,10 @@ - (instancetype)initWithURLSession:(NSURLSession *)session }]; } +- (dispatch_queue_t)defaultQueue { + return dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0); +} + @end NS_ASSUME_NONNULL_END diff --git a/FirebaseAppCheck/Sources/DebugProvider/API/FIRAppCheckDebugProviderAPIService.m b/FirebaseAppCheck/Sources/DebugProvider/API/FIRAppCheckDebugProviderAPIService.m index 9b702116ab7..3841b4324d1 100644 --- a/FirebaseAppCheck/Sources/DebugProvider/API/FIRAppCheckDebugProviderAPIService.m +++ b/FirebaseAppCheck/Sources/DebugProvider/API/FIRAppCheckDebugProviderAPIService.m @@ -82,7 +82,7 @@ - (instancetype)initWithAPIService:(id)APIService }); } -#pragma mark - +#pragma mark - Helpers - (FBLPromise *)HTTPBodyWithDebugToken:(NSString *)debugToken { if (debugToken.length <= 0) { diff --git a/FirebaseAppCheck/Tests/Fixture/AppAttestResponseMissingChallenge.json b/FirebaseAppCheck/Tests/Fixture/AppAttestResponseMissingChallenge.json new file mode 100644 index 00000000000..c75cd2bab33 --- /dev/null +++ b/FirebaseAppCheck/Tests/Fixture/AppAttestResponseMissingChallenge.json @@ -0,0 +1,3 @@ +{ + "ttl": "300s" +} diff --git a/FirebaseAppCheck/Tests/Fixture/AppAttestResponseSuccess.json b/FirebaseAppCheck/Tests/Fixture/AppAttestResponseSuccess.json new file mode 100644 index 00000000000..fb834e9128b --- /dev/null +++ b/FirebaseAppCheck/Tests/Fixture/AppAttestResponseSuccess.json @@ -0,0 +1,4 @@ +{ + "challenge": "cmFuZG9tX2NoYWxsZW5nZQ==", + "ttl": "300s" +} diff --git a/FirebaseAppCheck/Tests/Unit/AppAttestProvider/FIRAppAttestAPIServiceTests.m b/FirebaseAppCheck/Tests/Unit/AppAttestProvider/FIRAppAttestAPIServiceTests.m new file mode 100644 index 00000000000..e5d54eb4374 --- /dev/null +++ b/FirebaseAppCheck/Tests/Unit/AppAttestProvider/FIRAppAttestAPIServiceTests.m @@ -0,0 +1,233 @@ +/* + * Copyright 2021 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 + +#import "FBLPromise+Testing.h" +#import "OCMock.h" + +#import + +#import "FirebaseAppCheck/Sources/AppAttestProvider/API/FIRAppAttestAPIService.h" +#import "FirebaseAppCheck/Sources/Core/APIService/FIRAppCheckAPIService.h" +#import "FirebaseAppCheck/Sources/Core/Errors/FIRAppCheckErrorUtil.h" + +#import "FirebaseAppCheck/Tests/Unit/Utils/FIRFixtureLoader.h" +#import "SharedTestUtilities/URLSession/FIRURLSessionOCMockStub.h" + +@interface FIRAppAttestAPIServiceTests : XCTestCase + +@property(nonatomic) FIRAppAttestAPIService *appAttestAPIService; + +@property(nonatomic) id mockAPIService; + +@property(nonatomic) NSString *projectID; +@property(nonatomic) NSString *appID; + +@end + +@implementation FIRAppAttestAPIServiceTests + +- (void)setUp { + [super setUp]; + + self.projectID = @"project_id"; + self.appID = @"app_id"; + + self.mockAPIService = OCMProtocolMock(@protocol(FIRAppCheckAPIServiceProtocol)); + OCMStub([self.mockAPIService baseURL]).andReturn(@"https://test.appcheck.url.com/beta"); + + self.appAttestAPIService = [[FIRAppAttestAPIService alloc] initWithAPIService:self.mockAPIService + projectID:self.projectID + appID:self.appID]; +} + +- (void)tearDown { + [super tearDown]; + + self.appAttestAPIService = nil; + [self.mockAPIService stopMocking]; + self.mockAPIService = nil; +} + +- (void)testGetRandomChallengeWhenAPIResponseValid { + // 1. Prepare API response. + NSData *responseBody = [FIRFixtureLoader loadFixtureNamed:@"AppAttestResponseSuccess.json"]; + GULURLSessionDataResponse *validAPIResponse = [self APIResponseWithCode:200 + responseBody:responseBody]; + // 2. Stub API Service Request to return prepared API response. + [self stubMockAPIServiceRequestWithResponse:validAPIResponse]; + + // 3. Request the random challenge and verify results. + __auto_type *promise = [self.appAttestAPIService getRandomChallenge]; + XCTAssert(FBLWaitForPromisesWithTimeout(1)); + XCTAssert(promise.isFulfilled); + XCTAssertNotNil(promise.value); + XCTAssertNil(promise.error); + + NSString *challengeString = [[NSString alloc] initWithData:promise.value + encoding:NSUTF8StringEncoding]; + // The challenge stored in `AppAttestResponseSuccess.json` is a valid base64 encoding of + // the string "random_challenge". + XCTAssert([challengeString isEqualToString:@"random_challenge"]); + + OCMVerifyAll(self.mockAPIService); +} + +- (void)testGetRandomChallengeWhenAPIError { + // 1. Prepare API response. + NSString *responseBodyString = @"Generate challenge failed with invalid format."; + NSData *responseBody = [responseBodyString dataUsingEncoding:NSUTF8StringEncoding]; + GULURLSessionDataResponse *invalidAPIResponse = [self APIResponseWithCode:300 + responseBody:responseBody]; + NSError *APIError = [FIRAppCheckErrorUtil APIErrorWithHTTPResponse:invalidAPIResponse.HTTPResponse + data:invalidAPIResponse.HTTPBody]; + // 2. Stub API Service Request to return prepared API response. + [self stubMockAPIServiceRequestWithResponse:APIError]; + + // 3. Request the random challenge and verify results. + __auto_type *promise = [self.appAttestAPIService getRandomChallenge]; + XCTAssert(FBLWaitForPromisesWithTimeout(1)); + XCTAssert(promise.isRejected); + XCTAssertNotNil(promise.error); + XCTAssertNil(promise.value); + + // Assert error is as expected. + XCTAssertEqualObjects(promise.error.domain, kFIRAppCheckErrorDomain); + XCTAssertEqual(promise.error.code, FIRAppCheckErrorCodeUnknown); + + // Expect response body and HTTP status code to be included in the error. + NSString *failureReason = promise.error.userInfo[NSLocalizedFailureReasonErrorKey]; + XCTAssertTrue([failureReason containsString:@"300"]); + XCTAssertTrue([failureReason containsString:responseBodyString]); + + OCMVerifyAll(self.mockAPIService); +} + +- (void)testGetRandomChallengeWhenAPIResponseEmpty { + // 1. Prepare API response. + NSData *responseBody = [NSData data]; + GULURLSessionDataResponse *emptyAPIResponse = [self APIResponseWithCode:200 + responseBody:responseBody]; + // 2. Stub API Service Request to return prepared API response. + [self stubMockAPIServiceRequestWithResponse:emptyAPIResponse]; + + // 3. Request the random challenge and verify results. + __auto_type *promise = [self.appAttestAPIService getRandomChallenge]; + XCTAssert(FBLWaitForPromisesWithTimeout(1)); + XCTAssert(promise.isRejected); + XCTAssertNotNil(promise.error); + XCTAssertNil(promise.value); + + // Expect response body and HTTP status code to be included in the error. + NSString *failureReason = promise.error.userInfo[NSLocalizedFailureReasonErrorKey]; + XCTAssertEqualObjects(failureReason, @"Empty server response body."); + + OCMVerifyAll(self.mockAPIService); +} + +- (void)testGetRandomChallengeWhenAPIResponseInvalidFormat { + // 1. Prepare API response. + NSString *responseBodyString = @"Generate challenge failed with invalid format."; + NSData *responseBody = [responseBodyString dataUsingEncoding:NSUTF8StringEncoding]; + GULURLSessionDataResponse *validAPIResponse = [self APIResponseWithCode:200 + responseBody:responseBody]; + // 2. Stub API Service Request to return prepared API response. + [self stubMockAPIServiceRequestWithResponse:validAPIResponse]; + + // 3. Request the random challenge and verify results. + __auto_type *promise = [self.appAttestAPIService getRandomChallenge]; + XCTAssert(FBLWaitForPromisesWithTimeout(1)); + XCTAssert(promise.isRejected); + XCTAssertNotNil(promise.error); + XCTAssertNil(promise.value); + + // Expect response body and HTTP status code to be included in the error. + NSString *failureReason = promise.error.userInfo[NSLocalizedFailureReasonErrorKey]; + XCTAssertEqualObjects(failureReason, @"JSON serialization error."); + + OCMVerifyAll(self.mockAPIService); +} + +- (void)testGetRandomChallengeWhenResponseMissingField { + [self assertMissingFieldErrorWithFixture:@"AppAttestResponseMissingChallenge.json" + missingField:@"challenge"]; +} + +- (void)assertMissingFieldErrorWithFixture:(NSString *)fixtureName + missingField:(NSString *)fieldName { + // 1. Prepare API response. + NSData *missingFieldBody = [FIRFixtureLoader loadFixtureNamed:fixtureName]; + GULURLSessionDataResponse *incompleteAPIResponse = [self APIResponseWithCode:200 + responseBody:missingFieldBody]; + // 2. Stub API Service Request to return prepared API response. + [self stubMockAPIServiceRequestWithResponse:incompleteAPIResponse]; + + // 3. Request the random challenge and verify results. + __auto_type *promise = [self.appAttestAPIService getRandomChallenge]; + XCTAssert(FBLWaitForPromisesWithTimeout(1)); + XCTAssert(promise.isRejected); + XCTAssertNotNil(promise.error); + XCTAssertNil(promise.value); + + // Assert error is as expected. + XCTAssertEqualObjects(promise.error.domain, kFIRAppCheckErrorDomain); + XCTAssertEqual(promise.error.code, FIRAppCheckErrorCodeUnknown); + + // Expect missing field name to be included in the error. + NSString *failureReason = promise.error.userInfo[NSLocalizedFailureReasonErrorKey]; + NSString *fieldNameString = [NSString stringWithFormat:@"`%@`", fieldName]; + XCTAssertTrue([failureReason containsString:fieldNameString], + @"Fixture `%@`: expected missing field %@ error not found", fixtureName, + fieldNameString); +} + +#pragma mark - Helpers + +- (GULURLSessionDataResponse *)APIResponseWithCode:(NSInteger)code + responseBody:(NSData *)responseBody { + XCTAssertNotNil(responseBody); + NSHTTPURLResponse *HTTPResponse = [FIRURLSessionOCMockStub HTTPResponseWithCode:code]; + GULURLSessionDataResponse *APIResponse = + [[GULURLSessionDataResponse alloc] initWithResponse:HTTPResponse HTTPBody:responseBody]; + return APIResponse; +} + +- (void)stubMockAPIServiceRequestWithResponse:(id)response { + id URLValidationArg = [self URLValidationCheckBlock]; + OCMStub([self.mockAPIService sendRequestWithURL:URLValidationArg + HTTPMethod:@"POST" + body:nil + additionalHeaders:nil]) + .andDo(^(NSInvocation *invocation) { + XCTAssertFalse([NSThread isMainThread]); + }) + .andReturn([FBLPromise resolvedWith:response]); +} + +- (id)URLValidationCheckBlock { + NSString *expectedRequestURL = + [NSString stringWithFormat:@"%@/projects/%@/apps/%@:generateAppAttestChallenge", + [self.mockAPIService baseURL], self.projectID, self.appID]; + + id URLValidationArg = [OCMArg checkWithBlock:^BOOL(NSURL *URL) { + XCTAssertEqualObjects(URL.absoluteString, expectedRequestURL); + return YES; + }]; + return URLValidationArg; +} + +@end From ed07e4b5d692fa733dd0a06c950a49cdc6071e26 Mon Sep 17 00:00:00 2001 From: Nick Cooke <36927374+ncooke3@users.noreply.github.com> Date: Tue, 11 May 2021 15:56:26 -0400 Subject: [PATCH 06/37] Define Exchange AppAttest Assertion for FAC token API (#8058) --- .../Sources/AppAttestProvider/API/FIRAppAttestAPIService.h | 5 +++++ .../Sources/AppAttestProvider/API/FIRAppAttestAPIService.m | 7 +++++++ 2 files changed, 12 insertions(+) diff --git a/FirebaseAppCheck/Sources/AppAttestProvider/API/FIRAppAttestAPIService.h b/FirebaseAppCheck/Sources/AppAttestProvider/API/FIRAppAttestAPIService.h index 860b2224d0f..b3f913ffe8d 100644 --- a/FirebaseAppCheck/Sources/AppAttestProvider/API/FIRAppAttestAPIService.h +++ b/FirebaseAppCheck/Sources/AppAttestProvider/API/FIRAppAttestAPIService.h @@ -32,6 +32,11 @@ NS_ASSUME_NONNULL_BEGIN keyID:(NSString *)keyID challenge:(NSData *)challenge; +/// Exchanges attestation data (artifact & assertion) and a challenge for a FAC token. +- (FBLPromise *)appCheckTokenWithArtifact:(NSData *)artifact + challenge:(NSData *)challenge + assertion:(NSData *)assertion; + @end @interface FIRAppAttestAPIService : NSObject diff --git a/FirebaseAppCheck/Sources/AppAttestProvider/API/FIRAppAttestAPIService.m b/FirebaseAppCheck/Sources/AppAttestProvider/API/FIRAppAttestAPIService.m index b8ed562213f..304ca7a4217 100644 --- a/FirebaseAppCheck/Sources/AppAttestProvider/API/FIRAppAttestAPIService.m +++ b/FirebaseAppCheck/Sources/AppAttestProvider/API/FIRAppAttestAPIService.m @@ -50,6 +50,13 @@ - (instancetype)initWithAPIService:(id)APIService return self; } +- (FBLPromise *)appCheckTokenWithArtifact:(NSData *)artifact + challenge:(NSData *)challenge + assertion:(NSData *)assertion { + // TODO: Implement. + return [FBLPromise resolvedWith:nil]; +} + - (nonnull FBLPromise *) appCheckTokenWithAttestation:(nonnull NSData *)attestation keyID:(nonnull NSString *)keyID From 786f613be0f98ee9335ea2a7dba1c08ea33c3051 Mon Sep 17 00:00:00 2001 From: Maksym Malyhin Date: Wed, 12 May 2021 09:34:03 -0400 Subject: [PATCH 07/37] App Check App Attest: attestation request (#8059) * App Attest provider API integration WIP * update tests * Draft attestation response parsing * Attestation request draft * style * AppAttest Attestation API tests draft * Error cases tests * style * Cleanup and API docs * Merge fix * Fix OCMock imports * Fix nullability modifier * Formatting * comments --- .../API/FIRAppAttestAPIService.h | 23 ++- .../API/FIRAppAttestAPIService.m | 94 +++++++-- ...se.h => FIRAppAttestAttestationResponse.h} | 7 +- .../API/FIRAppAttestAttestationResponse.m | 92 +++++++++ .../FIRAppAttestInitialHandshakeResponse.m | 30 --- .../AppAttestProvider/FIRAppAttestProvider.m | 23 ++- .../APIService/FIRAppCheckToken+APIResponse.h | 4 + .../APIService/FIRAppCheckToken+APIResponse.m | 6 + .../Core/Errors/FIRAppCheckErrorUtil.h | 2 + .../Core/Errors/FIRAppCheckErrorUtil.m | 9 + .../AppAttestAttestationResponseSuccess.json | 7 + .../FIRAppAttestAPIServiceTests.m | 190 +++++++++++++++++- .../FIRAppAttestProviderTests.m | 71 ++++--- 13 files changed, 468 insertions(+), 90 deletions(-) rename FirebaseAppCheck/Sources/AppAttestProvider/API/{FIRAppAttestInitialHandshakeResponse.h => FIRAppAttestAttestationResponse.h} (79%) create mode 100644 FirebaseAppCheck/Sources/AppAttestProvider/API/FIRAppAttestAttestationResponse.m delete mode 100644 FirebaseAppCheck/Sources/AppAttestProvider/API/FIRAppAttestInitialHandshakeResponse.m create mode 100644 FirebaseAppCheck/Tests/Fixture/AppAttestAttestationResponseSuccess.json diff --git a/FirebaseAppCheck/Sources/AppAttestProvider/API/FIRAppAttestAPIService.h b/FirebaseAppCheck/Sources/AppAttestProvider/API/FIRAppAttestAPIService.h index b3f913ffe8d..568ee4e4791 100644 --- a/FirebaseAppCheck/Sources/AppAttestProvider/API/FIRAppAttestAPIService.h +++ b/FirebaseAppCheck/Sources/AppAttestProvider/API/FIRAppAttestAPIService.h @@ -17,20 +17,29 @@ #import @class FBLPromise; +@class FIRAppAttestAttestationResponse; @class FIRAppCheckToken; @protocol FIRAppCheckAPIServiceProtocol; NS_ASSUME_NONNULL_BEGIN +/// Methods to send API requests required for App Attest based attestation sequence. @protocol FIRAppAttestAPIServiceProtocol /// Request a random challenge from server. - (FBLPromise *)getRandomChallenge; -/// Exchanges attestation data to FAC token. -- (FBLPromise *)appCheckTokenWithAttestation:(NSData *)attestation - keyID:(NSString *)keyID - challenge:(NSData *)challenge; +/// Sends attestation data to Firebase backend for validation. +/// @param attestation The App Attest key attestation data obtained from the method +/// `-[DCAppAttestService attestKey:clientDataHash:completionHandler:]` using the random challenge +/// received from Firebase backend. +/// @param keyID The key ID used to generate the attestation. +/// @param challenge The challenge used to generate the attestation. +/// @return A promise that is fulfilled with a response object with an encrypted attestation +/// artifact and an Firebase App Check token or rejected with an error. +- (FBLPromise *)attestKeyWithAttestation:(NSData *)attestation + keyID:(NSString *)keyID + challenge:(NSData *)challenge; /// Exchanges attestation data (artifact & assertion) and a challenge for a FAC token. - (FBLPromise *)appCheckTokenWithArtifact:(NSData *)artifact @@ -39,8 +48,14 @@ NS_ASSUME_NONNULL_BEGIN @end +/// A default implementation of `FIRAppAttestAPIServiceProtocol`. @interface FIRAppAttestAPIService : NSObject +/// Default initializer. +/// @param APIService An instance implementing `FIRAppCheckAPIServiceProtocol` to be used to send +/// network requests to Firebase App Check backend. +/// @param projectID A Firebase project ID for the requests (`FIRApp.options.projectID`). +/// @param appID A Firebase app ID for the requests (`FIRApp.options.googleAppID`). - (instancetype)initWithAPIService:(id)APIService projectID:(NSString *)projectID appID:(NSString *)appID; diff --git a/FirebaseAppCheck/Sources/AppAttestProvider/API/FIRAppAttestAPIService.m b/FirebaseAppCheck/Sources/AppAttestProvider/API/FIRAppAttestAPIService.m index 304ca7a4217..861d75177f0 100644 --- a/FirebaseAppCheck/Sources/AppAttestProvider/API/FIRAppAttestAPIService.m +++ b/FirebaseAppCheck/Sources/AppAttestProvider/API/FIRAppAttestAPIService.m @@ -22,11 +22,21 @@ #import "FBLPromises.h" #endif +#import "FirebaseAppCheck/Sources/AppAttestProvider/API/FIRAppAttestAttestationResponse.h" #import "FirebaseAppCheck/Sources/Core/APIService/FIRAppCheckAPIService.h" #import #import "FirebaseAppCheck/Sources/Core/Errors/FIRAppCheckErrorUtil.h" +NS_ASSUME_NONNULL_BEGIN + +static NSString *const kRequestFieldAttestation = @"attestation_statement"; +static NSString *const kRequestFieldKeyID = @"key_id"; +static NSString *const kRequestFieldChallenge = @"challenge"; + +static NSString *const kContentTypeKey = @"Content-Type"; +static NSString *const kJSONContentType = @"application/json"; + @interface FIRAppAttestAPIService () @property(nonatomic, readonly) id APIService; @@ -50,6 +60,10 @@ - (instancetype)initWithAPIService:(id)APIService return self; } +- (dispatch_queue_t)backgroundQueue { + return dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0); +} + - (FBLPromise *)appCheckTokenWithArtifact:(NSData *)artifact challenge:(NSData *)challenge assertion:(NSData *)assertion { @@ -57,13 +71,7 @@ - (instancetype)initWithAPIService:(id)APIService return [FBLPromise resolvedWith:nil]; } -- (nonnull FBLPromise *) - appCheckTokenWithAttestation:(nonnull NSData *)attestation - keyID:(nonnull NSString *)keyID - challenge:(nonnull NSData *)challenge { - // TODO: Implement. - return [FBLPromise resolvedWith:nil]; -} +#pragma mark - Random Challenge - (nonnull FBLPromise *)getRandomChallenge { NSString *URLString = @@ -71,7 +79,7 @@ - (instancetype)initWithAPIService:(id)APIService self.APIService.baseURL, self.projectID, self.appID]; NSURL *URL = [NSURL URLWithString:URLString]; - return [FBLPromise onQueue:[self defaultQueue] + return [FBLPromise onQueue:[self backgroundQueue] do:^id _Nullable { return [self.APIService sendRequestWithURL:URL HTTPMethod:@"POST" @@ -86,7 +94,7 @@ - (instancetype)initWithAPIService:(id)APIService #pragma mark - Challenge response parsing - (FBLPromise *)randomChallengeWithAPIResponse:(GULURLSessionDataResponse *)response { - return [FBLPromise onQueue:[self defaultQueue] + return [FBLPromise onQueue:[self backgroundQueue] do:^id _Nullable { NSError *error; @@ -98,7 +106,7 @@ - (instancetype)initWithAPIService:(id)APIService }]; } -- (NSData *)randomChallengeFromResponseBody:(NSData *)response error:(NSError **)outError { +- (nullable NSData *)randomChallengeFromResponseBody:(NSData *)response error:(NSError **)outError { if (response.length <= 0) { FIRAppCheckSetErrorToPointer( [FIRAppCheckErrorUtil errorWithFailureReason:@"Empty server response body."], outError); @@ -126,8 +134,70 @@ - (NSData *)randomChallengeFromResponseBody:(NSData *)response error:(NSError ** return randomChallenge; } -- (dispatch_queue_t)defaultQueue { - return dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0); +#pragma mark - Attestation request + +- (FBLPromise *)attestKeyWithAttestation:(NSData *)attestation + keyID:(NSString *)keyID + challenge:(NSData *)challenge { + NSString *URLString = + [NSString stringWithFormat:@"%@/projects/%@/apps/%@:exchangeAppAttestAttestation", + self.APIService.baseURL, self.projectID, self.appID]; + NSURL *URL = [NSURL URLWithString:URLString]; + + return [self HTTPBodyWithAttestation:attestation keyID:keyID challenge:challenge] + .then(^FBLPromise *(NSData *HTTPBody) { + return [self.APIService sendRequestWithURL:URL + HTTPMethod:@"POST" + body:HTTPBody + additionalHeaders:@{kContentTypeKey : kJSONContentType}]; + }) + .then(^id _Nullable(GULURLSessionDataResponse *_Nullable URLResponse) { + NSError *error; + + __auto_type response = + [[FIRAppAttestAttestationResponse alloc] initWithResponseData:URLResponse.HTTPBody + requestDate:[NSDate date] + error:&error]; + + return response ?: error; + }); +} + +- (FBLPromise *)HTTPBodyWithAttestation:(NSData *)attestation + keyID:(NSString *)keyID + challenge:(NSData *)challenge { + if (attestation.length <= 0 || keyID.length <= 0 || challenge.length <= 0) { + FBLPromise *rejectedPromise = [FBLPromise pendingPromise]; + [rejectedPromise reject:[FIRAppCheckErrorUtil + errorWithFailureReason:@"Missing or empty request parameter."]]; + return rejectedPromise; + } + + return [FBLPromise + onQueue:[self backgroundQueue] + do:^id _Nullable { + NSError *encodingError; + NSData *payloadJSON = [NSJSONSerialization dataWithJSONObject:@{ + kRequestFieldKeyID : keyID, + kRequestFieldAttestation : [self base64StringWithData:attestation], + kRequestFieldChallenge : [self base64StringWithData:challenge] + } + options:0 + error:&encodingError]; + + if (payloadJSON != nil) { + return payloadJSON; + } else { + return [FIRAppCheckErrorUtil JSONSerializationError:encodingError]; + } + }]; +} + +- (NSString *)base64StringWithData:(NSData *)data { + // TODO: Need to encode in base64URL? + return [data base64EncodedStringWithOptions:0]; } @end + +NS_ASSUME_NONNULL_END diff --git a/FirebaseAppCheck/Sources/AppAttestProvider/API/FIRAppAttestInitialHandshakeResponse.h b/FirebaseAppCheck/Sources/AppAttestProvider/API/FIRAppAttestAttestationResponse.h similarity index 79% rename from FirebaseAppCheck/Sources/AppAttestProvider/API/FIRAppAttestInitialHandshakeResponse.h rename to FirebaseAppCheck/Sources/AppAttestProvider/API/FIRAppAttestAttestationResponse.h index 448d15e8c30..c17088e0a6b 100644 --- a/FirebaseAppCheck/Sources/AppAttestProvider/API/FIRAppAttestInitialHandshakeResponse.h +++ b/FirebaseAppCheck/Sources/AppAttestProvider/API/FIRAppAttestAttestationResponse.h @@ -20,7 +20,7 @@ NS_ASSUME_NONNULL_BEGIN -@interface FIRAppAttestInitialHandshakeResponse : NSObject +@interface FIRAppAttestAttestationResponse : NSObject /// App Attest attestation artifact required to refresh Firebase App Check token. @property(nonatomic, readonly) NSData *artifact; @@ -33,6 +33,11 @@ NS_ASSUME_NONNULL_BEGIN - (instancetype)initWithArtifact:(NSData *)artifact token:(FIRAppCheckToken *)token NS_DESIGNATED_INITIALIZER; +/// Init with the server response. +- (nullable instancetype)initWithResponseData:(NSData *)response + requestDate:(NSDate *)requestDate + error:(NSError **)outError; + @end NS_ASSUME_NONNULL_END diff --git a/FirebaseAppCheck/Sources/AppAttestProvider/API/FIRAppAttestAttestationResponse.m b/FirebaseAppCheck/Sources/AppAttestProvider/API/FIRAppAttestAttestationResponse.m new file mode 100644 index 00000000000..3b564b2c216 --- /dev/null +++ b/FirebaseAppCheck/Sources/AppAttestProvider/API/FIRAppAttestAttestationResponse.m @@ -0,0 +1,92 @@ +/* + * Copyright 2021 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 "FirebaseAppCheck/Sources/AppAttestProvider/API/FIRAppAttestAttestationResponse.h" + +#import "FirebaseAppCheck/Sources/Core/APIService/FIRAppCheckToken+APIResponse.h" +#import "FirebaseAppCheck/Sources/Core/Errors/FIRAppCheckErrorUtil.h" + +@implementation FIRAppAttestAttestationResponse + +- (instancetype)initWithArtifact:(NSData *)artifact token:(FIRAppCheckToken *)token { + self = [super init]; + if (self) { + _artifact = artifact; + _token = token; + } + return self; +} + +- (nullable instancetype)initWithResponseData:(NSData *)response + requestDate:(NSDate *)requestDate + error:(NSError **)outError { + if (response.length <= 0) { + FIRAppCheckSetErrorToPointer( + [FIRAppCheckErrorUtil + errorWithFailureReason: + @"Failed to parse the initial handshake response. Empty server response body."], + outError); + return nil; + } + + NSError *JSONError; + NSDictionary *responseDict = [NSJSONSerialization JSONObjectWithData:response + options:0 + error:&JSONError]; + + if (![responseDict isKindOfClass:[NSDictionary class]]) { + FIRAppCheckSetErrorToPointer([FIRAppCheckErrorUtil JSONSerializationError:JSONError], outError); + return nil; + } + + NSString *artifactBase64String = responseDict[@"artifact"]; + if (![artifactBase64String isKindOfClass:[NSString class]]) { + FIRAppCheckSetErrorToPointer( + [FIRAppCheckErrorUtil appAttestAttestationResponseErrorWithMissingField:@"artifact"], + outError); + return nil; + } + NSData *artifactData = [[NSData alloc] initWithBase64EncodedString:artifactBase64String + options:0]; + if (artifactData == nil) { + FIRAppCheckSetErrorToPointer( + [FIRAppCheckErrorUtil appAttestAttestationResponseErrorWithMissingField:@"artifact"], + outError); + return nil; + } + + NSDictionary *attestationTokenDict = responseDict[@"attestationToken"]; + if (![attestationTokenDict isKindOfClass:[NSDictionary class]]) { + FIRAppCheckSetErrorToPointer( + [FIRAppCheckErrorUtil + appAttestAttestationResponseErrorWithMissingField:@"attestationToken"], + outError); + return nil; + } + + FIRAppCheckToken *appCheckToken = + [[FIRAppCheckToken alloc] initWithResponseDict:attestationTokenDict + requestDate:requestDate + error:outError]; + + if (appCheckToken == nil) { + return nil; + } + + return [self initWithArtifact:artifactData token:appCheckToken]; +} + +@end diff --git a/FirebaseAppCheck/Sources/AppAttestProvider/API/FIRAppAttestInitialHandshakeResponse.m b/FirebaseAppCheck/Sources/AppAttestProvider/API/FIRAppAttestInitialHandshakeResponse.m deleted file mode 100644 index b217632979c..00000000000 --- a/FirebaseAppCheck/Sources/AppAttestProvider/API/FIRAppAttestInitialHandshakeResponse.m +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright 2021 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 "FirebaseAppCheck/Sources/AppAttestProvider/API/FIRAppAttestInitialHandshakeResponse.h" - -@implementation FIRAppAttestInitialHandshakeResponse - -- (instancetype)initWithArtifact:(NSData *)artifact token:(FIRAppCheckToken *)token { - self = [super init]; - if (self) { - _artifact = artifact; - _token = token; - } - return self; -} - -@end diff --git a/FirebaseAppCheck/Sources/AppAttestProvider/FIRAppAttestProvider.m b/FirebaseAppCheck/Sources/AppAttestProvider/FIRAppAttestProvider.m index 9526dfa9085..2a07e31737b 100644 --- a/FirebaseAppCheck/Sources/AppAttestProvider/FIRAppAttestProvider.m +++ b/FirebaseAppCheck/Sources/AppAttestProvider/FIRAppAttestProvider.m @@ -25,6 +25,7 @@ #endif #import "FirebaseAppCheck/Sources/AppAttestProvider/API/FIRAppAttestAPIService.h" +#import "FirebaseAppCheck/Sources/AppAttestProvider/API/FIRAppAttestAttestationResponse.h" #import "FirebaseAppCheck/Sources/AppAttestProvider/FIRAppAttestProviderState.h" #import "FirebaseAppCheck/Sources/AppAttestProvider/FIRAppAttestService.h" #import "FirebaseAppCheck/Sources/AppAttestProvider/Storage/FIRAppAttestArtifactStorage.h" @@ -184,14 +185,28 @@ - (void)getTokenWithCompletion:(void (^)(FIRAppCheckToken *_Nullable, NSError *_ }) // TODO: Handle a possible key rejection - generate another key. .thenOn(self.queue, - ^FBLPromise *(FIRAppAttestKeyAttestationResult *result) { + ^FBLPromise *( + FIRAppAttestKeyAttestationResult *result) { // 3. Exchange the attestation to FAC token. - return [self.APIService appCheckTokenWithAttestation:result.attestation - keyID:result.keyID - challenge:result.challenge]; + return [self.APIService attestKeyWithAttestation:result.attestation + keyID:result.keyID + challenge:result.challenge]; + }) + .thenOn(self.queue, + ^FBLPromise *(FIRAppAttestAttestationResponse *response) { + // 4. Save the artifact and return the received FAC token. + return [self saveArtifactAndGetAppCheckTokenFromResponse:response]; }); } +- (FBLPromise *)saveArtifactAndGetAppCheckTokenFromResponse: + (FIRAppAttestAttestationResponse *)response { + return [self.artifactStorage setArtifact:response.artifact].thenOn( + self.queue, ^FIRAppCheckToken *(id result) { + return response.token; + }); +} + #pragma mark - Token refresh sequence - (FBLPromise *)refreshTokenWithKeyID:(NSString *)keyID diff --git a/FirebaseAppCheck/Sources/Core/APIService/FIRAppCheckToken+APIResponse.h b/FirebaseAppCheck/Sources/Core/APIService/FIRAppCheckToken+APIResponse.h index b6fa24a6684..bbea948eeab 100644 --- a/FirebaseAppCheck/Sources/Core/APIService/FIRAppCheckToken+APIResponse.h +++ b/FirebaseAppCheck/Sources/Core/APIService/FIRAppCheckToken+APIResponse.h @@ -27,6 +27,10 @@ NS_ASSUME_NONNULL_BEGIN requestDate:(NSDate *)requestDate error:(NSError **)outError; +- (nullable instancetype)initWithResponseDict:(NSDictionary *)responseDict + requestDate:(NSDate *)requestDate + error:(NSError **)outError; + @end NS_ASSUME_NONNULL_END diff --git a/FirebaseAppCheck/Sources/Core/APIService/FIRAppCheckToken+APIResponse.m b/FirebaseAppCheck/Sources/Core/APIService/FIRAppCheckToken+APIResponse.m index 28a81f4951b..3fc188144f5 100644 --- a/FirebaseAppCheck/Sources/Core/APIService/FIRAppCheckToken+APIResponse.m +++ b/FirebaseAppCheck/Sources/Core/APIService/FIRAppCheckToken+APIResponse.m @@ -47,6 +47,12 @@ - (nullable instancetype)initWithTokenExchangeResponse:(NSData *)response return nil; } + return [self initWithResponseDict:responseDict requestDate:requestDate error:outError]; +} + +- (nullable instancetype)initWithResponseDict:(NSDictionary *)responseDict + requestDate:(NSDate *)requestDate + error:(NSError **)outError { NSString *token = responseDict[@"attestationToken"]; if (![token isKindOfClass:[NSString class]]) { FIRAppCheckSetErrorToPointer( diff --git a/FirebaseAppCheck/Sources/Core/Errors/FIRAppCheckErrorUtil.h b/FirebaseAppCheck/Sources/Core/Errors/FIRAppCheckErrorUtil.h index 077d3387b28..5e7fbc6ef3b 100644 --- a/FirebaseAppCheck/Sources/Core/Errors/FIRAppCheckErrorUtil.h +++ b/FirebaseAppCheck/Sources/Core/Errors/FIRAppCheckErrorUtil.h @@ -36,6 +36,8 @@ void FIRAppCheckSetErrorToPointer(NSError *error, NSError **pointer); + (NSError *)appCheckTokenResponseErrorWithMissingField:(NSString *)fieldName; ++ (NSError *)appAttestAttestationResponseErrorWithMissingField:(NSString *)fieldName; + + (NSError *)JSONSerializationError:(NSError *)error; + (NSError *)errorWithFailureReason:(NSString *)failureReason; diff --git a/FirebaseAppCheck/Sources/Core/Errors/FIRAppCheckErrorUtil.m b/FirebaseAppCheck/Sources/Core/Errors/FIRAppCheckErrorUtil.m index 93f62e22558..755b4d187bc 100644 --- a/FirebaseAppCheck/Sources/Core/Errors/FIRAppCheckErrorUtil.m +++ b/FirebaseAppCheck/Sources/Core/Errors/FIRAppCheckErrorUtil.m @@ -61,6 +61,15 @@ + (NSError *)appCheckTokenResponseErrorWithMissingField:(NSString *)fieldName { underlyingError:nil]; } ++ (NSError *)appAttestAttestationResponseErrorWithMissingField:(NSString *)fieldName { + NSString *failureReason = + [NSString stringWithFormat:@"Unexpected attestation response format. Field `%@` is missing.", + fieldName]; + return [self appCheckErrorWithCode:FIRAppCheckErrorCodeUnknown + failureReason:failureReason + underlyingError:nil]; +} + + (NSError *)JSONSerializationError:(NSError *)error { NSString *failureReason = [NSString stringWithFormat:@"JSON serialization error."]; return [self appCheckErrorWithCode:FIRAppCheckErrorCodeUnknown diff --git a/FirebaseAppCheck/Tests/Fixture/AppAttestAttestationResponseSuccess.json b/FirebaseAppCheck/Tests/Fixture/AppAttestAttestationResponseSuccess.json new file mode 100644 index 00000000000..5fcf918c9ee --- /dev/null +++ b/FirebaseAppCheck/Tests/Fixture/AppAttestAttestationResponseSuccess.json @@ -0,0 +1,7 @@ +{ + "artifact" : "dmFsaWQgRmlyZWJhc2UgYXBwIGF0dGVzdCBhcnRpZmFjdA==", + "attestationToken" : { + "attestationToken": "valid_app_check_token", + "ttl": "1800s" + } +} \ No newline at end of file diff --git a/FirebaseAppCheck/Tests/Unit/AppAttestProvider/FIRAppAttestAPIServiceTests.m b/FirebaseAppCheck/Tests/Unit/AppAttestProvider/FIRAppAttestAPIServiceTests.m index e5d54eb4374..7c0e8f463ea 100644 --- a/FirebaseAppCheck/Tests/Unit/AppAttestProvider/FIRAppAttestAPIServiceTests.m +++ b/FirebaseAppCheck/Tests/Unit/AppAttestProvider/FIRAppAttestAPIServiceTests.m @@ -16,16 +16,19 @@ #import +#import #import "FBLPromise+Testing.h" -#import "OCMock.h" #import #import "FirebaseAppCheck/Sources/AppAttestProvider/API/FIRAppAttestAPIService.h" +#import "FirebaseAppCheck/Sources/AppAttestProvider/API/FIRAppAttestAttestationResponse.h" #import "FirebaseAppCheck/Sources/Core/APIService/FIRAppCheckAPIService.h" #import "FirebaseAppCheck/Sources/Core/Errors/FIRAppCheckErrorUtil.h" +#import "FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRAppCheckToken.h" #import "FirebaseAppCheck/Tests/Unit/Utils/FIRFixtureLoader.h" +#import "SharedTestUtilities/Date/FIRDateTestUtils.h" #import "SharedTestUtilities/URLSession/FIRURLSessionOCMockStub.h" @interface FIRAppAttestAPIServiceTests : XCTestCase @@ -63,13 +66,15 @@ - (void)tearDown { self.mockAPIService = nil; } +#pragma mark - Random challenge request + - (void)testGetRandomChallengeWhenAPIResponseValid { // 1. Prepare API response. NSData *responseBody = [FIRFixtureLoader loadFixtureNamed:@"AppAttestResponseSuccess.json"]; GULURLSessionDataResponse *validAPIResponse = [self APIResponseWithCode:200 responseBody:responseBody]; // 2. Stub API Service Request to return prepared API response. - [self stubMockAPIServiceRequestWithResponse:validAPIResponse]; + [self stubMockAPIServiceRequestForChallengeRequestWithResponse:validAPIResponse]; // 3. Request the random challenge and verify results. __auto_type *promise = [self.appAttestAPIService getRandomChallenge]; @@ -96,7 +101,7 @@ - (void)testGetRandomChallengeWhenAPIError { NSError *APIError = [FIRAppCheckErrorUtil APIErrorWithHTTPResponse:invalidAPIResponse.HTTPResponse data:invalidAPIResponse.HTTPBody]; // 2. Stub API Service Request to return prepared API response. - [self stubMockAPIServiceRequestWithResponse:APIError]; + [self stubMockAPIServiceRequestForChallengeRequestWithResponse:APIError]; // 3. Request the random challenge and verify results. __auto_type *promise = [self.appAttestAPIService getRandomChallenge]; @@ -123,7 +128,7 @@ - (void)testGetRandomChallengeWhenAPIResponseEmpty { GULURLSessionDataResponse *emptyAPIResponse = [self APIResponseWithCode:200 responseBody:responseBody]; // 2. Stub API Service Request to return prepared API response. - [self stubMockAPIServiceRequestWithResponse:emptyAPIResponse]; + [self stubMockAPIServiceRequestForChallengeRequestWithResponse:emptyAPIResponse]; // 3. Request the random challenge and verify results. __auto_type *promise = [self.appAttestAPIService getRandomChallenge]; @@ -146,7 +151,7 @@ - (void)testGetRandomChallengeWhenAPIResponseInvalidFormat { GULURLSessionDataResponse *validAPIResponse = [self APIResponseWithCode:200 responseBody:responseBody]; // 2. Stub API Service Request to return prepared API response. - [self stubMockAPIServiceRequestWithResponse:validAPIResponse]; + [self stubMockAPIServiceRequestForChallengeRequestWithResponse:validAPIResponse]; // 3. Request the random challenge and verify results. __auto_type *promise = [self.appAttestAPIService getRandomChallenge]; @@ -174,7 +179,7 @@ - (void)assertMissingFieldErrorWithFixture:(NSString *)fixtureName GULURLSessionDataResponse *incompleteAPIResponse = [self APIResponseWithCode:200 responseBody:missingFieldBody]; // 2. Stub API Service Request to return prepared API response. - [self stubMockAPIServiceRequestWithResponse:incompleteAPIResponse]; + [self stubMockAPIServiceRequestForChallengeRequestWithResponse:incompleteAPIResponse]; // 3. Request the random challenge and verify results. __auto_type *promise = [self.appAttestAPIService getRandomChallenge]; @@ -195,6 +200,112 @@ - (void)assertMissingFieldErrorWithFixture:(NSString *)fixtureName fieldNameString); } +#pragma mark - Attestation request + +- (void)testAttestKeySuccess { + NSData *attestation = [self generateRandomData]; + NSData *challenge = [self generateRandomData]; + NSString *keyID = [NSUUID UUID].UUIDString; + + // 1. Prepare response. + NSData *responseBody = + [FIRFixtureLoader loadFixtureNamed:@"AppAttestAttestationResponseSuccess.json"]; + GULURLSessionDataResponse *validAPIResponse = [self APIResponseWithCode:200 + responseBody:responseBody]; + + // 2. Stub API Service + // 2.1. Return prepared response. + [self expectAttestAPIRequestWithAttestation:attestation + keyID:keyID + challenge:challenge + response:validAPIResponse + error:nil]; + + // 3. Send request. + __auto_type promise = [self.appAttestAPIService attestKeyWithAttestation:attestation + keyID:keyID + challenge:challenge]; + + // 4. Verify. + XCTAssert(FBLWaitForPromisesWithTimeout(1)); + + XCTAssertTrue(promise.isFulfilled); + XCTAssertNil(promise.error); + + NSData *expectedArtifact = + [@"valid Firebase app attest artifact" dataUsingEncoding:NSUTF8StringEncoding]; + + XCTAssertEqualObjects(promise.value.artifact, expectedArtifact); + XCTAssertEqualObjects(promise.value.token.token, @"valid_app_check_token"); + XCTAssertTrue([FIRDateTestUtils isDate:promise.value.token.expirationDate + approximatelyEqualCurrentPlusTimeInterval:1800 + precision:10]); + + OCMVerifyAll(self.mockAPIService); +} + +- (void)testAttestKeyNetworkError { + NSData *attestation = [self generateRandomData]; + NSData *challenge = [self generateRandomData]; + NSString *keyID = [NSUUID UUID].UUIDString; + + // 1. Stub API Service + // 1.1. Return prepared response. + NSError *networkError = [NSError errorWithDomain:self.name code:0 userInfo:nil]; + [self expectAttestAPIRequestWithAttestation:attestation + keyID:keyID + challenge:challenge + response:nil + error:networkError]; + + // 2. Send request. + __auto_type promise = [self.appAttestAPIService attestKeyWithAttestation:attestation + keyID:keyID + challenge:challenge]; + + // 3. Verify. + XCTAssert(FBLWaitForPromisesWithTimeout(1)); + + XCTAssertTrue(promise.isRejected); + XCTAssertNil(promise.value); + XCTAssertEqualObjects(promise.error, networkError); + + OCMVerifyAll(self.mockAPIService); +} + +- (void)testAttestKeyUnexpectedResponse { + NSData *attestation = [self generateRandomData]; + NSData *challenge = [self generateRandomData]; + NSString *keyID = [NSUUID UUID].UUIDString; + + // 1. Prepare unexpected response. + NSData *responseBody = [FIRFixtureLoader loadFixtureNamed:@"DeviceCheckResponseSuccess.json"]; + GULURLSessionDataResponse *validAPIResponse = [self APIResponseWithCode:200 + responseBody:responseBody]; + + // 2. Stub API Service + // 2.1. Return prepared response. + [self expectAttestAPIRequestWithAttestation:attestation + keyID:keyID + challenge:challenge + response:validAPIResponse + error:nil]; + + // 3. Send request. + __auto_type promise = [self.appAttestAPIService attestKeyWithAttestation:attestation + keyID:keyID + challenge:challenge]; + + // 4. Verify. + XCTAssert(FBLWaitForPromisesWithTimeout(1)); + + XCTAssertTrue(promise.isRejected); + XCTAssertNil(promise.value); + XCTAssertNotNil(promise.error); + + OCMVerifyAll(self.mockAPIService); +} + #pragma mark - Helpers - (GULURLSessionDataResponse *)APIResponseWithCode:(NSInteger)code @@ -206,8 +317,8 @@ - (GULURLSessionDataResponse *)APIResponseWithCode:(NSInteger)code return APIResponse; } -- (void)stubMockAPIServiceRequestWithResponse:(id)response { - id URLValidationArg = [self URLValidationCheckBlock]; +- (void)stubMockAPIServiceRequestForChallengeRequestWithResponse:(id)response { + id URLValidationArg = [self URLValidationArgumentWithResource:@"generateAppAttestChallenge"]; OCMStub([self.mockAPIService sendRequestWithURL:URLValidationArg HTTPMethod:@"POST" body:nil @@ -218,10 +329,10 @@ - (void)stubMockAPIServiceRequestWithResponse:(id)response { .andReturn([FBLPromise resolvedWith:response]); } -- (id)URLValidationCheckBlock { +- (id)URLValidationArgumentWithResource:(NSString *)resource { NSString *expectedRequestURL = - [NSString stringWithFormat:@"%@/projects/%@/apps/%@:generateAppAttestChallenge", - [self.mockAPIService baseURL], self.projectID, self.appID]; + [NSString stringWithFormat:@"%@/projects/%@/apps/%@:%@", [self.mockAPIService baseURL], + self.projectID, self.appID, resource]; id URLValidationArg = [OCMArg checkWithBlock:^BOOL(NSURL *URL) { XCTAssertEqualObjects(URL.absoluteString, expectedRequestURL); @@ -230,4 +341,61 @@ - (id)URLValidationCheckBlock { return URLValidationArg; } +- (void)expectAttestAPIRequestWithAttestation:(NSData *)attestation + keyID:(NSString *)keyID + challenge:(NSData *)challenge + response:(nullable GULURLSessionDataResponse *)response + error:(nullable NSError *)error { + id URLValidationArg = [self URLValidationArgumentWithResource:@"exchangeAppAttestAttestation"]; + + id bodyValidationArg = [OCMArg checkWithBlock:^BOOL(NSData *requestBody) { + NSDictionary *decodedData = [NSJSONSerialization JSONObjectWithData:requestBody + options:0 + error:nil]; + + XCTAssert([decodedData isKindOfClass:[NSDictionary class]]); + + // Validate attestation field. + NSString *base64EncodedAttestation = decodedData[@"attestation_statement"]; + XCTAssert([base64EncodedAttestation isKindOfClass:[NSString class]]); + + NSData *decodedAttestation = + [[NSData alloc] initWithBase64EncodedString:base64EncodedAttestation options:0]; + XCTAssertEqualObjects(decodedAttestation, attestation); + + // Validate challenge field. + NSString *base64EncodedChallenge = decodedData[@"challenge"]; + XCTAssert([base64EncodedAttestation isKindOfClass:[NSString class]]); + + NSData *decodedChallenge = [[NSData alloc] initWithBase64EncodedString:base64EncodedChallenge + options:0]; + XCTAssertEqualObjects(decodedChallenge, challenge); + + // Validate key ID field. + NSString *keyIDField = decodedData[@"key_id"]; + XCTAssert([base64EncodedAttestation isKindOfClass:[NSString class]]); + + XCTAssertEqualObjects(keyIDField, keyID); + + return YES; + }]; + + FBLPromise *resultPromise = [FBLPromise pendingPromise]; + if (error) { + [resultPromise reject:error]; + } else { + [resultPromise fulfill:response]; + } + + OCMExpect([self.mockAPIService sendRequestWithURL:URLValidationArg + HTTPMethod:@"POST" + body:bodyValidationArg + additionalHeaders:@{@"Content-Type" : @"application/json"}]) + .andReturn(resultPromise); +} + +- (NSData *)generateRandomData { + return [[NSUUID UUID].UUIDString dataUsingEncoding:NSUTF8StringEncoding]; +} + @end diff --git a/FirebaseAppCheck/Tests/Unit/AppAttestProvider/FIRAppAttestProviderTests.m b/FirebaseAppCheck/Tests/Unit/AppAttestProvider/FIRAppAttestProviderTests.m index 9ad57e7bcdd..786a80547c0 100644 --- a/FirebaseAppCheck/Tests/Unit/AppAttestProvider/FIRAppAttestProviderTests.m +++ b/FirebaseAppCheck/Tests/Unit/AppAttestProvider/FIRAppAttestProviderTests.m @@ -16,12 +16,13 @@ #import +#import #import "FBLPromise+Testing.h" -#import "OCMock.h" #import "FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRAppAttestProvider.h" #import "FirebaseAppCheck/Sources/AppAttestProvider/API/FIRAppAttestAPIService.h" +#import "FirebaseAppCheck/Sources/AppAttestProvider/API/FIRAppAttestAttestationResponse.h" #import "FirebaseAppCheck/Sources/AppAttestProvider/FIRAppAttestService.h" #import "FirebaseAppCheck/Sources/AppAttestProvider/Storage/FIRAppAttestArtifactStorage.h" #import "FirebaseAppCheck/Sources/AppAttestProvider/Storage/FIRAppAttestKeyIDStorage.h" @@ -98,9 +99,9 @@ - (void)testGetTokenWhenAppAttestIsNotSupported { OCMReject([self.mockAppAttestService attestKey:OCMOCK_ANY clientDataHash:OCMOCK_ANY completionHandler:OCMOCK_ANY]); - OCMReject([self.mockAPIService appCheckTokenWithAttestation:OCMOCK_ANY - keyID:OCMOCK_ANY - challenge:OCMOCK_ANY]); + OCMReject([self.mockAPIService attestKeyWithAttestation:OCMOCK_ANY + keyID:OCMOCK_ANY + challenge:OCMOCK_ANY]); // 3. Call get token. XCTestExpectation *completionExpectation = @@ -154,15 +155,22 @@ - (void)testGetToken_WhenNoExistingKey_Success { clientDataHash:expectedChallengeHash completionHandler:attestCompletionArg]); - // 7. Expect exchange request to be sent. + // 7. Expect key attestation request to be sent. FIRAppCheckToken *FACToken = [[FIRAppCheckToken alloc] initWithToken:@"FAC token" expirationDate:[NSDate date]]; - OCMExpect([self.mockAPIService appCheckTokenWithAttestation:attestationData - keyID:generatedKeyID - challenge:randomChallenge]) - .andReturn([FBLPromise resolvedWith:FACToken]); + NSData *artifactData = [@"attestation artifact" dataUsingEncoding:NSUTF8StringEncoding]; + __auto_type attestKeyResponse = + [[FIRAppAttestAttestationResponse alloc] initWithArtifact:artifactData token:FACToken]; + OCMExpect([self.mockAPIService attestKeyWithAttestation:attestationData + keyID:generatedKeyID + challenge:randomChallenge]) + .andReturn([FBLPromise resolvedWith:attestKeyResponse]); + + // 8. Expect the artifact received from Firebase backend to be saved. + OCMExpect([self.mockArtifactStorage setArtifact:artifactData]) + .andReturn([FBLPromise resolvedWith:artifactData]); - // 8. Call get token. + // 9. Call get token. XCTestExpectation *completionExpectation = [self expectationWithDescription:@"completionExpectation"]; [self.provider @@ -176,7 +184,7 @@ - (void)testGetToken_WhenNoExistingKey_Success { [self waitForExpectations:@[ completionExpectation ] timeout:0.5]; - // 9. Verify mocks. + // 10. Verify mocks. [self verifyAllMocks]; } @@ -216,15 +224,22 @@ - (void)testGetToken_WhenExistingUnregisteredKey_Success { clientDataHash:expectedChallengeHash completionHandler:attestCompletionArg]); - // 8. Expect exchange request to be sent. + // 8. Expect key attestation request to be sent. FIRAppCheckToken *FACToken = [[FIRAppCheckToken alloc] initWithToken:@"FAC token" expirationDate:[NSDate date]]; - OCMExpect([self.mockAPIService appCheckTokenWithAttestation:attestationData - keyID:existingKeyID - challenge:randomChallenge]) - .andReturn([FBLPromise resolvedWith:FACToken]); - - // 9. Call get token. + NSData *artifactData = [@"attestation artifact" dataUsingEncoding:NSUTF8StringEncoding]; + __auto_type attestKeyResponse = + [[FIRAppAttestAttestationResponse alloc] initWithArtifact:artifactData token:FACToken]; + OCMExpect([self.mockAPIService attestKeyWithAttestation:attestationData + keyID:existingKeyID + challenge:randomChallenge]) + .andReturn([FBLPromise resolvedWith:attestKeyResponse]); + + // 9. Expect the artifact received from Firebase backend to be saved. + OCMExpect([self.mockArtifactStorage setArtifact:artifactData]) + .andReturn([FBLPromise resolvedWith:artifactData]); + + // 10. Call get token. XCTestExpectation *completionExpectation = [self expectationWithDescription:@"completionExpectation"]; [self.provider @@ -238,7 +253,7 @@ - (void)testGetToken_WhenExistingUnregisteredKey_Success { [self waitForExpectations:@[ completionExpectation ] timeout:0.5]; - // 10. Verify mocks. + // 11. Verify mocks. [self verifyAllMocks]; } @@ -271,9 +286,9 @@ - (void)testGetToken_WhenUnregisteredKeyAndRandomChallengeError { OCMReject([self.mockAppAttestService attestKey:OCMOCK_ANY clientDataHash:OCMOCK_ANY completionHandler:OCMOCK_ANY]); - OCMReject([self.mockAPIService appCheckTokenWithAttestation:OCMOCK_ANY - keyID:OCMOCK_ANY - challenge:OCMOCK_ANY]); + OCMReject([self.mockAPIService attestKeyWithAttestation:OCMOCK_ANY + keyID:OCMOCK_ANY + challenge:OCMOCK_ANY]); // 6. Call get token. XCTestExpectation *completionExpectation = @@ -325,9 +340,9 @@ - (void)testGetToken_WhenUnregisteredKeyAndKeyAttestationError { completionHandler:attestCompletionArg]); // 6. Don't exchange API request. - OCMReject([self.mockAPIService appCheckTokenWithAttestation:OCMOCK_ANY - keyID:OCMOCK_ANY - challenge:OCMOCK_ANY]); + OCMReject([self.mockAPIService attestKeyWithAttestation:OCMOCK_ANY + keyID:OCMOCK_ANY + challenge:OCMOCK_ANY]); // 7. Call get token. XCTestExpectation *completionExpectation = @@ -380,9 +395,9 @@ - (void)testGetToken_WhenUnregisteredKeyAndKeyAttestationExchangeError { NSError *exchangeError = [NSError errorWithDomain:@"testGetTokenWhenKeyAttestationExchangeError" code:0 userInfo:nil]; - OCMExpect([self.mockAPIService appCheckTokenWithAttestation:attestationData - keyID:existingKeyID - challenge:randomChallenge]) + OCMExpect([self.mockAPIService attestKeyWithAttestation:attestationData + keyID:existingKeyID + challenge:randomChallenge]) .andReturn([self rejectedPromiseWithError:exchangeError]); // 7. Call get token. From a35d7bd0efffd0ce3ae117197d39664d90791b5f Mon Sep 17 00:00:00 2001 From: Maksym Malyhin Date: Wed, 12 May 2021 16:01:28 -0400 Subject: [PATCH 08/37] App Check App Attest initial handshake adjustments (#8067) * calculatre sha256 of random challenge for attestation * Test app adjustments * cleanup * use trailing closures in the test app --- .../FIRAppCheckTestApp/AppDelegate.swift | 88 ++++++++++++++----- .../Apps/FIRAppCheckTestApp/Podfile | 2 - .../AppAttestProvider/FIRAppAttestProvider.m | 54 ++++++------ .../Core/Utils/FIRAppCheckCryptoUtils.h | 27 ++++++ .../Core/Utils/FIRAppCheckCryptoUtils.m | 29 ++++++ .../FIRAppAttestProviderTests.m | 38 ++++---- .../Unit/Core/FIRAppCheckCryptoUtilsTests.m | 41 +++++++++ 7 files changed, 207 insertions(+), 72 deletions(-) create mode 100644 FirebaseAppCheck/Sources/Core/Utils/FIRAppCheckCryptoUtils.h create mode 100644 FirebaseAppCheck/Sources/Core/Utils/FIRAppCheckCryptoUtils.m create mode 100644 FirebaseAppCheck/Tests/Unit/Core/FIRAppCheckCryptoUtilsTests.m diff --git a/FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestApp/AppDelegate.swift b/FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestApp/AppDelegate.swift index bc35918a136..80f42aa38d5 100644 --- a/FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestApp/AppDelegate.swift +++ b/FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestApp/AppDelegate.swift @@ -26,32 +26,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate { .LaunchOptionsKey: Any]?) -> Bool { FirebaseApp.configure() - guard let firebaseApp = FirebaseApp.app() else { - return true - } - - FIRDeviceCheckProvider(app: firebaseApp)?.getToken(completion: { token, error in - if let token = token { - print("DeviceCheck token: \(token.token), expiration date: \(token.expirationDate)") - } - - if let error = error { - print("DeviceCheck error: \((error as NSError).userInfo)") - } - }) - - if let debugProvider = FIRAppCheckDebugProvider(app: firebaseApp) { - print("Debug token: \(debugProvider.currentDebugToken())") + requestDeviceCheckToken() - debugProvider.getToken(completion: { token, error in - if let token = token { - print("Debug FAC token: \(token.token), expiration date: \(token.expirationDate)") - } + requestDebugToken() - if let error = error { - print("Debug error: \(error)") - } - }) + if #available(iOS 14.0, *) { + requestAppAttestToken() } return true @@ -76,4 +56,64 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. // Use this method to release any resources that were specific to the discarded scenes, as they will not return. } + + // MARK: App Check providers + + func requestDeviceCheckToken() { + guard let firebaseApp = FirebaseApp.app() else { + return + } + + DeviceCheckProvider(app: firebaseApp)?.getToken { token, error in + if let token = token { + print("DeviceCheck token: \(token.token), expiration date: \(token.expirationDate)") + } + + if let error = error { + print("DeviceCheck error: \((error as NSError).userInfo)") + } + } + } + + func requestDebugToken() { + guard let firebaseApp = FirebaseApp.app() else { + return + } + + if let debugProvider = AppCheckDebugProvider(app: firebaseApp) { + print("Debug token: \(debugProvider.currentDebugToken())") + + debugProvider.getToken { token, error in + if let token = token { + print("Debug FAC token: \(token.token), expiration date: \(token.expirationDate)") + } + + if let error = error { + print("Debug error: \(error)") + } + } + } + } + + @available(iOS 14.0, *) + func requestAppAttestToken() { + guard let firebaseApp = FirebaseApp.app() else { + return + } + + guard let appAttestProvider = AppAttestProvider(app: firebaseApp) else { + print("Failed to instantiate AppAttestProvider") + return + } + + appAttestProvider.getToken { token, error in + if let token = token { + print("App Attest FAC token: \(token.token), expiration date: \(token.expirationDate)") + } + + if let error = error { + print("App Attest error: \(error)") + } + } + } } diff --git a/FirebaseAppCheck/Apps/FIRAppCheckTestApp/Podfile b/FirebaseAppCheck/Apps/FIRAppCheckTestApp/Podfile index 7121c0bd967..10a467bf586 100644 --- a/FirebaseAppCheck/Apps/FIRAppCheckTestApp/Podfile +++ b/FirebaseAppCheck/Apps/FIRAppCheckTestApp/Podfile @@ -11,7 +11,5 @@ target 'FIRAppCheckTestApp' do pod 'FirebaseAppCheck', :path => '../../../' pod 'FirebaseCore', :path => '../../../' - pod 'GoogleUtilities', :path => '../../../' pod 'FirebaseCoreDiagnostics', :path => '../../../' - pod 'GoogleDataTransport', :path => '../../../' end diff --git a/FirebaseAppCheck/Sources/AppAttestProvider/FIRAppAttestProvider.m b/FirebaseAppCheck/Sources/AppAttestProvider/FIRAppAttestProvider.m index 2a07e31737b..b8ebca316e9 100644 --- a/FirebaseAppCheck/Sources/AppAttestProvider/FIRAppAttestProvider.m +++ b/FirebaseAppCheck/Sources/AppAttestProvider/FIRAppAttestProvider.m @@ -32,6 +32,7 @@ #import "FirebaseAppCheck/Sources/AppAttestProvider/Storage/FIRAppAttestKeyIDStorage.h" #import "FirebaseAppCheck/Sources/Core/APIService/FIRAppCheckAPIService.h" #import "FirebaseAppCheck/Sources/Core/Errors/FIRAppCheckErrorUtil.h" +#import "FirebaseAppCheck/Sources/Core/Utils/FIRAppCheckCryptoUtils.h" #import "FirebaseCore/Sources/Private/FirebaseCoreInternal.h" @@ -156,7 +157,7 @@ - (void)getTokenWithCompletion:(void (^)(FIRAppCheckToken *_Nullable, NSError *_ break; case FIRAppAttestAttestationStateKeyRegistered: - // Refresh FAC token using the existing registerred App Attest key pair. + // Refresh FAC token using the existing registered App Attest key pair. return [self refreshTokenWithKeyID:attestState.appAttestKeyID artifact:attestState.attestationArtifact]; break; @@ -207,6 +208,31 @@ - (void)getTokenWithCompletion:(void (^)(FIRAppCheckToken *_Nullable, NSError *_ }); } +- (FBLPromise *)attestKey:(NSString *)keyID + challenge:(NSData *)challenge { + return [FBLPromise onQueue:self.queue + do:^NSData *_Nullable { + return [FIRAppCheckCryptoUtils sha256HashFromData:challenge]; + }] + .thenOn( + self.queue, + ^FBLPromise *(NSData *challengeHash) { + return [FBLPromise onQueue:self.queue + wrapObjectOrErrorCompletion:^(FBLPromiseObjectOrErrorCompletion _Nonnull handler) { + [self.appAttestService attestKey:keyID + clientDataHash:challengeHash + completionHandler:handler]; + }]; + }) + .thenOn(self.queue, ^FBLPromise *(NSData *attestation) { + FIRAppAttestKeyAttestationResult *result = + [[FIRAppAttestKeyAttestationResult alloc] initWithKeyID:keyID + challenge:challenge + attestation:attestation]; + return [FBLPromise resolvedWith:result]; + }); +} + #pragma mark - Token refresh sequence - (FBLPromise *)refreshTokenWithKeyID:(NSString *)keyID @@ -289,32 +315,6 @@ - (void)getTokenWithCompletion:(void (^)(FIRAppCheckToken *_Nullable, NSError *_ }); } -- (FBLPromise *)attestKey:(NSString *)keyID - challenge:(NSData *)challenge { - return [FBLPromise onQueue:self.queue - do:^id _Nullable { - // TODO: Hash challenge. - return [challenge base64EncodedDataWithOptions:0]; - }] - .thenOn( - self.queue, - ^FBLPromise *(NSData *challengeHash) { - return [FBLPromise onQueue:self.queue - wrapObjectOrErrorCompletion:^(FBLPromiseObjectOrErrorCompletion _Nonnull handler) { - [self.appAttestService attestKey:keyID - clientDataHash:challengeHash - completionHandler:handler]; - }]; - }) - .thenOn(self.queue, ^FBLPromise *(NSData *attestation) { - FIRAppAttestKeyAttestationResult *result = - [[FIRAppAttestKeyAttestationResult alloc] initWithKeyID:keyID - challenge:challenge - attestation:attestation]; - return [FBLPromise resolvedWith:result]; - }); -} - @end NS_ASSUME_NONNULL_END diff --git a/FirebaseAppCheck/Sources/Core/Utils/FIRAppCheckCryptoUtils.h b/FirebaseAppCheck/Sources/Core/Utils/FIRAppCheckCryptoUtils.h new file mode 100644 index 00000000000..1e1ac28e3e5 --- /dev/null +++ b/FirebaseAppCheck/Sources/Core/Utils/FIRAppCheckCryptoUtils.h @@ -0,0 +1,27 @@ +/* + * Copyright 2021 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 + +NS_ASSUME_NONNULL_BEGIN + +@interface FIRAppCheckCryptoUtils : NSObject + ++ (NSData *)sha256HashFromData:(NSData *)dataToHash; + +@end + +NS_ASSUME_NONNULL_END diff --git a/FirebaseAppCheck/Sources/Core/Utils/FIRAppCheckCryptoUtils.m b/FirebaseAppCheck/Sources/Core/Utils/FIRAppCheckCryptoUtils.m new file mode 100644 index 00000000000..9e2807b5007 --- /dev/null +++ b/FirebaseAppCheck/Sources/Core/Utils/FIRAppCheckCryptoUtils.m @@ -0,0 +1,29 @@ +/* + * Copyright 2021 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 "FirebaseAppCheck/Sources/Core/Utils/FIRAppCheckCryptoUtils.h" + +#import + +@implementation FIRAppCheckCryptoUtils + ++ (NSData *)sha256HashFromData:(NSData *)dataToHash { + NSMutableData *digest = [[NSMutableData alloc] initWithLength:CC_SHA256_DIGEST_LENGTH]; + CC_SHA256(dataToHash.bytes, (CC_LONG)dataToHash.length, digest.mutableBytes); + return [digest copy]; +} + +@end diff --git a/FirebaseAppCheck/Tests/Unit/AppAttestProvider/FIRAppAttestProviderTests.m b/FirebaseAppCheck/Tests/Unit/AppAttestProvider/FIRAppAttestProviderTests.m index 786a80547c0..1d895aede02 100644 --- a/FirebaseAppCheck/Tests/Unit/AppAttestProvider/FIRAppAttestProviderTests.m +++ b/FirebaseAppCheck/Tests/Unit/AppAttestProvider/FIRAppAttestProviderTests.m @@ -51,6 +51,9 @@ @interface FIRAppAttestProviderTests : XCTestCase @property(nonatomic) OCMockObject *mockStorage; @property(nonatomic) OCMockObject *mockArtifactStorage; +@property(nonatomic) NSData *randomChallenge; +@property(nonatomic) NSData *randomChallengeHash; + @end @implementation FIRAppAttestProviderTests @@ -67,6 +70,11 @@ - (void)setUp { APIService:self.mockAPIService keyIDStorage:self.mockStorage artifactStorage:self.mockArtifactStorage]; + + self.randomChallenge = [@"random challenge" dataUsingEncoding:NSUTF8StringEncoding]; + self.randomChallengeHash = + [[NSData alloc] initWithBase64EncodedString:@"vEq8yE9g+WwfifNqC2wsXN9M3NIDeOKpDBVYLpGbUDY=" + options:0]; } - (void)tearDown { @@ -143,16 +151,14 @@ - (void)testGetToken_WhenNoExistingKey_Success { .andReturn([FBLPromise resolvedWith:generatedKeyID]); // 5. Expect random challenge to be requested. - NSData *randomChallenge = [@"random challenge" dataUsingEncoding:NSUTF8StringEncoding]; OCMExpect([self.mockAPIService getRandomChallenge]) - .andReturn([FBLPromise resolvedWith:randomChallenge]); + .andReturn([FBLPromise resolvedWith:self.randomChallenge]); // 6. Expect the key to be attested with the challenge. - NSData *expectedChallengeHash = [randomChallenge base64EncodedDataWithOptions:0]; NSData *attestationData = [@"attestation data" dataUsingEncoding:NSUTF8StringEncoding]; id attestCompletionArg = [OCMArg invokeBlockWithArgs:attestationData, [NSNull null], nil]; OCMExpect([self.mockAppAttestService attestKey:generatedKeyID - clientDataHash:expectedChallengeHash + clientDataHash:self.randomChallengeHash completionHandler:attestCompletionArg]); // 7. Expect key attestation request to be sent. @@ -163,7 +169,7 @@ - (void)testGetToken_WhenNoExistingKey_Success { [[FIRAppAttestAttestationResponse alloc] initWithArtifact:artifactData token:FACToken]; OCMExpect([self.mockAPIService attestKeyWithAttestation:attestationData keyID:generatedKeyID - challenge:randomChallenge]) + challenge:self.randomChallenge]) .andReturn([FBLPromise resolvedWith:attestKeyResponse]); // 8. Expect the artifact received from Firebase backend to be saved. @@ -212,16 +218,14 @@ - (void)testGetToken_WhenExistingUnregisteredKey_Success { OCMExpect([self.mockArtifactStorage getArtifact]).andReturn(rejectedPromise); // 6. Expect random challenge to be requested. - NSData *randomChallenge = [@"random challenge" dataUsingEncoding:NSUTF8StringEncoding]; OCMExpect([self.mockAPIService getRandomChallenge]) - .andReturn([FBLPromise resolvedWith:randomChallenge]); + .andReturn([FBLPromise resolvedWith:self.randomChallenge]); // 7. Expect the key to be attested with the challenge. - NSData *expectedChallengeHash = [randomChallenge base64EncodedDataWithOptions:0]; NSData *attestationData = [@"attestation data" dataUsingEncoding:NSUTF8StringEncoding]; id attestCompletionArg = [OCMArg invokeBlockWithArgs:attestationData, [NSNull null], nil]; OCMExpect([self.mockAppAttestService attestKey:existingKeyID - clientDataHash:expectedChallengeHash + clientDataHash:self.randomChallengeHash completionHandler:attestCompletionArg]); // 8. Expect key attestation request to be sent. @@ -232,7 +236,7 @@ - (void)testGetToken_WhenExistingUnregisteredKey_Success { [[FIRAppAttestAttestationResponse alloc] initWithArtifact:artifactData token:FACToken]; OCMExpect([self.mockAPIService attestKeyWithAttestation:attestationData keyID:existingKeyID - challenge:randomChallenge]) + challenge:self.randomChallenge]) .andReturn([FBLPromise resolvedWith:attestKeyResponse]); // 9. Expect the artifact received from Firebase backend to be saved. @@ -325,18 +329,16 @@ - (void)testGetToken_WhenUnregisteredKeyAndKeyAttestationError { OCMExpect([self.mockArtifactStorage getArtifact]).andReturn(rejectedPromise); // 4. Expect random challenge to be requested. - NSData *randomChallenge = [@"random challenge" dataUsingEncoding:NSUTF8StringEncoding]; OCMExpect([self.mockAPIService getRandomChallenge]) - .andReturn([FBLPromise resolvedWith:randomChallenge]); + .andReturn([FBLPromise resolvedWith:self.randomChallenge]); // 5. Expect the key to be attested with the challenge. - NSData *expectedChallengeHash = [randomChallenge base64EncodedDataWithOptions:0]; NSError *attestationError = [NSError errorWithDomain:@"testGetTokenWhenKeyAttestationError" code:0 userInfo:nil]; id attestCompletionArg = [OCMArg invokeBlockWithArgs:[NSNull null], attestationError, nil]; OCMExpect([self.mockAppAttestService attestKey:existingKeyID - clientDataHash:expectedChallengeHash + clientDataHash:self.randomChallengeHash completionHandler:attestCompletionArg]); // 6. Don't exchange API request. @@ -379,16 +381,14 @@ - (void)testGetToken_WhenUnregisteredKeyAndKeyAttestationExchangeError { OCMExpect([self.mockArtifactStorage getArtifact]).andReturn(rejectedPromise); // 4. Expect random challenge to be requested. - NSData *randomChallenge = [@"random challenge" dataUsingEncoding:NSUTF8StringEncoding]; OCMExpect([self.mockAPIService getRandomChallenge]) - .andReturn([FBLPromise resolvedWith:randomChallenge]); + .andReturn([FBLPromise resolvedWith:self.randomChallenge]); // 5. Expect the key to be attested with the challenge. - NSData *expectedChallengeHash = [randomChallenge base64EncodedDataWithOptions:0]; NSData *attestationData = [@"attestation data" dataUsingEncoding:NSUTF8StringEncoding]; id attestCompletionArg = [OCMArg invokeBlockWithArgs:attestationData, [NSNull null], nil]; OCMExpect([self.mockAppAttestService attestKey:existingKeyID - clientDataHash:expectedChallengeHash + clientDataHash:self.randomChallengeHash completionHandler:attestCompletionArg]); // 6. Expect exchange request to be sent. @@ -397,7 +397,7 @@ - (void)testGetToken_WhenUnregisteredKeyAndKeyAttestationExchangeError { userInfo:nil]; OCMExpect([self.mockAPIService attestKeyWithAttestation:attestationData keyID:existingKeyID - challenge:randomChallenge]) + challenge:self.randomChallenge]) .andReturn([self rejectedPromiseWithError:exchangeError]); // 7. Call get token. diff --git a/FirebaseAppCheck/Tests/Unit/Core/FIRAppCheckCryptoUtilsTests.m b/FirebaseAppCheck/Tests/Unit/Core/FIRAppCheckCryptoUtilsTests.m new file mode 100644 index 00000000000..5460d715f16 --- /dev/null +++ b/FirebaseAppCheck/Tests/Unit/Core/FIRAppCheckCryptoUtilsTests.m @@ -0,0 +1,41 @@ +/* + * Copyright 2021 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 + +#import "FirebaseAppCheck/Sources/Core/Utils/FIRAppCheckCryptoUtils.h" + +@interface FIRAppCheckCryptoUtilsTests : XCTestCase + +@end + +@implementation FIRAppCheckCryptoUtilsTests + +- (void)testSHA256HashFromData { + NSData *dataToHash = [@"some data to hash" dataUsingEncoding:NSUTF8StringEncoding]; + + NSData *hashData = [FIRAppCheckCryptoUtils sha256HashFromData:dataToHash]; + + // Convert to a base64 encoded string to compare. + NSString *base64EncodedHashString = [hashData base64EncodedStringWithOptions:0]; + + // Base64 encoded hash of UTF8 encoded string "some data to hash". + NSString *expectedHashString = @"ai2iCUOTHpg0/BLP5btHu9muQ0iaMHJpYrV29OOZPlA="; + + XCTAssertEqualObjects(base64EncodedHashString, expectedHashString); +} + +@end From c2bb06bdf92e13f95a968cac419fbafb44cfc4ae Mon Sep 17 00:00:00 2001 From: Nick Cooke <36927374+ncooke3@users.noreply.github.com> Date: Thu, 13 May 2021 11:39:44 -0400 Subject: [PATCH 09/37] Implement API for ExchangeAppAttestAssertionRequest endpoint (#8065) * Implement assertion exchange * Tweak existing tests * Add tests * Rename JSON to better match gRPC message * Add HTTPBody helper * Review * Review 2 * Review 3 --- .../API/FIRAppAttestAPIService.h | 6 +- .../API/FIRAppAttestAPIService.m | 150 ++++++++++----- ...n => FACTokenExchangeResponseSuccess.json} | 0 .../FIRAppAttestAPIServiceTests.m | 180 +++++++++++++++++- .../Unit/Core/FIRAppCheckAPIServiceTests.m | 3 +- .../FIRDeviceCheckAPIServiceTests.m | 6 +- 6 files changed, 293 insertions(+), 52 deletions(-) rename FirebaseAppCheck/Tests/Fixture/{DeviceCheckResponseSuccess.json => FACTokenExchangeResponseSuccess.json} (100%) diff --git a/FirebaseAppCheck/Sources/AppAttestProvider/API/FIRAppAttestAPIService.h b/FirebaseAppCheck/Sources/AppAttestProvider/API/FIRAppAttestAPIService.h index 568ee4e4791..a3ae83f4879 100644 --- a/FirebaseAppCheck/Sources/AppAttestProvider/API/FIRAppAttestAPIService.h +++ b/FirebaseAppCheck/Sources/AppAttestProvider/API/FIRAppAttestAPIService.h @@ -42,9 +42,9 @@ NS_ASSUME_NONNULL_BEGIN challenge:(NSData *)challenge; /// Exchanges attestation data (artifact & assertion) and a challenge for a FAC token. -- (FBLPromise *)appCheckTokenWithArtifact:(NSData *)artifact - challenge:(NSData *)challenge - assertion:(NSData *)assertion; +- (FBLPromise *)getAppCheckTokenWithArtifact:(NSData *)artifact + challenge:(NSData *)challenge + assertion:(NSData *)assertion; @end diff --git a/FirebaseAppCheck/Sources/AppAttestProvider/API/FIRAppAttestAPIService.m b/FirebaseAppCheck/Sources/AppAttestProvider/API/FIRAppAttestAPIService.m index 861d75177f0..6f66048265b 100644 --- a/FirebaseAppCheck/Sources/AppAttestProvider/API/FIRAppAttestAPIService.m +++ b/FirebaseAppCheck/Sources/AppAttestProvider/API/FIRAppAttestAPIService.m @@ -30,12 +30,20 @@ NS_ASSUME_NONNULL_BEGIN +// TODO: Verify the following request fields. +static NSString *const kRequestFieldArtifact = @"artifact"; +static NSString *const kRequestFieldAssertion = @"assertion"; static NSString *const kRequestFieldAttestation = @"attestation_statement"; -static NSString *const kRequestFieldKeyID = @"key_id"; static NSString *const kRequestFieldChallenge = @"challenge"; +static NSString *const kRequestFieldKeyID = @"key_id"; + +static NSString *const kExchangeAppAttestAssertionEndpoint = @"exchangeAppAttestAssertion"; +static NSString *const kExchangeAppAttestAttestationEndpoint = @"exchangeAppAttestAttestation"; +static NSString *const kGenerateAppAttestChallengeEndpoint = @"generateAppAttestChallenge"; static NSString *const kContentTypeKey = @"Content-Type"; static NSString *const kJSONContentType = @"application/json"; +static NSString *const kHTTPMethodPost = @"POST"; @interface FIRAppAttestAPIService () @@ -60,29 +68,34 @@ - (instancetype)initWithAPIService:(id)APIService return self; } -- (dispatch_queue_t)backgroundQueue { - return dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0); -} +#pragma mark - Assertion request + +- (FBLPromise *)getAppCheckTokenWithArtifact:(NSData *)artifact + challenge:(NSData *)challenge + assertion:(NSData *)assertion { + NSURL *URL = [self URLForEndpoint:kExchangeAppAttestAssertionEndpoint]; -- (FBLPromise *)appCheckTokenWithArtifact:(NSData *)artifact - challenge:(NSData *)challenge - assertion:(NSData *)assertion { - // TODO: Implement. - return [FBLPromise resolvedWith:nil]; + return [self HTTPBodyWithArtifact:artifact challenge:challenge assertion:assertion] + .then(^FBLPromise *(NSData *HTTPBody) { + return [self.APIService sendRequestWithURL:URL + HTTPMethod:kHTTPMethodPost + body:HTTPBody + additionalHeaders:@{kContentTypeKey : kJSONContentType}]; + }) + .then(^id _Nullable(GULURLSessionDataResponse *_Nullable response) { + return [self.APIService appCheckTokenWithAPIResponse:response]; + }); } #pragma mark - Random Challenge - (nonnull FBLPromise *)getRandomChallenge { - NSString *URLString = - [NSString stringWithFormat:@"%@/projects/%@/apps/%@:generateAppAttestChallenge", - self.APIService.baseURL, self.projectID, self.appID]; - NSURL *URL = [NSURL URLWithString:URLString]; + NSURL *URL = [self URLForEndpoint:kGenerateAppAttestChallengeEndpoint]; return [FBLPromise onQueue:[self backgroundQueue] do:^id _Nullable { return [self.APIService sendRequestWithURL:URL - HTTPMethod:@"POST" + HTTPMethod:kHTTPMethodPost body:nil additionalHeaders:nil]; }] @@ -139,28 +152,50 @@ - (nullable NSData *)randomChallengeFromResponseBody:(NSData *)response error:(N - (FBLPromise *)attestKeyWithAttestation:(NSData *)attestation keyID:(NSString *)keyID challenge:(NSData *)challenge { - NSString *URLString = - [NSString stringWithFormat:@"%@/projects/%@/apps/%@:exchangeAppAttestAttestation", - self.APIService.baseURL, self.projectID, self.appID]; - NSURL *URL = [NSURL URLWithString:URLString]; + NSURL *URL = [self URLForEndpoint:kExchangeAppAttestAttestationEndpoint]; return [self HTTPBodyWithAttestation:attestation keyID:keyID challenge:challenge] .then(^FBLPromise *(NSData *HTTPBody) { return [self.APIService sendRequestWithURL:URL - HTTPMethod:@"POST" + HTTPMethod:kHTTPMethodPost body:HTTPBody additionalHeaders:@{kContentTypeKey : kJSONContentType}]; }) - .then(^id _Nullable(GULURLSessionDataResponse *_Nullable URLResponse) { - NSError *error; + .thenOn( + [self backgroundQueue], ^id _Nullable(GULURLSessionDataResponse *_Nullable URLResponse) { + NSError *error; - __auto_type response = - [[FIRAppAttestAttestationResponse alloc] initWithResponseData:URLResponse.HTTPBody - requestDate:[NSDate date] - error:&error]; + __auto_type response = + [[FIRAppAttestAttestationResponse alloc] initWithResponseData:URLResponse.HTTPBody + requestDate:[NSDate date] + error:&error]; - return response ?: error; - }); + return response ?: error; + }); +} + +#pragma mark - Request HTTP Body + +- (FBLPromise *)HTTPBodyWithArtifact:(NSData *)artifact + challenge:(NSData *)challenge + assertion:(NSData *)assertion { + if (artifact.length <= 0 || challenge.length <= 0 || assertion.length <= 0) { + FBLPromise *rejectedPromise = [FBLPromise pendingPromise]; + [rejectedPromise reject:[FIRAppCheckErrorUtil + errorWithFailureReason:@"Missing or empty request parameter."]]; + return rejectedPromise; + } + + return [FBLPromise onQueue:[self backgroundQueue] + do:^id { + id JSONObject = @{ + kRequestFieldArtifact : [self base64StringWithData:artifact], + kRequestFieldChallenge : [self base64StringWithData:challenge], + kRequestFieldAssertion : [self base64StringWithData:assertion] + }; + + return [self HTTPBodyWithJSONObject:JSONObject]; + }]; } - (FBLPromise *)HTTPBodyWithAttestation:(NSData *)attestation @@ -173,31 +208,56 @@ - (nullable NSData *)randomChallengeFromResponseBody:(NSData *)response error:(N return rejectedPromise; } - return [FBLPromise - onQueue:[self backgroundQueue] - do:^id _Nullable { - NSError *encodingError; - NSData *payloadJSON = [NSJSONSerialization dataWithJSONObject:@{ - kRequestFieldKeyID : keyID, - kRequestFieldAttestation : [self base64StringWithData:attestation], - kRequestFieldChallenge : [self base64StringWithData:challenge] - } - options:0 - error:&encodingError]; - - if (payloadJSON != nil) { - return payloadJSON; - } else { - return [FIRAppCheckErrorUtil JSONSerializationError:encodingError]; - } - }]; + return [FBLPromise onQueue:[self backgroundQueue] + do:^id { + id JSONObject = @{ + kRequestFieldKeyID : keyID, + kRequestFieldAttestation : [self base64StringWithData:attestation], + kRequestFieldChallenge : [self base64StringWithData:challenge] + }; + + return [self HTTPBodyWithJSONObject:JSONObject]; + }]; +} + +- (FBLPromise *)HTTPBodyWithJSONObject:(nonnull id)JSONObject { + NSError *encodingError; + NSData *payloadJSON = [NSJSONSerialization dataWithJSONObject:JSONObject + options:0 + error:&encodingError]; + FBLPromise *HTTPBodyPromise = [FBLPromise pendingPromise]; + if (payloadJSON) { + [HTTPBodyPromise fulfill:payloadJSON]; + } else { + [HTTPBodyPromise reject:[FIRAppCheckErrorUtil JSONSerializationError:encodingError]]; + } + return HTTPBodyPromise; } +#pragma mark - Helpers + - (NSString *)base64StringWithData:(NSData *)data { // TODO: Need to encode in base64URL? return [data base64EncodedStringWithOptions:0]; } +- (NSURL *)URLForEndpoint:(NSString *)endpoint { + NSString *URL = [[self class] URLWithBaseURL:self.APIService.baseURL + projectID:self.projectID + appID:self.appID]; + return [NSURL URLWithString:[NSString stringWithFormat:@"%@:%@", URL, endpoint]]; +} + ++ (NSString *)URLWithBaseURL:(NSString *)baseURL + projectID:(NSString *)projectID + appID:(NSString *)appID { + return [NSString stringWithFormat:@"%@/projects/%@/apps/%@", baseURL, projectID, appID]; +} + +- (dispatch_queue_t)backgroundQueue { + return dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0); +} + @end NS_ASSUME_NONNULL_END diff --git a/FirebaseAppCheck/Tests/Fixture/DeviceCheckResponseSuccess.json b/FirebaseAppCheck/Tests/Fixture/FACTokenExchangeResponseSuccess.json similarity index 100% rename from FirebaseAppCheck/Tests/Fixture/DeviceCheckResponseSuccess.json rename to FirebaseAppCheck/Tests/Fixture/FACTokenExchangeResponseSuccess.json diff --git a/FirebaseAppCheck/Tests/Unit/AppAttestProvider/FIRAppAttestAPIServiceTests.m b/FirebaseAppCheck/Tests/Unit/AppAttestProvider/FIRAppAttestAPIServiceTests.m index 7c0e8f463ea..cce578b7a76 100644 --- a/FirebaseAppCheck/Tests/Unit/AppAttestProvider/FIRAppAttestAPIServiceTests.m +++ b/FirebaseAppCheck/Tests/Unit/AppAttestProvider/FIRAppAttestAPIServiceTests.m @@ -200,6 +200,117 @@ - (void)assertMissingFieldErrorWithFixture:(NSString *)fixtureName fieldNameString); } +#pragma mark - Assertion request + +- (void)testGetAppCheckTokenSuccess { + NSData *artifact = [self generateRandomData]; + NSData *challenge = [self generateRandomData]; + NSData *assertion = [self generateRandomData]; + + // 1. Prepare response. + NSData *responseBody = + [FIRFixtureLoader loadFixtureNamed:@"FACTokenExchangeResponseSuccess.json"]; + GULURLSessionDataResponse *validAPIResponse = [self APIResponseWithCode:200 + responseBody:responseBody]; + + // 2. Stub API Service + // 2.1. Return prepared response. + [self expectTokenAPIRequestWithArtifact:artifact + challenge:challenge + assertion:assertion + response:validAPIResponse + error:nil]; + // 2.2. Return token from parsed response. + FIRAppCheckToken *expectedToken = [[FIRAppCheckToken alloc] initWithToken:@"app_check_token" + expirationDate:[NSDate date]]; + [self expectTokenWithAPIReponse:validAPIResponse toReturnToken:expectedToken]; + + // 3. Send request. + __auto_type promise = [self.appAttestAPIService getAppCheckTokenWithArtifact:artifact + challenge:challenge + assertion:assertion]; + // 4. Verify. + XCTAssert(FBLWaitForPromisesWithTimeout(1)); + + XCTAssertTrue(promise.isFulfilled); + XCTAssertNil(promise.error); + + XCTAssertEqualObjects(promise.value, expectedToken); + XCTAssertEqualObjects(promise.value.token, expectedToken.token); + XCTAssertEqualObjects(promise.value.expirationDate, expectedToken.expirationDate); + + OCMVerifyAll(self.mockAPIService); +} + +- (void)testGetAppCheckTokenNetworkError { + NSData *artifact = [self generateRandomData]; + NSData *challenge = [self generateRandomData]; + NSData *assertion = [self generateRandomData]; + + // 1. Prepare response. + NSData *responseBody = + [FIRFixtureLoader loadFixtureNamed:@"FACTokenExchangeResponseSuccess.json"]; + GULURLSessionDataResponse *validAPIResponse = [self APIResponseWithCode:200 + responseBody:responseBody]; + + // 2. Stub API Service + // 2.1. Return prepared response. + NSError *networkError = [NSError errorWithDomain:self.name code:0 userInfo:nil]; + [self expectTokenAPIRequestWithArtifact:artifact + challenge:challenge + assertion:assertion + response:validAPIResponse + error:networkError]; + + // 3. Send request. + __auto_type promise = [self.appAttestAPIService getAppCheckTokenWithArtifact:artifact + challenge:challenge + assertion:assertion]; + // 4. Verify. + XCTAssert(FBLWaitForPromisesWithTimeout(1)); + + XCTAssertTrue(promise.isRejected); + XCTAssertNil(promise.value); + XCTAssertEqualObjects(promise.error, networkError); + + OCMVerifyAll(self.mockAPIService); +} + +- (void)testGetAppCheckTokenUnexpectedResponse { + NSData *artifact = [self generateRandomData]; + NSData *challenge = [self generateRandomData]; + NSData *assertion = [self generateRandomData]; + + // 1. Prepare response. + NSData *responseBody = + [FIRFixtureLoader loadFixtureNamed:@"DeviceCheckResponseMissingToken.json"]; + GULURLSessionDataResponse *validAPIResponse = [self APIResponseWithCode:200 + responseBody:responseBody]; + + // 2. Stub API Service + // 2.1. Return prepared response. + [self expectTokenAPIRequestWithArtifact:artifact + challenge:challenge + assertion:assertion + response:validAPIResponse + error:nil]; + // 2.2. Return token from parsed response. + [self expectTokenWithAPIReponse:validAPIResponse toReturnToken:nil]; + + // 3. Send request. + __auto_type promise = [self.appAttestAPIService getAppCheckTokenWithArtifact:artifact + challenge:challenge + assertion:assertion]; + // 4. Verify. + XCTAssert(FBLWaitForPromisesWithTimeout(1)); + + XCTAssertTrue(promise.isRejected); + XCTAssertNil(promise.value); + XCTAssertNotNil(promise.error); + + OCMVerifyAll(self.mockAPIService); +} + #pragma mark - Attestation request - (void)testAttestKeySuccess { @@ -279,7 +390,8 @@ - (void)testAttestKeyUnexpectedResponse { NSString *keyID = [NSUUID UUID].UUIDString; // 1. Prepare unexpected response. - NSData *responseBody = [FIRFixtureLoader loadFixtureNamed:@"DeviceCheckResponseSuccess.json"]; + NSData *responseBody = + [FIRFixtureLoader loadFixtureNamed:@"FACTokenExchangeResponseSuccess.json"]; GULURLSessionDataResponse *validAPIResponse = [self APIResponseWithCode:200 responseBody:responseBody]; @@ -341,6 +453,72 @@ - (id)URLValidationArgumentWithResource:(NSString *)resource { return URLValidationArg; } +- (void)expectTokenAPIRequestWithArtifact:(NSData *)attestation + challenge:(NSData *)challenge + assertion:(NSData *)assertion + response:(nullable GULURLSessionDataResponse *)response + error:(nullable NSError *)error { + id URLValidationArg = [self URLValidationArgumentWithResource:@"exchangeAppAttestAssertion"]; + + id bodyValidationArg = [OCMArg checkWithBlock:^BOOL(NSData *requestBody) { + NSDictionary *decodedData = [NSJSONSerialization JSONObjectWithData:requestBody + options:0 + error:nil]; + + XCTAssert([decodedData isKindOfClass:[NSDictionary class]]); + + // Validate artifact field. + NSString *base64EncodedArtifact = decodedData[@"artifact"]; + XCTAssert([base64EncodedArtifact isKindOfClass:[NSString class]]); + + NSData *decodedAttestation = [[NSData alloc] initWithBase64EncodedString:base64EncodedArtifact + options:0]; + XCTAssertEqualObjects(decodedAttestation, attestation); + + // Validate challenge field. + NSString *base64EncodedChallenge = decodedData[@"challenge"]; + XCTAssert([base64EncodedChallenge isKindOfClass:[NSString class]]); + + NSData *decodedChallenge = [[NSData alloc] initWithBase64EncodedString:base64EncodedChallenge + options:0]; + XCTAssertEqualObjects(decodedChallenge, challenge); + + // Validate assertion field. + NSString *base64EncodedAssertion = decodedData[@"assertion"]; + XCTAssert([base64EncodedAssertion isKindOfClass:[NSString class]]); + + NSData *decodedAssertion = [[NSData alloc] initWithBase64EncodedString:base64EncodedAssertion + options:0]; + XCTAssertEqualObjects(decodedAssertion, assertion); + + return YES; + }]; + + FBLPromise *responsePromise = [FBLPromise pendingPromise]; + if (error) { + [responsePromise reject:error]; + } else { + [responsePromise fulfill:response]; + } + OCMExpect([self.mockAPIService sendRequestWithURL:URLValidationArg + HTTPMethod:@"POST" + body:bodyValidationArg + additionalHeaders:@{@"Content-Type" : @"application/json"}]) + .andReturn(responsePromise); +} + +- (void)expectTokenWithAPIReponse:(nonnull GULURLSessionDataResponse *)response + toReturnToken:(nullable FIRAppCheckToken *)token { + FBLPromise *tokenPromise = [FBLPromise pendingPromise]; + if (token) { + [tokenPromise fulfill:token]; + } else { + NSError *tokenError = [NSError errorWithDomain:self.name code:0 userInfo:nil]; + [tokenPromise reject:tokenError]; + } + OCMExpect([self.mockAPIService appCheckTokenWithAPIResponse:response]).andReturn(tokenPromise); +} + - (void)expectAttestAPIRequestWithAttestation:(NSData *)attestation keyID:(NSString *)keyID challenge:(NSData *)challenge diff --git a/FirebaseAppCheck/Tests/Unit/Core/FIRAppCheckAPIServiceTests.m b/FirebaseAppCheck/Tests/Unit/Core/FIRAppCheckAPIServiceTests.m index 60a40224308..0bc103b7eeb 100644 --- a/FirebaseAppCheck/Tests/Unit/Core/FIRAppCheckAPIServiceTests.m +++ b/FirebaseAppCheck/Tests/Unit/Core/FIRAppCheckAPIServiceTests.m @@ -203,7 +203,8 @@ - (void)testDataRequestNot2xxHTTPStatusCode { - (void)testAppCheckTokenWithAPIResponseValidResponse { // 1. Prepare input parameters. - NSData *responseBody = [FIRFixtureLoader loadFixtureNamed:@"DeviceCheckResponseSuccess.json"]; + NSData *responseBody = + [FIRFixtureLoader loadFixtureNamed:@"FACTokenExchangeResponseSuccess.json"]; XCTAssertNotNil(responseBody); NSHTTPURLResponse *HTTPResponse = [FIRURLSessionOCMockStub HTTPResponseWithCode:200]; GULURLSessionDataResponse *APIResponse = diff --git a/FirebaseAppCheck/Tests/Unit/DeviceCheckProvider/FIRDeviceCheckAPIServiceTests.m b/FirebaseAppCheck/Tests/Unit/DeviceCheckProvider/FIRDeviceCheckAPIServiceTests.m index 49f8e32bdfd..582c172e9e8 100644 --- a/FirebaseAppCheck/Tests/Unit/DeviceCheckProvider/FIRDeviceCheckAPIServiceTests.m +++ b/FirebaseAppCheck/Tests/Unit/DeviceCheckProvider/FIRDeviceCheckAPIServiceTests.m @@ -84,7 +84,8 @@ - (void)testAppCheckTokenSuccess { id HTTPBodyValidationArg = [self HTTPBodyValidationArgWithDeviceToken:deviceTokenData]; - NSData *responseBody = [FIRFixtureLoader loadFixtureNamed:@"DeviceCheckResponseSuccess.json"]; + NSData *responseBody = + [FIRFixtureLoader loadFixtureNamed:@"FACTokenExchangeResponseSuccess.json"]; XCTAssertNotNil(responseBody); NSHTTPURLResponse *HTTPResponse = [FIRURLSessionOCMockStub HTTPResponseWithCode:200]; @@ -134,7 +135,8 @@ - (void)testAppCheckTokenResponseParsingError { id HTTPBodyValidationArg = [self HTTPBodyValidationArgWithDeviceToken:deviceTokenData]; - NSData *responseBody = [FIRFixtureLoader loadFixtureNamed:@"DeviceCheckResponseSuccess.json"]; + NSData *responseBody = + [FIRFixtureLoader loadFixtureNamed:@"FACTokenExchangeResponseSuccess.json"]; XCTAssertNotNil(responseBody); NSHTTPURLResponse *HTTPResponse = [FIRURLSessionOCMockStub HTTPResponseWithCode:200]; From ac17a23579022d06321ea7899c4cfaf66874b359 Mon Sep 17 00:00:00 2001 From: Maksym Malyhin Date: Mon, 17 May 2021 10:33:34 -0400 Subject: [PATCH 10/37] App Check App Attest assertion flow (#8083) * App Attest assertion workflow draft * send request * assertion flow tests * style --- .../AppAttestProvider/FIRAppAttestProvider.m | 80 +++++- .../AppAttestProvider/FIRAppAttestService.h | 6 + .../FIRAppAttestProviderTests.m | 229 +++++++++++++++++- 3 files changed, 305 insertions(+), 10 deletions(-) diff --git a/FirebaseAppCheck/Sources/AppAttestProvider/FIRAppAttestProvider.m b/FirebaseAppCheck/Sources/AppAttestProvider/FIRAppAttestProvider.m index b8ebca316e9..a5fc32911ee 100644 --- a/FirebaseAppCheck/Sources/AppAttestProvider/FIRAppAttestProvider.m +++ b/FirebaseAppCheck/Sources/AppAttestProvider/FIRAppAttestProvider.m @@ -67,6 +67,35 @@ - (instancetype)initWithKeyID:(NSString *)keyID @end +/// A data object that contains information required for assertion request. +@interface FIRAppAttestAssertionData : NSObject + +@property(nonatomic, readonly) NSData *challenge; +@property(nonatomic, readonly) NSData *artifact; +@property(nonatomic, readonly) NSData *assertion; + +- (instancetype)initWithChallenge:(NSData *)challenge + artifact:(NSData *)artifact + assertion:(NSData *)assertion; + +@end + +@implementation FIRAppAttestAssertionData + +- (instancetype)initWithChallenge:(NSData *)challenge + artifact:(NSData *)artifact + assertion:(NSData *)assertion { + self = [super init]; + if (self) { + _challenge = challenge; + _artifact = artifact; + _assertion = assertion; + } + return self; +} + +@end + @interface FIRAppAttestProvider () @property(nonatomic, readonly) id APIService; @@ -165,7 +194,7 @@ - (void)getTokenWithCompletion:(void (^)(FIRAppCheckToken *_Nullable, NSError *_ }); } -#pragma mark - Initial handshake sequence +#pragma mark - Initial handshake sequence (attestation) - (FBLPromise *)initialHandshakeWithKeyID:(nullable NSString *)keyID { // 1. Request a random challenge and get App Attest key ID concurrently. @@ -233,12 +262,55 @@ - (void)getTokenWithCompletion:(void (^)(FIRAppCheckToken *_Nullable, NSError *_ }); } -#pragma mark - Token refresh sequence +#pragma mark - Token refresh sequence (assertion) - (FBLPromise *)refreshTokenWithKeyID:(NSString *)keyID artifact:(NSData *)artifact { - // TODO: Implement (b/186438346). - return [FBLPromise resolvedWith:nil]; + return [self.APIService getRandomChallenge] + .thenOn(self.queue, + ^FBLPromise *(NSData *challenge) { + return [self generateAssertionWithKeyID:keyID + artifact:artifact + challenge:challenge]; + }) + .thenOn(self.queue, ^id(FIRAppAttestAssertionData *assertion) { + return [self.APIService getAppCheckTokenWithArtifact:assertion.artifact + challenge:assertion.challenge + assertion:assertion.assertion]; + }); +} + +- (FBLPromise *)generateAssertionWithKeyID:(NSString *)keyID + artifact:(NSData *)artifact + challenge:(NSData *)challenge { + // 1. Calculate the statement and its hash for assertion. + return [FBLPromise + onQueue:self.queue + do:^NSData *_Nullable { + // 1.1. Compose statement to generate assertion for. + NSMutableData *statementForAssertion = [artifact mutableCopy]; + [statementForAssertion appendData:challenge]; + + // 1.2. Get the statement SHA256 hash. + return [FIRAppCheckCryptoUtils sha256HashFromData:[statementForAssertion copy]]; + }] + .thenOn( + self.queue, + ^FBLPromise *(NSData *statementHash) { + // 2. Generate App Attest assertion. + return [FBLPromise onQueue:self.queue + wrapObjectOrErrorCompletion:^(FBLPromiseObjectOrErrorCompletion _Nonnull handler) { + [self.appAttestService generateAssertion:keyID + clientDataHash:statementHash + completionHandler:handler]; + }]; + }) + // 3. Compose the result object. + .thenOn(self.queue, ^FIRAppAttestAssertionData *(NSData *assertion) { + return [[FIRAppAttestAssertionData alloc] initWithChallenge:challenge + artifact:artifact + assertion:assertion]; + }); } #pragma mark - State handling diff --git a/FirebaseAppCheck/Sources/AppAttestProvider/FIRAppAttestService.h b/FirebaseAppCheck/Sources/AppAttestProvider/FIRAppAttestService.h index fd254aaacca..27409a82d91 100644 --- a/FirebaseAppCheck/Sources/AppAttestProvider/FIRAppAttestService.h +++ b/FirebaseAppCheck/Sources/AppAttestProvider/FIRAppAttestService.h @@ -16,6 +16,8 @@ #import +@class FBLPromise; + NS_ASSUME_NONNULL_BEGIN /// See `DCAppAttestService` @@ -31,6 +33,10 @@ NS_ASSUME_NONNULL_BEGIN clientDataHash:(NSData *)clientDataHash completionHandler:(void (^)(NSData *attestationObject, NSError *error))completionHandler; +- (void)generateAssertion:(NSString *)keyId + clientDataHash:(NSData *)clientDataHash + completionHandler:(void (^)(NSData *assertionObject, NSError *error))completionHandler; + @end NS_ASSUME_NONNULL_END diff --git a/FirebaseAppCheck/Tests/Unit/AppAttestProvider/FIRAppAttestProviderTests.m b/FirebaseAppCheck/Tests/Unit/AppAttestProvider/FIRAppAttestProviderTests.m index 1d895aede02..04c59c61382 100644 --- a/FirebaseAppCheck/Tests/Unit/AppAttestProvider/FIRAppAttestProviderTests.m +++ b/FirebaseAppCheck/Tests/Unit/AppAttestProvider/FIRAppAttestProviderTests.m @@ -27,6 +27,7 @@ #import "FirebaseAppCheck/Sources/AppAttestProvider/Storage/FIRAppAttestArtifactStorage.h" #import "FirebaseAppCheck/Sources/AppAttestProvider/Storage/FIRAppAttestKeyIDStorage.h" #import "FirebaseAppCheck/Sources/Core/Errors/FIRAppCheckErrorUtil.h" +#import "FirebaseAppCheck/Sources/Core/Utils/FIRAppCheckCryptoUtils.h" #import "FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRAppCheckToken.h" #import "FirebaseCore/Sources/Private/FirebaseCoreInternal.h" @@ -34,6 +35,7 @@ // Currently FIRAppAttestProvider is available only on iOS. #if TARGET_OS_IOS +API_AVAILABLE(ios(14.0)) @interface FIRAppAttestProvider (Tests) - (instancetype)initWithAppAttestService:(id)appAttestService APIService:(id)APIService @@ -85,6 +87,8 @@ - (void)tearDown { self.mockAppAttestService = nil; } +#pragma mark - Init tests + - (void)testInitWithValidApp { FIROptions *options = [[FIROptions alloc] initWithGoogleAppID:@"app_id" GCMSenderID:@"sender_id"]; options.APIKey = @"api_key"; @@ -94,6 +98,8 @@ - (void)testInitWithValidApp { XCTAssertNotNil([[FIRAppAttestProvider alloc] initWithApp:app]); } +#pragma mark - Initial handshake (attestation) + - (void)testGetTokenWhenAppAttestIsNotSupported { // 1. Expect FIRAppAttestService.isSupported. [OCMExpect([self.mockAppAttestService isSupported]) andReturnValue:@(NO)]; @@ -279,11 +285,7 @@ - (void)testGetToken_WhenUnregisteredKeyAndRandomChallengeError { OCMExpect([self.mockArtifactStorage getArtifact]).andReturn(rejectedPromise); // 4. Expect random challenge to be requested. - NSError *challengeError = [NSError errorWithDomain:@"testGetToken_WhenRandomChallengeError" - code:NSNotFound - userInfo:nil]; - OCMExpect([self.mockAPIService getRandomChallenge]) - .andReturn([self rejectedPromiseWithError:challengeError]); + NSError *challengeError = [self expectRandomChallengeRequestError]; // 5. Don't expect other steps. OCMReject([self.mockStorage setAppAttestKeyID:OCMOCK_ANY]); @@ -417,7 +419,213 @@ - (void)testGetToken_WhenUnregisteredKeyAndKeyAttestationExchangeError { [self verifyAllMocks]; } -// TODO: FAC token refresh tests (b/186438346). +#pragma mark - FAC token refresh (assertion) + +- (void)testGetToken_WhenKeyRegistered_Success { + // 1. Expect FIRAppAttestService.isSupported. + [OCMExpect([self.mockAppAttestService isSupported]) andReturnValue:@(YES)]; + + // 2. Expect storage getAppAttestKeyID. + NSString *existingKeyID = @"existingKeyID"; + OCMExpect([self.mockStorage getAppAttestKeyID]) + .andReturn([FBLPromise resolvedWith:existingKeyID]); + + // 3. Expect a stored artifact to be requested. + NSData *storedArtifact = [@"storedArtifact" dataUsingEncoding:NSUTF8StringEncoding]; + OCMExpect([self.mockArtifactStorage getArtifact]) + .andReturn([FBLPromise resolvedWith:storedArtifact]); + + // 4. Expect random challenge to be requested. + OCMExpect([self.mockAPIService getRandomChallenge]) + .andReturn([FBLPromise resolvedWith:self.randomChallenge]); + + // 5. Expect assertion to be requested. + NSData *assertion = [@"generatedAssertion" dataUsingEncoding:NSUTF8StringEncoding]; + id completionBlockArg = [OCMArg invokeBlockWithArgs:assertion, [NSNull null], nil]; + OCMExpect([self.mockAppAttestService + generateAssertion:existingKeyID + clientDataHash:[self dataHashForAssertionWithArtifactData:storedArtifact] + completionHandler:completionBlockArg]); + + // 6. Expect assertion request to be sent. + FIRAppCheckToken *FACToken = [[FIRAppCheckToken alloc] initWithToken:@"FAC token" + expirationDate:[NSDate date]]; + OCMExpect([self.mockAPIService getAppCheckTokenWithArtifact:storedArtifact + challenge:self.randomChallenge + assertion:assertion]) + .andReturn([FBLPromise resolvedWith:FACToken]); + + // 7. Call get token. + XCTestExpectation *completionExpectation = + [self expectationWithDescription:@"completionExpectation"]; + [self.provider + getTokenWithCompletion:^(FIRAppCheckToken *_Nullable token, NSError *_Nullable error) { + [completionExpectation fulfill]; + + XCTAssertEqualObjects(token.token, FACToken.token); + XCTAssertEqualObjects(token.expirationDate, FACToken.expirationDate); + XCTAssertNil(error); + }]; + + [self waitForExpectations:@[ completionExpectation ] timeout:0.5]; + + // 8. Verify mocks. + [self verifyAllMocks]; +} + +- (void)testGetToken_WhenKeyRegisteredAndChallengeRequestError { + // 1. Expect FIRAppAttestService.isSupported. + [OCMExpect([self.mockAppAttestService isSupported]) andReturnValue:@(YES)]; + + // 2. Expect storage getAppAttestKeyID. + NSString *existingKeyID = @"existingKeyID"; + OCMExpect([self.mockStorage getAppAttestKeyID]) + .andReturn([FBLPromise resolvedWith:existingKeyID]); + + // 3. Expect a stored artifact to be requested. + NSData *storedArtifact = [@"storedArtifact" dataUsingEncoding:NSUTF8StringEncoding]; + OCMExpect([self.mockArtifactStorage getArtifact]) + .andReturn([FBLPromise resolvedWith:storedArtifact]); + + // 4. Expect random challenge to be requested. + NSError *challengeError = [self expectRandomChallengeRequestError]; + + // 5. Don't expect assertion to be requested. + OCMReject([self.mockAppAttestService generateAssertion:OCMOCK_ANY + clientDataHash:OCMOCK_ANY + completionHandler:OCMOCK_ANY]); + + // 6. Don't expect assertion request to be sent. + OCMReject([self.mockAPIService getAppCheckTokenWithArtifact:OCMOCK_ANY + challenge:OCMOCK_ANY + assertion:OCMOCK_ANY]); + + // 7. Call get token. + XCTestExpectation *completionExpectation = + [self expectationWithDescription:@"completionExpectation"]; + [self.provider + getTokenWithCompletion:^(FIRAppCheckToken *_Nullable token, NSError *_Nullable error) { + [completionExpectation fulfill]; + + XCTAssertNil(token); + XCTAssertEqualObjects(error, challengeError); + }]; + + [self waitForExpectations:@[ completionExpectation ] timeout:0.5]; + + // 8. Verify mocks. + [self verifyAllMocks]; +} + +- (void)testGetToken_WhenKeyRegisteredAndGenerateAssertionError { + // 1. Expect FIRAppAttestService.isSupported. + [OCMExpect([self.mockAppAttestService isSupported]) andReturnValue:@(YES)]; + + // 2. Expect storage getAppAttestKeyID. + NSString *existingKeyID = @"existingKeyID"; + OCMExpect([self.mockStorage getAppAttestKeyID]) + .andReturn([FBLPromise resolvedWith:existingKeyID]); + + // 3. Expect a stored artifact to be requested. + NSData *storedArtifact = [@"storedArtifact" dataUsingEncoding:NSUTF8StringEncoding]; + OCMExpect([self.mockArtifactStorage getArtifact]) + .andReturn([FBLPromise resolvedWith:storedArtifact]); + + // 4. Expect random challenge to be requested. + OCMExpect([self.mockAPIService getRandomChallenge]) + .andReturn([FBLPromise resolvedWith:self.randomChallenge]); + + // 5. Don't expect assertion to be requested. + NSError *generateAssertionError = + [NSError errorWithDomain:@"testGetToken_WhenKeyRegisteredAndGenerateAssertionError" + code:0 + userInfo:nil]; + id completionBlockArg = [OCMArg invokeBlockWithArgs:[NSNull null], generateAssertionError, nil]; + OCMExpect([self.mockAppAttestService + generateAssertion:existingKeyID + clientDataHash:[self dataHashForAssertionWithArtifactData:storedArtifact] + completionHandler:completionBlockArg]); + + // 6. Don't expect assertion request to be sent. + OCMReject([self.mockAPIService getAppCheckTokenWithArtifact:OCMOCK_ANY + challenge:OCMOCK_ANY + assertion:OCMOCK_ANY]); + + // 7. Call get token. + XCTestExpectation *completionExpectation = + [self expectationWithDescription:@"completionExpectation"]; + [self.provider + getTokenWithCompletion:^(FIRAppCheckToken *_Nullable token, NSError *_Nullable error) { + [completionExpectation fulfill]; + + XCTAssertNil(token); + XCTAssertEqualObjects(error, generateAssertionError); + }]; + + [self waitForExpectations:@[ completionExpectation ] timeout:0.5]; + + // 8. Verify mocks. + [self verifyAllMocks]; +} + +- (void)testGetToken_WhenKeyRegisteredAndTokenExchangeRequestError { + // 1. Expect FIRAppAttestService.isSupported. + [OCMExpect([self.mockAppAttestService isSupported]) andReturnValue:@(YES)]; + + // 2. Expect storage getAppAttestKeyID. + NSString *existingKeyID = @"existingKeyID"; + OCMExpect([self.mockStorage getAppAttestKeyID]) + .andReturn([FBLPromise resolvedWith:existingKeyID]); + + // 3. Expect a stored artifact to be requested. + NSData *storedArtifact = [@"storedArtifact" dataUsingEncoding:NSUTF8StringEncoding]; + OCMExpect([self.mockArtifactStorage getArtifact]) + .andReturn([FBLPromise resolvedWith:storedArtifact]); + + // 4. Expect random challenge to be requested. + OCMExpect([self.mockAPIService getRandomChallenge]) + .andReturn([FBLPromise resolvedWith:self.randomChallenge]); + + // 5. Don't expect assertion to be requested. + NSData *assertion = [@"generatedAssertion" dataUsingEncoding:NSUTF8StringEncoding]; + id completionBlockArg = [OCMArg invokeBlockWithArgs:assertion, [NSNull null], nil]; + OCMExpect([self.mockAppAttestService + generateAssertion:existingKeyID + clientDataHash:[self dataHashForAssertionWithArtifactData:storedArtifact] + completionHandler:completionBlockArg]); + + // 6. Expect assertion request to be sent. + NSError *tokenExchangeError = + [NSError errorWithDomain:@"testGetToken_WhenKeyRegisteredAndTokenExchangeRequestError" + code:0 + userInfo:nil]; + OCMExpect([self.mockAPIService getAppCheckTokenWithArtifact:storedArtifact + challenge:self.randomChallenge + assertion:assertion]) + .andReturn([self rejectedPromiseWithError:tokenExchangeError]); + + // 7. Call get token. + XCTestExpectation *completionExpectation = + [self expectationWithDescription:@"completionExpectation"]; + [self.provider + getTokenWithCompletion:^(FIRAppCheckToken *_Nullable token, NSError *_Nullable error) { + [completionExpectation fulfill]; + + XCTAssertNil(token); + XCTAssertEqualObjects(error, tokenExchangeError); + }]; + + [self waitForExpectations:@[ completionExpectation ] timeout:0.5]; + + // 8. Verify mocks. + [self verifyAllMocks]; +} + +- (NSData *)dataHashForAssertionWithArtifactData:(NSData *)artifact { + NSMutableData *statement = [artifact mutableCopy]; + [statement appendData:self.randomChallenge]; + return [FIRAppCheckCryptoUtils sha256HashFromData:statement]; +} #pragma mark - Helpers @@ -427,6 +635,15 @@ - (FBLPromise *)rejectedPromiseWithError:(NSError *)error { return rejectedPromise; } +- (NSError *)expectRandomChallengeRequestError { + NSError *challengeError = [NSError errorWithDomain:@"testGetToken_WhenRandomChallengeError" + code:NSNotFound + userInfo:nil]; + OCMExpect([self.mockAPIService getRandomChallenge]) + .andReturn([self rejectedPromiseWithError:challengeError]); + return challengeError; +} + - (void)verifyAllMocks { OCMVerifyAll(self.mockAppAttestService); OCMVerifyAll(self.mockAPIService); From cea2f687b8523931195201181d2c169106629248 Mon Sep 17 00:00:00 2001 From: Maksym Malyhin Date: Tue, 18 May 2021 17:01:24 -0400 Subject: [PATCH 11/37] App Check: store App Attest artifact per key ID (#8097) * Update artifact storage API and tests * Artifact storage implementation update * Save artifact for a key ID * Style * typos --- .../AppAttestProvider/FIRAppAttestProvider.m | 40 ++++++---- .../Storage/FIRAppAttestArtifactStorage.h | 12 ++- .../Storage/FIRAppAttestArtifactStorage.m | 18 +++-- .../Storage/FIRAppAttestStoredArtifact.h | 41 ++++++++++ .../Storage/FIRAppAttestStoredArtifact.m | 74 +++++++++++++++++++ .../FIRAppAttestProviderTests.m | 22 +++--- .../FIRAppAttestArtifactStorageTests.m | 67 +++++++++++++---- 7 files changed, 226 insertions(+), 48 deletions(-) create mode 100644 FirebaseAppCheck/Sources/AppAttestProvider/Storage/FIRAppAttestStoredArtifact.h create mode 100644 FirebaseAppCheck/Sources/AppAttestProvider/Storage/FIRAppAttestStoredArtifact.m diff --git a/FirebaseAppCheck/Sources/AppAttestProvider/FIRAppAttestProvider.m b/FirebaseAppCheck/Sources/AppAttestProvider/FIRAppAttestProvider.m index a5fc32911ee..aee9a380a44 100644 --- a/FirebaseAppCheck/Sources/AppAttestProvider/FIRAppAttestProvider.m +++ b/FirebaseAppCheck/Sources/AppAttestProvider/FIRAppAttestProvider.m @@ -215,23 +215,35 @@ - (void)getTokenWithCompletion:(void (^)(FIRAppCheckToken *_Nullable, NSError *_ }) // TODO: Handle a possible key rejection - generate another key. .thenOn(self.queue, - ^FBLPromise *( - FIRAppAttestKeyAttestationResult *result) { - // 3. Exchange the attestation to FAC token. - return [self.APIService attestKeyWithAttestation:result.attestation - keyID:result.keyID - challenge:result.challenge]; + ^FBLPromise *(FIRAppAttestKeyAttestationResult *result) { + // 3. Exchange the attestation to FAC token and pass the results to the next step. + NSArray *attestationResults = @[ + // 3.1. Just pass the attestation result to the next step. + [FBLPromise resolvedWith:result], + // 3.2. Exchange the attestation to FAC token. + [self.APIService attestKeyWithAttestation:result.attestation + keyID:result.keyID + challenge:result.challenge] + ]; + + return [FBLPromise onQueue:self.queue all:attestationResults]; }) - .thenOn(self.queue, - ^FBLPromise *(FIRAppAttestAttestationResponse *response) { - // 4. Save the artifact and return the received FAC token. - return [self saveArtifactAndGetAppCheckTokenFromResponse:response]; - }); + .thenOn(self.queue, ^FBLPromise *(NSArray *attestationResults) { + // 4. Save the artifact and return the received FAC token. + + FIRAppAttestKeyAttestationResult *attestation = attestationResults.firstObject; + FIRAppAttestAttestationResponse *firebaseAttestationResponse = + attestationResults.lastObject; + + return [self saveArtifactAndGetAppCheckTokenFromResponse:firebaseAttestationResponse + keyID:attestation.keyID]; + }); } - (FBLPromise *)saveArtifactAndGetAppCheckTokenFromResponse: - (FIRAppAttestAttestationResponse *)response { - return [self.artifactStorage setArtifact:response.artifact].thenOn( + (FIRAppAttestAttestationResponse *)response + keyID:(NSString *)keyID { + return [self.artifactStorage setArtifact:response.artifact forKey:keyID].thenOn( self.queue, ^FIRAppCheckToken *(id result) { return response.token; }); @@ -339,7 +351,7 @@ - (void)getTokenWithCompletion:(void (^)(FIRAppCheckToken *_Nullable, NSError *_ // 3. Check for stored attestation artifact received from Firebase backend. NSData *attestationArtifact = - FBLPromiseAwait([self.artifactStorage getArtifact], &error); + FBLPromiseAwait([self.artifactStorage getArtifactForKey:appAttestKeyID], &error); if (attestationArtifact == nil) { return [[FIRAppAttestProviderState alloc] initWithGeneratedKeyID:appAttestKeyID]; } diff --git a/FirebaseAppCheck/Sources/AppAttestProvider/Storage/FIRAppAttestArtifactStorage.h b/FirebaseAppCheck/Sources/AppAttestProvider/Storage/FIRAppAttestArtifactStorage.h index 91fe25e1085..8f2bd9e97c4 100644 --- a/FirebaseAppCheck/Sources/AppAttestProvider/Storage/FIRAppAttestArtifactStorage.h +++ b/FirebaseAppCheck/Sources/AppAttestProvider/Storage/FIRAppAttestArtifactStorage.h @@ -24,16 +24,20 @@ NS_ASSUME_NONNULL_BEGIN /// Check token obtained with App Attest provider. @protocol FIRAppAttestArtifactStorageProtocol -/// Set the artifact. +/// Set the artifact. An artifact previously set for *any* key ID will be replaced by the new one +/// with the new key ID. The storage always stores a single artifact. /// @param artifact The artifact data to store. Pass `nil` to remove the stored artifact. +/// @param keyID The App Attest key ID used to generate the artifact. /// @return An artifact that is resolved with the artifact data passed into the method in case of /// success or is rejected with an error. -- (FBLPromise *)setArtifact:(nullable NSData *)artifact; +- (FBLPromise *)setArtifact:(nullable NSData *)artifact forKey:(NSString *)keyID; /// Get the artifact. +/// @param keyID The App Attest key ID used to generate the artifact. /// @return A promise that is resolved with the artifact data if artifact exists, is resolved with -/// `nil` if no artifact found or is rejected with an error. -- (FBLPromise *)getArtifact; +/// `nil` if no artifact found (or the existing artifact was set for a different key ID) or is +/// rejected with an error. +- (FBLPromise *)getArtifactForKey:(NSString *)keyID; @end diff --git a/FirebaseAppCheck/Sources/AppAttestProvider/Storage/FIRAppAttestArtifactStorage.m b/FirebaseAppCheck/Sources/AppAttestProvider/Storage/FIRAppAttestArtifactStorage.m index 744370647f6..101f2a33a87 100644 --- a/FirebaseAppCheck/Sources/AppAttestProvider/Storage/FIRAppAttestArtifactStorage.m +++ b/FirebaseAppCheck/Sources/AppAttestProvider/Storage/FIRAppAttestArtifactStorage.m @@ -22,6 +22,8 @@ #import "FBLPromises.h" #endif +#import "FirebaseAppCheck/Sources/AppAttestProvider/Storage/FIRAppAttestStoredArtifact.h" + #import NS_ASSUME_NONNULL_BEGIN @@ -64,22 +66,26 @@ - (instancetype)initWithAppName:(NSString *)appName accessGroup:accessGroup]; } -- (FBLPromise *)getArtifact { +- (FBLPromise *)getArtifactForKey:(NSString *)keyID { return [self.keychainStorage getObjectForKey:[self artifactKey] - objectClass:[NSData class] + objectClass:[FIRAppAttestStoredArtifact class] accessGroup:self.accessGroup] .then(^NSData *(id storedArtifact) { - if ([(NSObject *)storedArtifact isKindOfClass:[NSData class]]) { - return (NSData *)storedArtifact; + FIRAppAttestStoredArtifact *artifact = (FIRAppAttestStoredArtifact *)storedArtifact; + if ([artifact isKindOfClass:[FIRAppAttestStoredArtifact class]] && + [artifact.keyID isEqualToString:keyID]) { + return artifact.artifact; } else { return nil; } }); } -- (FBLPromise *)setArtifact:(nullable NSData *)artifact { +- (FBLPromise *)setArtifact:(nullable NSData *)artifact forKey:(nonnull NSString *)keyID { if (artifact) { - return [self.keychainStorage setObject:artifact + FIRAppAttestStoredArtifact *storedArtifact = + [[FIRAppAttestStoredArtifact alloc] initWithKeyID:keyID artifact:artifact]; + return [self.keychainStorage setObject:storedArtifact forKey:[self artifactKey] accessGroup:self.accessGroup] .then(^id _Nullable(NSNull *_Nullable value) { diff --git a/FirebaseAppCheck/Sources/AppAttestProvider/Storage/FIRAppAttestStoredArtifact.h b/FirebaseAppCheck/Sources/AppAttestProvider/Storage/FIRAppAttestStoredArtifact.h new file mode 100644 index 00000000000..c78a0058d7d --- /dev/null +++ b/FirebaseAppCheck/Sources/AppAttestProvider/Storage/FIRAppAttestStoredArtifact.h @@ -0,0 +1,41 @@ +/* + * Copyright 2021 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 + +NS_ASSUME_NONNULL_BEGIN + +@interface FIRAppAttestStoredArtifact : NSObject + +/// The App Attest key ID used to generate the artifact. +@property(nonatomic, readonly) NSString *keyID; + +/// The Firebase App Attest artifact generated by the backend. +@property(nonatomic, readonly) NSData *artifact; + +/// The object version. +/// WARNING: The version must be incremented if properties are added, removed or modified. Migration +/// must be handled accordingly in `initWithCoder:` method. +@property(nonatomic, readonly) NSInteger storageVersion; + +- (instancetype)init NS_UNAVAILABLE; + +- (instancetype)initWithKeyID:(NSString *)keyID + artifact:(NSData *)artifact NS_DESIGNATED_INITIALIZER; + +@end + +NS_ASSUME_NONNULL_END diff --git a/FirebaseAppCheck/Sources/AppAttestProvider/Storage/FIRAppAttestStoredArtifact.m b/FirebaseAppCheck/Sources/AppAttestProvider/Storage/FIRAppAttestStoredArtifact.m new file mode 100644 index 00000000000..c0272f1c919 --- /dev/null +++ b/FirebaseAppCheck/Sources/AppAttestProvider/Storage/FIRAppAttestStoredArtifact.m @@ -0,0 +1,74 @@ +/* + * Copyright 2021 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 "FirebaseAppCheck/Sources/AppAttestProvider/Storage/FIRAppAttestStoredArtifact.h" + +static NSString *const kKeyIDKey = @"keyID"; +static NSString *const kArtifactKey = @"artifact"; +static NSString *const kStorageVersionKey = @"storageVersion"; + +static NSInteger const kStorageVersion = 1; + +@implementation FIRAppAttestStoredArtifact + +- (instancetype)initWithKeyID:(NSString *)keyID artifact:(NSData *)artifact { + self = [super init]; + if (self) { + _keyID = keyID; + _artifact = artifact; + } + return self; +} + +- (NSInteger)storageVersion { + return kStorageVersion; +} + +#pragma mark - NSSecureCoding + ++ (BOOL)supportsSecureCoding { + return YES; +} + +- (void)encodeWithCoder:(nonnull NSCoder *)coder { + [coder encodeObject:self.keyID forKey:kKeyIDKey]; + [coder encodeObject:self.artifact forKey:kArtifactKey]; + [coder encodeInteger:self.storageVersion forKey:kStorageVersionKey]; +} + +- (nullable instancetype)initWithCoder:(nonnull NSCoder *)coder { + NSInteger storageVersion = [coder decodeIntegerForKey:kStorageVersionKey]; + + if (storageVersion < kStorageVersion) { + // Handle migration here when new versions are added + } + + // If the version of the stored object is equal or higher than the current version then try the + // best to get enough data to initialize the object. + NSString *keyID = [coder decodeObjectOfClass:[NSString class] forKey:kKeyIDKey]; + if (keyID.length < 1) { + return nil; + } + + NSData *artifact = [coder decodeObjectOfClass:[NSData class] forKey:kArtifactKey]; + if (artifact.length < 1) { + return nil; + } + + return [self initWithKeyID:keyID artifact:artifact]; +} + +@end diff --git a/FirebaseAppCheck/Tests/Unit/AppAttestProvider/FIRAppAttestProviderTests.m b/FirebaseAppCheck/Tests/Unit/AppAttestProvider/FIRAppAttestProviderTests.m index 04c59c61382..f24ea7fe9f3 100644 --- a/FirebaseAppCheck/Tests/Unit/AppAttestProvider/FIRAppAttestProviderTests.m +++ b/FirebaseAppCheck/Tests/Unit/AppAttestProvider/FIRAppAttestProviderTests.m @@ -107,7 +107,7 @@ - (void)testGetTokenWhenAppAttestIsNotSupported { // 2. Don't expect other operations. OCMReject([self.mockStorage getAppAttestKeyID]); OCMReject([self.mockAppAttestService generateKeyWithCompletionHandler:OCMOCK_ANY]); - OCMReject([self.mockArtifactStorage getArtifact]); + OCMReject([self.mockArtifactStorage getArtifactForKey:OCMOCK_ANY]); OCMReject([self.mockAPIService getRandomChallenge]); OCMReject([self.mockStorage setAppAttestKeyID:OCMOCK_ANY]); OCMReject([self.mockAppAttestService attestKey:OCMOCK_ANY @@ -179,7 +179,7 @@ - (void)testGetToken_WhenNoExistingKey_Success { .andReturn([FBLPromise resolvedWith:attestKeyResponse]); // 8. Expect the artifact received from Firebase backend to be saved. - OCMExpect([self.mockArtifactStorage setArtifact:artifactData]) + OCMExpect([self.mockArtifactStorage setArtifact:artifactData forKey:generatedKeyID]) .andReturn([FBLPromise resolvedWith:artifactData]); // 9. Call get token. @@ -221,7 +221,7 @@ - (void)testGetToken_WhenExistingUnregisteredKey_Success { [NSError errorWithDomain:@"testGetToken_WhenExistingUnregisteredKey_Success" code:NSNotFound userInfo:nil]]; - OCMExpect([self.mockArtifactStorage getArtifact]).andReturn(rejectedPromise); + OCMExpect([self.mockArtifactStorage getArtifactForKey:existingKeyID]).andReturn(rejectedPromise); // 6. Expect random challenge to be requested. OCMExpect([self.mockAPIService getRandomChallenge]) @@ -246,7 +246,7 @@ - (void)testGetToken_WhenExistingUnregisteredKey_Success { .andReturn([FBLPromise resolvedWith:attestKeyResponse]); // 9. Expect the artifact received from Firebase backend to be saved. - OCMExpect([self.mockArtifactStorage setArtifact:artifactData]) + OCMExpect([self.mockArtifactStorage setArtifact:artifactData forKey:existingKeyID]) .andReturn([FBLPromise resolvedWith:artifactData]); // 10. Call get token. @@ -282,7 +282,7 @@ - (void)testGetToken_WhenUnregisteredKeyAndRandomChallengeError { [NSError errorWithDomain:@"testGetToken_WhenExistingUnregisteredKey_Success" code:NSNotFound userInfo:nil]]; - OCMExpect([self.mockArtifactStorage getArtifact]).andReturn(rejectedPromise); + OCMExpect([self.mockArtifactStorage getArtifactForKey:existingKeyID]).andReturn(rejectedPromise); // 4. Expect random challenge to be requested. NSError *challengeError = [self expectRandomChallengeRequestError]; @@ -328,7 +328,7 @@ - (void)testGetToken_WhenUnregisteredKeyAndKeyAttestationError { [NSError errorWithDomain:@"testGetToken_WhenExistingUnregisteredKey_Success" code:NSNotFound userInfo:nil]]; - OCMExpect([self.mockArtifactStorage getArtifact]).andReturn(rejectedPromise); + OCMExpect([self.mockArtifactStorage getArtifactForKey:existingKeyID]).andReturn(rejectedPromise); // 4. Expect random challenge to be requested. OCMExpect([self.mockAPIService getRandomChallenge]) @@ -380,7 +380,7 @@ - (void)testGetToken_WhenUnregisteredKeyAndKeyAttestationExchangeError { [NSError errorWithDomain:@"testGetToken_WhenExistingUnregisteredKey_Success" code:NSNotFound userInfo:nil]]; - OCMExpect([self.mockArtifactStorage getArtifact]).andReturn(rejectedPromise); + OCMExpect([self.mockArtifactStorage getArtifactForKey:existingKeyID]).andReturn(rejectedPromise); // 4. Expect random challenge to be requested. OCMExpect([self.mockAPIService getRandomChallenge]) @@ -432,7 +432,7 @@ - (void)testGetToken_WhenKeyRegistered_Success { // 3. Expect a stored artifact to be requested. NSData *storedArtifact = [@"storedArtifact" dataUsingEncoding:NSUTF8StringEncoding]; - OCMExpect([self.mockArtifactStorage getArtifact]) + OCMExpect([self.mockArtifactStorage getArtifactForKey:existingKeyID]) .andReturn([FBLPromise resolvedWith:storedArtifact]); // 4. Expect random challenge to be requested. @@ -484,7 +484,7 @@ - (void)testGetToken_WhenKeyRegisteredAndChallengeRequestError { // 3. Expect a stored artifact to be requested. NSData *storedArtifact = [@"storedArtifact" dataUsingEncoding:NSUTF8StringEncoding]; - OCMExpect([self.mockArtifactStorage getArtifact]) + OCMExpect([self.mockArtifactStorage getArtifactForKey:existingKeyID]) .andReturn([FBLPromise resolvedWith:storedArtifact]); // 4. Expect random challenge to be requested. @@ -528,7 +528,7 @@ - (void)testGetToken_WhenKeyRegisteredAndGenerateAssertionError { // 3. Expect a stored artifact to be requested. NSData *storedArtifact = [@"storedArtifact" dataUsingEncoding:NSUTF8StringEncoding]; - OCMExpect([self.mockArtifactStorage getArtifact]) + OCMExpect([self.mockArtifactStorage getArtifactForKey:existingKeyID]) .andReturn([FBLPromise resolvedWith:storedArtifact]); // 4. Expect random challenge to be requested. @@ -579,7 +579,7 @@ - (void)testGetToken_WhenKeyRegisteredAndTokenExchangeRequestError { // 3. Expect a stored artifact to be requested. NSData *storedArtifact = [@"storedArtifact" dataUsingEncoding:NSUTF8StringEncoding]; - OCMExpect([self.mockArtifactStorage getArtifact]) + OCMExpect([self.mockArtifactStorage getArtifactForKey:existingKeyID]) .andReturn([FBLPromise resolvedWith:storedArtifact]); // 4. Expect random challenge to be requested. diff --git a/FirebaseAppCheck/Tests/Unit/AppAttestProvider/Storage/FIRAppAttestArtifactStorageTests.m b/FirebaseAppCheck/Tests/Unit/AppAttestProvider/Storage/FIRAppAttestArtifactStorageTests.m index 4a9beb429e3..8ee607a0d49 100644 --- a/FirebaseAppCheck/Tests/Unit/AppAttestProvider/Storage/FIRAppAttestArtifactStorageTests.m +++ b/FirebaseAppCheck/Tests/Unit/AppAttestProvider/Storage/FIRAppAttestArtifactStorageTests.m @@ -42,8 +42,6 @@ - (void)setUp { } - (void)tearDown { - // Cleanup the storage. - [self.storage setArtifact:nil]; self.storage = nil; [super tearDown]; } @@ -53,17 +51,19 @@ - (void)testSetAndGetArtifact { } - (void)testRemoveArtifact { + NSString *keyID = [NSUUID UUID].UUIDString; + // 1. Save an artifact to storage and check it is stored. [self assertSetGetForStorage]; // 2. Remove artifact. - __auto_type setPromise = [self.storage setArtifact:nil]; + __auto_type setPromise = [self.storage setArtifact:nil forKey:keyID]; XCTAssert(FBLWaitForPromisesWithTimeout(0.5)); XCTAssertNil(setPromise.value); XCTAssertNil(setPromise.error); // 3. Check it has been removed. - __auto_type getPromise = [self.storage getArtifact]; + __auto_type getPromise = [self.storage getArtifactForKey:keyID]; XCTAssert(FBLWaitForPromisesWithTimeout(0.5)); XCTAssertNil(getPromise.value); XCTAssertNil(getPromise.error); @@ -87,26 +87,67 @@ - (void)testSetAndGetPerApp { appID2:@"app_id_2"]; } +- (void)testSetArtifactForOneKeyGetForAnotherKey { + // Set an artifact for a key. + [self assertSetGetForStorage]; + + // Try to get artifact for a different key. + NSString *keyID = [NSUUID UUID].UUIDString; + __auto_type getPromise = [self.storage getArtifactForKey:keyID]; + XCTAssert(FBLWaitForPromisesWithTimeout(0.5)); + XCTAssertNil(getPromise.value); + XCTAssertNil(getPromise.error); +} + +- (void)testSetArtifactForNewKeyRemovesArtifactForOldKey { + // 1. Store an artifact. + NSString *oldKeyID = [self assertSetGetForStorage]; + + // 2. Replace the artifact. + NSString *newKeyID = [self assertSetGetForStorage]; + XCTAssertNotEqualObjects(oldKeyID, newKeyID); + + // 3. Check old artifact was removed. + __auto_type getPromise = [self.storage getArtifactForKey:oldKeyID]; + XCTAssert(FBLWaitForPromisesWithTimeout(0.5)); + XCTAssertNil(getPromise.value); + XCTAssertNil(getPromise.error); +} + #pragma mark - Helpers -- (void)assertSetGetForStorage { +/// Sets a random artifact for a random key and asserts it can be read. +/// @return The random key ID used to set and get the artifact. +- (NSString *)assertSetGetForStorage { NSData *artifactToSet = [[NSUUID UUID].UUIDString dataUsingEncoding:NSUTF8StringEncoding]; + NSString *keyID = [NSUUID UUID].UUIDString; - __auto_type setPromise = [self.storage setArtifact:artifactToSet]; + __auto_type setPromise = [self.storage setArtifact:artifactToSet forKey:keyID]; XCTAssert(FBLWaitForPromisesWithTimeout(0.5)); XCTAssertEqualObjects(setPromise.value, artifactToSet); XCTAssertNil(setPromise.error); - __auto_type getPromise = [self.storage getArtifact]; + __auto_type getPromise = [self.storage getArtifactForKey:keyID]; XCTAssert(FBLWaitForPromisesWithTimeout(0.5)); XCTAssertEqualObjects(getPromise.value, artifactToSet); XCTAssertNil(getPromise.error); + + __weak __auto_type weakSelf = self; + [self addTeardownBlock:^{ + // Cleanup storage. + [weakSelf.storage setArtifact:nil forKey:keyID]; + XCTAssert(FBLWaitForPromisesWithTimeout(0.5)); + }]; + + return keyID; } - (void)assertIndependentSetGetForStoragesWithAppName1:(NSString *)appName1 appID1:(NSString *)appID1 appName2:(NSString *)appName2 appID2:(NSString *)appID2 { + NSString *keyID = [NSUUID UUID].UUIDString; + // Create two storages. FIRAppAttestArtifactStorage *storage1 = [[FIRAppAttestArtifactStorage alloc] initWithAppName:appName1 appID:appID1 accessGroup:nil]; @@ -114,24 +155,24 @@ - (void)assertIndependentSetGetForStoragesWithAppName1:(NSString *)appName1 [[FIRAppAttestArtifactStorage alloc] initWithAppName:appName2 appID:appID2 accessGroup:nil]; // 1. Independently set artifacts for the two storages. NSData *artifact1 = [@"app_attest_artifact1" dataUsingEncoding:NSUTF8StringEncoding]; - FBLPromise *setPromise1 = [storage1 setArtifact:artifact1]; + FBLPromise *setPromise1 = [storage1 setArtifact:artifact1 forKey:keyID]; XCTAssert(FBLWaitForPromisesWithTimeout(0.5)); XCTAssertEqualObjects(setPromise1.value, artifact1); XCTAssertNil(setPromise1.error); NSData *artifact2 = [@"app_attest_artifact2" dataUsingEncoding:NSUTF8StringEncoding]; - __auto_type setPromise2 = [storage2 setArtifact:artifact2]; + __auto_type setPromise2 = [storage2 setArtifact:artifact2 forKey:keyID]; XCTAssert(FBLWaitForPromisesWithTimeout(0.5)); XCTAssertEqualObjects(setPromise2.value, artifact2); XCTAssertNil(setPromise2.error); // 2. Get artifacts for the two storages. - __auto_type getPromise1 = [storage1 getArtifact]; + __auto_type getPromise1 = [storage1 getArtifactForKey:keyID]; XCTAssert(FBLWaitForPromisesWithTimeout(0.5)); XCTAssertEqualObjects(getPromise1.value, artifact1); XCTAssertNil(getPromise1.error); - __auto_type getPromise2 = [storage2 getArtifact]; + __auto_type getPromise2 = [storage2 getArtifactForKey:keyID]; XCTAssert(FBLWaitForPromisesWithTimeout(0.5)); XCTAssertEqualObjects(getPromise2.value, artifact2); XCTAssertNil(getPromise2.error); @@ -140,8 +181,8 @@ - (void)assertIndependentSetGetForStoragesWithAppName1:(NSString *)appName1 XCTAssertNotEqualObjects(getPromise1.value, getPromise2.value); // Cleanup storages. - [storage1 setArtifact:nil]; - [storage2 setArtifact:nil]; + [storage1 setArtifact:nil forKey:keyID]; + [storage2 setArtifact:nil forKey:keyID]; XCTAssert(FBLWaitForPromisesWithTimeout(0.5)); } From 2b70bf0657cc47927b9e31c8e45fdc3a8bcd2ee2 Mon Sep 17 00:00:00 2001 From: Maksym Malyhin Date: Fri, 21 May 2021 12:34:13 -0400 Subject: [PATCH 12/37] App Check: prevent concurrent token requests (#8117) * App Attest multiple get token method invocation tests * Ensure a single App Attest handshake sequence at the time * FIRAppCheckTests: get token request merging tests * FIRAppCheck: Ensure a single get token operation at the time * formatting * Test new request after merged requests * Release finished operation promise * Style * Typos * typo * Request merging tests for error cases * formatting --- .../AppAttestProvider/FIRAppAttestProvider.m | 25 ++ FirebaseAppCheck/Sources/Core/FIRAppCheck.m | 24 ++ .../FIRAppAttestProviderTests.m | 244 ++++++++++++++---- .../Tests/Unit/Core/FIRAppCheckTests.m | 197 +++++++++++--- 4 files changed, 404 insertions(+), 86 deletions(-) diff --git a/FirebaseAppCheck/Sources/AppAttestProvider/FIRAppAttestProvider.m b/FirebaseAppCheck/Sources/AppAttestProvider/FIRAppAttestProvider.m index aee9a380a44..1f8ce402360 100644 --- a/FirebaseAppCheck/Sources/AppAttestProvider/FIRAppAttestProvider.m +++ b/FirebaseAppCheck/Sources/AppAttestProvider/FIRAppAttestProvider.m @@ -103,6 +103,8 @@ @interface FIRAppAttestProvider () @property(nonatomic, readonly) id keyIDStorage; @property(nonatomic, readonly) id artifactStorage; +@property(nonatomic, nullable) FBLPromise *ongoingGetTokenOperation; + @property(nonatomic, readonly) dispatch_queue_t queue; @end @@ -172,6 +174,29 @@ - (void)getTokenWithCompletion:(void (^)(FIRAppCheckToken *_Nullable, NSError *_ } - (FBLPromise *)getToken { + return [FBLPromise onQueue:self.queue + do:^id _Nullable { + if (self.ongoingGetTokenOperation == nil) { + // Kick off a new handshake sequence only when there is not an ongoing + // handshake to avoid race conditions. + self.ongoingGetTokenOperation = + [self createGetTokenSequencePromise] + + // Release the ongoing operation promise on completion. + .then(^FIRAppCheckToken *(FIRAppCheckToken *token) { + self.ongoingGetTokenOperation = nil; + return token; + }) + .recover(^NSError *(NSError *error) { + self.ongoingGetTokenOperation = nil; + return error; + }); + } + return self.ongoingGetTokenOperation; + }]; +} + +- (FBLPromise *)createGetTokenSequencePromise { // Check attestation state to decide on the next steps. return [self attestationState].thenOn(self.queue, ^id(FIRAppAttestProviderState *attestState) { switch (attestState.state) { diff --git a/FirebaseAppCheck/Sources/Core/FIRAppCheck.m b/FirebaseAppCheck/Sources/Core/FIRAppCheck.m index a3478617226..ea5599c4176 100644 --- a/FirebaseAppCheck/Sources/Core/FIRAppCheck.m +++ b/FirebaseAppCheck/Sources/Core/FIRAppCheck.m @@ -70,6 +70,8 @@ @interface FIRAppCheck () @property(nonatomic, readonly, nullable) id tokenRefresher; +@property(nonatomic, nullable) FBLPromise *ongoingRetrieveOrRefreshTokenPromise; + @end @implementation FIRAppCheck @@ -242,6 +244,28 @@ - (nonnull NSString *)notificationTokenKey { #pragma mark - FAA token cache - (FBLPromise *)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(^FIRAppCheckToken *(FIRAppCheckToken *token) { + self.ongoingRetrieveOrRefreshTokenPromise = nil; + return token; + }) + .recover(^NSError *(NSError *error) { + self.ongoingRetrieveOrRefreshTokenPromise = nil; + return error; + }); + } + return self.ongoingRetrieveOrRefreshTokenPromise; + }]; +} + +- (FBLPromise *)createRetrieveOrRefreshTokenPromiseForcingRefresh: + (BOOL)forcingRefresh { return [self getCachedValidTokenForcingRefresh:forcingRefresh].recover( ^id _Nullable(NSError *_Nonnull error) { return [self refreshToken]; diff --git a/FirebaseAppCheck/Tests/Unit/AppAttestProvider/FIRAppAttestProviderTests.m b/FirebaseAppCheck/Tests/Unit/AppAttestProvider/FIRAppAttestProviderTests.m index f24ea7fe9f3..5e89306c25f 100644 --- a/FirebaseAppCheck/Tests/Unit/AppAttestProvider/FIRAppAttestProviderTests.m +++ b/FirebaseAppCheck/Tests/Unit/AppAttestProvider/FIRAppAttestProviderTests.m @@ -422,55 +422,7 @@ - (void)testGetToken_WhenUnregisteredKeyAndKeyAttestationExchangeError { #pragma mark - FAC token refresh (assertion) - (void)testGetToken_WhenKeyRegistered_Success { - // 1. Expect FIRAppAttestService.isSupported. - [OCMExpect([self.mockAppAttestService isSupported]) andReturnValue:@(YES)]; - - // 2. Expect storage getAppAttestKeyID. - NSString *existingKeyID = @"existingKeyID"; - OCMExpect([self.mockStorage getAppAttestKeyID]) - .andReturn([FBLPromise resolvedWith:existingKeyID]); - - // 3. Expect a stored artifact to be requested. - NSData *storedArtifact = [@"storedArtifact" dataUsingEncoding:NSUTF8StringEncoding]; - OCMExpect([self.mockArtifactStorage getArtifactForKey:existingKeyID]) - .andReturn([FBLPromise resolvedWith:storedArtifact]); - - // 4. Expect random challenge to be requested. - OCMExpect([self.mockAPIService getRandomChallenge]) - .andReturn([FBLPromise resolvedWith:self.randomChallenge]); - - // 5. Expect assertion to be requested. - NSData *assertion = [@"generatedAssertion" dataUsingEncoding:NSUTF8StringEncoding]; - id completionBlockArg = [OCMArg invokeBlockWithArgs:assertion, [NSNull null], nil]; - OCMExpect([self.mockAppAttestService - generateAssertion:existingKeyID - clientDataHash:[self dataHashForAssertionWithArtifactData:storedArtifact] - completionHandler:completionBlockArg]); - - // 6. Expect assertion request to be sent. - FIRAppCheckToken *FACToken = [[FIRAppCheckToken alloc] initWithToken:@"FAC token" - expirationDate:[NSDate date]]; - OCMExpect([self.mockAPIService getAppCheckTokenWithArtifact:storedArtifact - challenge:self.randomChallenge - assertion:assertion]) - .andReturn([FBLPromise resolvedWith:FACToken]); - - // 7. Call get token. - XCTestExpectation *completionExpectation = - [self expectationWithDescription:@"completionExpectation"]; - [self.provider - getTokenWithCompletion:^(FIRAppCheckToken *_Nullable token, NSError *_Nullable error) { - [completionExpectation fulfill]; - - XCTAssertEqualObjects(token.token, FACToken.token); - XCTAssertEqualObjects(token.expirationDate, FACToken.expirationDate); - XCTAssertNil(error); - }]; - - [self waitForExpectations:@[ completionExpectation ] timeout:0.5]; - - // 8. Verify mocks. - [self verifyAllMocks]; + [self assertGetToken_WhenKeyRegistered_Success]; } - (void)testGetToken_WhenKeyRegisteredAndChallengeRequestError { @@ -621,6 +573,148 @@ - (void)testGetToken_WhenKeyRegisteredAndTokenExchangeRequestError { [self verifyAllMocks]; } +#pragma mark - Request merging + +- (void)testGetToken_WhenCalledSeveralTimesSuccess_ThenThereIsOnlyOneOngoingHandshake { + // 1. Expect FIRAppAttestService.isSupported. + [OCMExpect([self.mockAppAttestService isSupported]) andReturnValue:@(YES)]; + + // 2. Expect storage getAppAttestKeyID. + NSString *existingKeyID = @"existingKeyID"; + OCMExpect([self.mockStorage getAppAttestKeyID]) + .andReturn([FBLPromise resolvedWith:existingKeyID]); + + // 3. Expect a stored artifact to be requested. + NSData *storedArtifact = [@"storedArtifact" dataUsingEncoding:NSUTF8StringEncoding]; + OCMExpect([self.mockArtifactStorage getArtifactForKey:existingKeyID]) + .andReturn([FBLPromise resolvedWith:storedArtifact]); + + // 4. Expect random challenge to be requested. + // 4.1. Create a pending promise to fulfill later. + FBLPromise *challengeRequestPromise = [FBLPromise pendingPromise]; + // 4.2. Stub getRandomChallenge method. + OCMExpect([self.mockAPIService getRandomChallenge]).andReturn(challengeRequestPromise); + + // 5. Expect assertion to be requested. + NSData *assertion = [@"generatedAssertion" dataUsingEncoding:NSUTF8StringEncoding]; + id completionBlockArg = [OCMArg invokeBlockWithArgs:assertion, [NSNull null], nil]; + OCMExpect([self.mockAppAttestService + generateAssertion:existingKeyID + clientDataHash:[self dataHashForAssertionWithArtifactData:storedArtifact] + completionHandler:completionBlockArg]); + + // 6. Expect assertion request to be sent. + FIRAppCheckToken *FACToken = [[FIRAppCheckToken alloc] initWithToken:@"FAC token" + expirationDate:[NSDate date]]; + OCMExpect([self.mockAPIService getAppCheckTokenWithArtifact:storedArtifact + challenge:self.randomChallenge + assertion:assertion]) + .andReturn([FBLPromise resolvedWith:FACToken]); + + // 7. Call get token several times. + NSInteger callsCount = 10; + NSMutableArray *completionExpectations = [NSMutableArray arrayWithCapacity:callsCount]; + + for (NSInteger i = 0; i < callsCount; i++) { + // 7.1 Expect the completion to be called for each get token method called. + XCTestExpectation *completionExpectation = [self + expectationWithDescription:[NSString stringWithFormat:@"completionExpectation%@", @(i)]]; + [completionExpectations addObject:completionExpectation]; + + // 7.2. Call get token. + [self.provider + getTokenWithCompletion:^(FIRAppCheckToken *_Nullable token, NSError *_Nullable error) { + [completionExpectation fulfill]; + + XCTAssertEqualObjects(token.token, FACToken.token); + XCTAssertEqualObjects(token.expirationDate, FACToken.expirationDate); + XCTAssertNil(error); + }]; + } + + // 7.3. Resolve get challenge promise to finish the operation. + [challengeRequestPromise fulfill:self.randomChallenge]; + + // 7.4. Wait for all completions to be called. + [self waitForExpectations:completionExpectations timeout:1]; + + // 8. Verify mocks. + [self verifyAllMocks]; + + // 9. Check another get token call after. + [self assertGetToken_WhenKeyRegistered_Success]; +} + +- (void)testGetToken_WhenCalledSeveralTimesError_ThenThereIsOnlyOneOngoingHandshake { + // 1. Expect FIRAppAttestService.isSupported. + [OCMExpect([self.mockAppAttestService isSupported]) andReturnValue:@(YES)]; + + // 2. Expect storage getAppAttestKeyID. + NSString *existingKeyID = @"existingKeyID"; + OCMExpect([self.mockStorage getAppAttestKeyID]) + .andReturn([FBLPromise resolvedWith:existingKeyID]); + + // 3. Expect a stored artifact to be requested. + NSData *storedArtifact = [@"storedArtifact" dataUsingEncoding:NSUTF8StringEncoding]; + OCMExpect([self.mockArtifactStorage getArtifactForKey:existingKeyID]) + .andReturn([FBLPromise resolvedWith:storedArtifact]); + + // 4. Expect random challenge to be requested. + OCMExpect([self.mockAPIService getRandomChallenge]) + .andReturn([FBLPromise resolvedWith:self.randomChallenge]); + + // 5. Don't expect assertion to be requested. + NSData *assertion = [@"generatedAssertion" dataUsingEncoding:NSUTF8StringEncoding]; + id completionBlockArg = [OCMArg invokeBlockWithArgs:assertion, [NSNull null], nil]; + OCMExpect([self.mockAppAttestService + generateAssertion:existingKeyID + clientDataHash:[self dataHashForAssertionWithArtifactData:storedArtifact] + completionHandler:completionBlockArg]); + + // 6. Expect assertion request to be sent. + // 6.1. Create a pending promise to reject later. + FBLPromise *assertionRequestPromise = [FBLPromise pendingPromise]; + // 6.2. Stub assertion request. + OCMExpect([self.mockAPIService getAppCheckTokenWithArtifact:storedArtifact + challenge:self.randomChallenge + assertion:assertion]) + .andReturn(assertionRequestPromise); + // 6.3. Create an expected error to be rejected with later. + NSError *assertionRequestError = [NSError errorWithDomain:self.name code:0 userInfo:nil]; + + // 7. Call get token several times. + NSInteger callsCount = 10; + NSMutableArray *completionExpectations = [NSMutableArray arrayWithCapacity:callsCount]; + + for (NSInteger i = 0; i < callsCount; i++) { + // 7.1 Expect the completion to be called for each get token method called. + XCTestExpectation *completionExpectation = [self + expectationWithDescription:[NSString stringWithFormat:@"completionExpectation%@", @(i)]]; + [completionExpectations addObject:completionExpectation]; + + // 7.2. Call get token. + [self.provider + getTokenWithCompletion:^(FIRAppCheckToken *_Nullable token, NSError *_Nullable error) { + [completionExpectation fulfill]; + + XCTAssertEqualObjects(error, assertionRequestError); + XCTAssertNil(token); + }]; + } + + // 7.3. Reject get challenge promise to finish the operation. + [assertionRequestPromise reject:assertionRequestError]; + + // 7.4. Wait for all completions to be called. + [self waitForExpectations:completionExpectations timeout:1]; + + // 8. Verify mocks. + [self verifyAllMocks]; + + // 9. Check another get token call after. + [self assertGetToken_WhenKeyRegistered_Success]; +} + - (NSData *)dataHashForAssertionWithArtifactData:(NSData *)artifact { NSMutableData *statement = [artifact mutableCopy]; [statement appendData:self.randomChallenge]; @@ -651,6 +745,58 @@ - (void)verifyAllMocks { OCMVerifyAll(self.mockArtifactStorage); } +- (void)assertGetToken_WhenKeyRegistered_Success { + // 1. Expect FIRAppAttestService.isSupported. + [OCMExpect([self.mockAppAttestService isSupported]) andReturnValue:@(YES)]; + + // 2. Expect storage getAppAttestKeyID. + NSString *existingKeyID = [NSUUID UUID].UUIDString; + OCMExpect([self.mockStorage getAppAttestKeyID]) + .andReturn([FBLPromise resolvedWith:existingKeyID]); + + // 3. Expect a stored artifact to be requested. + NSData *storedArtifact = [[NSUUID UUID].UUIDString dataUsingEncoding:NSUTF8StringEncoding]; + OCMExpect([self.mockArtifactStorage getArtifactForKey:existingKeyID]) + .andReturn([FBLPromise resolvedWith:storedArtifact]); + + // 4. Expect random challenge to be requested. + OCMExpect([self.mockAPIService getRandomChallenge]) + .andReturn([FBLPromise resolvedWith:self.randomChallenge]); + + // 5. Expect assertion to be requested. + NSData *assertion = [[NSUUID UUID].UUIDString dataUsingEncoding:NSUTF8StringEncoding]; + id completionBlockArg = [OCMArg invokeBlockWithArgs:assertion, [NSNull null], nil]; + OCMExpect([self.mockAppAttestService + generateAssertion:existingKeyID + clientDataHash:[self dataHashForAssertionWithArtifactData:storedArtifact] + completionHandler:completionBlockArg]); + + // 6. Expect assertion request to be sent. + FIRAppCheckToken *FACToken = [[FIRAppCheckToken alloc] initWithToken:[NSUUID UUID].UUIDString + expirationDate:[NSDate date]]; + OCMExpect([self.mockAPIService getAppCheckTokenWithArtifact:storedArtifact + challenge:self.randomChallenge + assertion:assertion]) + .andReturn([FBLPromise resolvedWith:FACToken]); + + // 7. Call get token. + XCTestExpectation *completionExpectation = + [self expectationWithDescription:@"completionExpectation"]; + [self.provider + getTokenWithCompletion:^(FIRAppCheckToken *_Nullable token, NSError *_Nullable error) { + [completionExpectation fulfill]; + + XCTAssertEqualObjects(token.token, FACToken.token); + XCTAssertEqualObjects(token.expirationDate, FACToken.expirationDate); + XCTAssertNil(error); + }]; + + [self waitForExpectations:@[ completionExpectation ] timeout:0.5]; + + // 8. Verify mocks. + [self verifyAllMocks]; +} + @end #endif // TARGET_OS_IOS diff --git a/FirebaseAppCheck/Tests/Unit/Core/FIRAppCheckTests.m b/FirebaseAppCheck/Tests/Unit/Core/FIRAppCheckTests.m index 61699b20a94..5e0458ab31d 100644 --- a/FirebaseAppCheck/Tests/Unit/Core/FIRAppCheckTests.m +++ b/FirebaseAppCheck/Tests/Unit/Core/FIRAppCheckTests.m @@ -35,6 +35,9 @@ #import "FirebaseCore/Sources/Private/FirebaseCoreInternal.h" +// The FAC token value returned when an error occurs. +static NSString *const kDummyToken = @"eyJlcnJvciI6IlVOS05PV05fRVJST1IifQ=="; + @interface FIRAppCheck (Tests) - (instancetype)initWithAppName:(NSString *)appName appCheckProvider:(id)appCheckProvider @@ -235,33 +238,7 @@ - (void)testGetToken_WhenNoCache_Success { } - (void)testGetToken_WhenCachedTokenIsValid_Success { - FIRAppCheckToken *cachedToken = [[FIRAppCheckToken alloc] initWithToken:@"valid" - expirationDate:[NSDate distantFuture]]; - - // 1. Expect token to be requested from storage. - OCMExpect([self.mockStorage getToken]).andReturn([FBLPromise resolvedWith:cachedToken]); - - // 2. Don't expect token requested from app check provider. - OCMReject([self.mockAppCheckProvider getTokenWithCompletion:[OCMArg any]]); - - // 3. Don't expect token update notification to be sent. - XCTestExpectation *notificationExpectation = [self tokenUpdateNotificationWithExpectedToken:@""]; - notificationExpectation.inverted = YES; - - // 4. Request token. - XCTestExpectation *getTokenExpectation = [self expectationWithDescription:@"getToken"]; - [self.appCheck getTokenForcingRefresh:NO - completion:^(id tokenResult) { - [getTokenExpectation fulfill]; - XCTAssertNotNil(tokenResult); - XCTAssertEqualObjects(tokenResult.token, cachedToken.token); - XCTAssertNil(tokenResult.error); - }]; - - // 5. Wait for expectations and validate mocks. - [self waitForExpectations:@[ notificationExpectation, getTokenExpectation ] timeout:0.5]; - OCMVerifyAll(self.mockStorage); - OCMVerifyAll(self.mockAppCheckProvider); + [self assertGetToken_WhenCachedTokenIsValid_Success]; } - (void)testGetTokenForcingRefresh_WhenCachedTokenIsValid_Success { @@ -358,18 +335,17 @@ - (void)testGetToken_AppCheckProviderError { // 5. Request token. XCTestExpectation *getTokenExpectation = [self expectationWithDescription:@"getToken"]; - [self.appCheck - getTokenForcingRefresh:NO - completion:^(id result) { - [getTokenExpectation fulfill]; + [self.appCheck getTokenForcingRefresh:NO + completion:^(id result) { + [getTokenExpectation fulfill]; - XCTAssertNotNil(result); - XCTAssertEqualObjects(result.token, @"eyJlcnJvciI6IlVOS05PV05fRVJST1IifQ=="); + XCTAssertNotNil(result); + XCTAssertEqualObjects(result.token, kDummyToken); - // TODO: Expect a public domain error to be returned - not the - // internal one. - XCTAssertEqualObjects(result.error, providerError); - }]; + // TODO: Expect a public domain error to be returned - not the + // internal one. + XCTAssertEqualObjects(result.error, providerError); + }]; // 6. Wait for expectations and validate mocks. [self waitForExpectations:@[ notificationExpectation, getTokenExpectation ] timeout:0.5]; @@ -483,6 +459,123 @@ - (void)testSetIsTokenAutoRefreshEnabled { OCMVerifyAll(self.mockSettings); } +#pragma mark - Merging multiple get token requests + +- (void)testGetToken_WhenCalledSeveralTimesSuccess_ThenThereIsOnlyOneOperation { + // 1. Expect token to be requested from storage. + OCMExpect([self.mockStorage getToken]).andReturn([FBLPromise resolvedWith:nil]); + + // 2. Expect token requested from app check provider. + FIRAppCheckToken *tokenToReturn = [[FIRAppCheckToken alloc] initWithToken:[NSUUID UUID].UUIDString + expirationDate:[NSDate distantFuture]]; + id completionArg = [OCMArg invokeBlockWithArgs:tokenToReturn, [NSNull null], nil]; + OCMExpect([self.mockAppCheckProvider getTokenWithCompletion:completionArg]); + + // 3. Expect new token to be stored. + // 3.1. Create a pending promise to resolve later. + FBLPromise *storeTokenPromise = [FBLPromise pendingPromise]; + // 3.2. Stub storage set token method. + OCMExpect([self.mockStorage setToken:tokenToReturn]).andReturn(storeTokenPromise); + + // 4. Expect token update notification to be sent. + XCTestExpectation *notificationExpectation = + [self tokenUpdateNotificationWithExpectedToken:tokenToReturn.token]; + + // 5. Request token several times. + NSInteger getTokenCallsCount = 10; + NSMutableArray *getTokenCompletionExpectations = + [NSMutableArray arrayWithCapacity:getTokenCallsCount]; + + for (NSInteger i = 0; i < getTokenCallsCount; i++) { + // 5.1. Expect a completion to be called for each method call. + XCTestExpectation *getTokenExpectation = + [self expectationWithDescription:[NSString stringWithFormat:@"getToken%@", @(i)]]; + [getTokenCompletionExpectations addObject:getTokenExpectation]; + + // 5.2. Call get token. + [self.appCheck getTokenForcingRefresh:NO + completion:^(id tokenResult) { + [getTokenExpectation fulfill]; + + XCTAssertNotNil(tokenResult); + XCTAssertEqualObjects(tokenResult.token, tokenToReturn.token); + XCTAssertNil(tokenResult.error); + }]; + } + + // 5.3. Fulfill the pending promise to finish the get token operation. + [storeTokenPromise fulfill:tokenToReturn]; + + // 6. Wait for expectations and validate mocks. + [self waitForExpectations:[getTokenCompletionExpectations + arrayByAddingObject:notificationExpectation] + timeout:0.5]; + OCMVerifyAll(self.mockStorage); + OCMVerifyAll(self.mockAppCheckProvider); + + // 7. Check a get token call after. + [self assertGetToken_WhenCachedTokenIsValid_Success]; +} + +- (void)testGetToken_WhenCalledSeveralTimesError_ThenThereIsOnlyOneOperation { + // 1. Expect token to be requested from storage. + OCMExpect([self.mockStorage getToken]).andReturn([FBLPromise resolvedWith:nil]); + + // 2. Expect token requested from app check provider. + FIRAppCheckToken *tokenToReturn = [[FIRAppCheckToken alloc] initWithToken:[NSUUID UUID].UUIDString + expirationDate:[NSDate distantFuture]]; + id completionArg = [OCMArg invokeBlockWithArgs:tokenToReturn, [NSNull null], nil]; + OCMExpect([self.mockAppCheckProvider getTokenWithCompletion:completionArg]); + + // 3. Expect new token to be stored. + // 3.1. Create a pending promise to resolve later. + FBLPromise *storeTokenPromise = [FBLPromise pendingPromise]; + // 3.2. Stub storage set token method. + OCMExpect([self.mockStorage setToken:tokenToReturn]).andReturn(storeTokenPromise); + // 3.3. Create an expected error to be rejected with later. + NSError *storageError = [NSError errorWithDomain:self.name code:0 userInfo:nil]; + + // 4. Don't expect token update notification to be sent. + XCTestExpectation *notificationExpectation = + [self tokenUpdateNotificationWithExpectedToken:tokenToReturn.token]; + notificationExpectation.inverted = YES; + + // 5. Request token several times. + NSInteger getTokenCallsCount = 10; + NSMutableArray *getTokenCompletionExpectations = + [NSMutableArray arrayWithCapacity:getTokenCallsCount]; + + for (NSInteger i = 0; i < getTokenCallsCount; i++) { + // 5.1. Expect a completion to be called for each method call. + XCTestExpectation *getTokenExpectation = + [self expectationWithDescription:[NSString stringWithFormat:@"getToken%@", @(i)]]; + [getTokenCompletionExpectations addObject:getTokenExpectation]; + + // 5.2. Call get token. + [self.appCheck getTokenForcingRefresh:NO + completion:^(id tokenResult) { + [getTokenExpectation fulfill]; + + XCTAssertNotNil(tokenResult); + XCTAssertEqualObjects(tokenResult.error, storageError); + XCTAssertEqualObjects(tokenResult.token, kDummyToken); + }]; + } + + // 5.3. Reject the pending promise to finish the get token operation. + [storeTokenPromise reject:storageError]; + + // 6. Wait for expectations and validate mocks. + [self waitForExpectations:[getTokenCompletionExpectations + arrayByAddingObject:notificationExpectation] + timeout:0.5]; + OCMVerifyAll(self.mockStorage); + OCMVerifyAll(self.mockAppCheckProvider); + + // 7. Check a get token call after. + [self assertGetToken_WhenCachedTokenIsValid_Success]; +} + #pragma mark - Helpers - (void)stubSetTokenRefreshHandler { @@ -512,4 +605,34 @@ - (XCTestExpectation *)tokenUpdateNotificationWithExpectedToken:(NSString *)expe return expectation; } +- (void)assertGetToken_WhenCachedTokenIsValid_Success { + FIRAppCheckToken *cachedToken = [[FIRAppCheckToken alloc] initWithToken:[NSUUID UUID].UUIDString + expirationDate:[NSDate distantFuture]]; + + // 1. Expect token to be requested from storage. + OCMExpect([self.mockStorage getToken]).andReturn([FBLPromise resolvedWith:cachedToken]); + + // 2. Don't expect token requested from app check provider. + OCMReject([self.mockAppCheckProvider getTokenWithCompletion:[OCMArg any]]); + + // 3. Don't expect token update notification to be sent. + XCTestExpectation *notificationExpectation = [self tokenUpdateNotificationWithExpectedToken:@""]; + notificationExpectation.inverted = YES; + + // 4. Request token. + XCTestExpectation *getTokenExpectation = [self expectationWithDescription:@"getToken"]; + [self.appCheck getTokenForcingRefresh:NO + completion:^(id tokenResult) { + [getTokenExpectation fulfill]; + XCTAssertNotNil(tokenResult); + XCTAssertEqualObjects(tokenResult.token, cachedToken.token); + XCTAssertNil(tokenResult.error); + }]; + + // 5. Wait for expectations and validate mocks. + [self waitForExpectations:@[ notificationExpectation, getTokenExpectation ] timeout:0.5]; + OCMVerifyAll(self.mockStorage); + OCMVerifyAll(self.mockAppCheckProvider); +} + @end From 21021875507c6698679dbb2704ac46849214c68e Mon Sep 17 00:00:00 2001 From: Maksym Malyhin Date: Fri, 21 May 2021 12:59:32 -0400 Subject: [PATCH 13/37] Changelog --- FirebaseAppCheck/CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/FirebaseAppCheck/CHANGELOG.md b/FirebaseAppCheck/CHANGELOG.md index bdabac1d9d6..f63a3726530 100644 --- a/FirebaseAppCheck/CHANGELOG.md +++ b/FirebaseAppCheck/CHANGELOG.md @@ -1,2 +1,4 @@ +# 8.1.0 -- M97 +- [added] Apple's App Attest attestation provider support. (#8133) # v8.0.0 -- M95 - [added] Firebase abuse reduction support SDK. (#7928, #7937, #7948) \ No newline at end of file From b8f80f31304508ded7c6c425b3c9d6ce7456a6ce Mon Sep 17 00:00:00 2001 From: Maksym Malyhin Date: Tue, 1 Jun 2021 09:27:51 -0400 Subject: [PATCH 14/37] App Check App Attest: handle attestation rejection (#8170) * Remove/update outdated TODOs * [WIP] Attestation rejection handling draft * style * retry tests draft * reset key ID before retry * Reset attestation * test error and fixes * style * More details in the name * Some debug logging * style * Use specific codes for log messages * style --- .../API/FIRAppAttestAPIService.m | 2 - .../Errors/FIRAppAttestRejectionError.h | 27 +++ .../Errors/FIRAppAttestRejectionError.m | 29 +++ .../AppAttestProvider/FIRAppAttestProvider.m | 111 ++++++++---- .../Core/APIService/FIRAppCheckAPIService.m | 7 +- .../Core/Errors/FIRAppCheckErrorUtil.h | 6 +- .../Core/Errors/FIRAppCheckErrorUtil.m | 13 +- .../Core/Errors/FIRAppCheckHTTPError.h | 32 ++++ .../Core/Errors/FIRAppCheckHTTPError.m | 74 ++++++++ FirebaseAppCheck/Sources/Core/FIRAppCheck.m | 4 +- .../Sources/Core/FIRAppCheckLogger.h | 26 ++- .../Sources/Core/FIRAppCheckLogger.m | 35 ++++ .../DebugProvider/FIRAppCheckDebugProvider.m | 6 +- .../FIRAppCheckDebugProviderFactory.m | 2 +- .../FIRDeviceCheckProvider.m | 3 +- .../FIRAppAttestAPIServiceTests.m | 6 +- .../FIRAppAttestProviderTests.m | 170 +++++++++++++++++- .../Tests/Unit/Core/FIRAppCheckTests.m | 4 +- 18 files changed, 496 insertions(+), 61 deletions(-) create mode 100644 FirebaseAppCheck/Sources/AppAttestProvider/Errors/FIRAppAttestRejectionError.h create mode 100644 FirebaseAppCheck/Sources/AppAttestProvider/Errors/FIRAppAttestRejectionError.m create mode 100644 FirebaseAppCheck/Sources/Core/Errors/FIRAppCheckHTTPError.h create mode 100644 FirebaseAppCheck/Sources/Core/Errors/FIRAppCheckHTTPError.m diff --git a/FirebaseAppCheck/Sources/AppAttestProvider/API/FIRAppAttestAPIService.m b/FirebaseAppCheck/Sources/AppAttestProvider/API/FIRAppAttestAPIService.m index 6f66048265b..1c1c09f7b0d 100644 --- a/FirebaseAppCheck/Sources/AppAttestProvider/API/FIRAppAttestAPIService.m +++ b/FirebaseAppCheck/Sources/AppAttestProvider/API/FIRAppAttestAPIService.m @@ -30,7 +30,6 @@ NS_ASSUME_NONNULL_BEGIN -// TODO: Verify the following request fields. static NSString *const kRequestFieldArtifact = @"artifact"; static NSString *const kRequestFieldAssertion = @"assertion"; static NSString *const kRequestFieldAttestation = @"attestation_statement"; @@ -237,7 +236,6 @@ - (nullable NSData *)randomChallengeFromResponseBody:(NSData *)response error:(N #pragma mark - Helpers - (NSString *)base64StringWithData:(NSData *)data { - // TODO: Need to encode in base64URL? return [data base64EncodedStringWithOptions:0]; } diff --git a/FirebaseAppCheck/Sources/AppAttestProvider/Errors/FIRAppAttestRejectionError.h b/FirebaseAppCheck/Sources/AppAttestProvider/Errors/FIRAppAttestRejectionError.h new file mode 100644 index 00000000000..3718f1c8718 --- /dev/null +++ b/FirebaseAppCheck/Sources/AppAttestProvider/Errors/FIRAppAttestRejectionError.h @@ -0,0 +1,27 @@ +/* + * Copyright 2021 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 + +NS_ASSUME_NONNULL_BEGIN + +@interface FIRAppAttestRejectionError : NSError + +- (instancetype)init; + +@end + +NS_ASSUME_NONNULL_END diff --git a/FirebaseAppCheck/Sources/AppAttestProvider/Errors/FIRAppAttestRejectionError.m b/FirebaseAppCheck/Sources/AppAttestProvider/Errors/FIRAppAttestRejectionError.m new file mode 100644 index 00000000000..2e4ee1fb7ff --- /dev/null +++ b/FirebaseAppCheck/Sources/AppAttestProvider/Errors/FIRAppAttestRejectionError.m @@ -0,0 +1,29 @@ +/* + * Copyright 2021 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 "FirebaseAppCheck/Sources/AppAttestProvider/Errors/FIRAppAttestRejectionError.h" + +#import "FirebaseAppCheck/Sources/Core/Errors/FIRAppCheckErrorUtil.h" + +@implementation FIRAppAttestRejectionError + +- (instancetype)init { + return [self initWithDomain:kFIRAppCheckErrorDomain + code:FIRAppCheckErrorCodeUnknown + userInfo:nil]; +} + +@end diff --git a/FirebaseAppCheck/Sources/AppAttestProvider/FIRAppAttestProvider.m b/FirebaseAppCheck/Sources/AppAttestProvider/FIRAppAttestProvider.m index 1f8ce402360..ef4621bc813 100644 --- a/FirebaseAppCheck/Sources/AppAttestProvider/FIRAppAttestProvider.m +++ b/FirebaseAppCheck/Sources/AppAttestProvider/FIRAppAttestProvider.m @@ -31,9 +31,14 @@ #import "FirebaseAppCheck/Sources/AppAttestProvider/Storage/FIRAppAttestArtifactStorage.h" #import "FirebaseAppCheck/Sources/AppAttestProvider/Storage/FIRAppAttestKeyIDStorage.h" #import "FirebaseAppCheck/Sources/Core/APIService/FIRAppCheckAPIService.h" -#import "FirebaseAppCheck/Sources/Core/Errors/FIRAppCheckErrorUtil.h" +#import "FirebaseAppCheck/Sources/Core/FIRAppCheckLogger.h" + #import "FirebaseAppCheck/Sources/Core/Utils/FIRAppCheckCryptoUtils.h" +#import "FirebaseAppCheck/Sources/AppAttestProvider/Errors/FIRAppAttestRejectionError.h" +#import "FirebaseAppCheck/Sources/Core/Errors/FIRAppCheckErrorUtil.h" +#import "FirebaseAppCheck/Sources/Core/Errors/FIRAppCheckHTTPError.h" + #import "FirebaseCore/Sources/Private/FirebaseCoreInternal.h" NS_ASSUME_NONNULL_BEGIN @@ -201,6 +206,8 @@ - (void)getTokenWithCompletion:(void (^)(FIRAppCheckToken *_Nullable, NSError *_ return [self attestationState].thenOn(self.queue, ^id(FIRAppAttestProviderState *attestState) { switch (attestState.state) { case FIRAppAttestAttestationStateUnsupported: + FIRAppCheckDebugLog(kFIRLoggerAppCheckMessageCodeAppAttestNotSupported, + @"App Attest is not supported."); return attestState.appAttestUnsupportedError; break; @@ -222,37 +229,19 @@ - (void)getTokenWithCompletion:(void (^)(FIRAppCheckToken *_Nullable, NSError *_ #pragma mark - Initial handshake sequence (attestation) - (FBLPromise *)initialHandshakeWithKeyID:(nullable NSString *)keyID { - // 1. Request a random challenge and get App Attest key ID concurrently. + // 1. Attest the device. Retry once on 403 from Firebase backend (attestation rejection error). + __block NSString *keyIDForAttempt = keyID; return [FBLPromise onQueue:self.queue - all:@[ - // 1.1. Request random challenge. - [self.APIService getRandomChallenge], - // 1.2. Get App Attest key ID. - [self generateAppAttestKeyIDIfNeeded:keyID] - ]] - .thenOn(self.queue, - ^FBLPromise *(NSArray *challengeAndKeyID) { - // 2. Attest the key. - NSData *challenge = challengeAndKeyID.firstObject; - NSString *keyID = challengeAndKeyID.lastObject; - - return [self attestKey:keyID challenge:challenge]; - }) - // TODO: Handle a possible key rejection - generate another key. - .thenOn(self.queue, - ^FBLPromise *(FIRAppAttestKeyAttestationResult *result) { - // 3. Exchange the attestation to FAC token and pass the results to the next step. - NSArray *attestationResults = @[ - // 3.1. Just pass the attestation result to the next step. - [FBLPromise resolvedWith:result], - // 3.2. Exchange the attestation to FAC token. - [self.APIService attestKeyWithAttestation:result.attestation - keyID:result.keyID - challenge:result.challenge] - ]; - - return [FBLPromise onQueue:self.queue all:attestationResults]; - }) + attempts:1 + delay:0 + condition:^BOOL(NSInteger attemptCount, NSError *_Nonnull error) { + // Reset keyID before retrying. + keyIDForAttempt = nil; + return [error isKindOfClass:[FIRAppAttestRejectionError class]]; + } + retry:^FBLPromise *_Nullable { + return [self attestKeyGenerateIfNeededWithID:keyIDForAttempt]; + }] .thenOn(self.queue, ^FBLPromise *(NSArray *attestationResults) { // 4. Save the artifact and return the received FAC token. @@ -299,6 +288,66 @@ - (void)getTokenWithCompletion:(void (^)(FIRAppCheckToken *_Nullable, NSError *_ }); } +- (FBLPromise *)attestKeyGenerateIfNeededWithID: + (nullable NSString *)keyID { + // 1. Request a random challenge and get App Attest key ID concurrently. + return [FBLPromise onQueue:self.queue + all:@[ + // 1.1. Request random challenge. + [self.APIService getRandomChallenge], + // 1.2. Get App Attest key ID. + [self generateAppAttestKeyIDIfNeeded:keyID] + ]] + .thenOn(self.queue, + ^FBLPromise *(NSArray *challengeAndKeyID) { + // 2. Attest the key. + NSData *challenge = challengeAndKeyID.firstObject; + NSString *keyID = challengeAndKeyID.lastObject; + + return [self attestKey:keyID challenge:challenge]; + }) + .thenOn(self.queue, + ^FBLPromise *(FIRAppAttestKeyAttestationResult *result) { + // 3. Exchange the attestation to FAC token and pass the results to the next step. + NSArray *attestationResults = @[ + // 3.1. Just pass the attestation result to the next step. + [FBLPromise resolvedWith:result], + // 3.2. Exchange the attestation to FAC token. + [self.APIService attestKeyWithAttestation:result.attestation + keyID:result.keyID + challenge:result.challenge] + ]; + + return [FBLPromise onQueue:self.queue all:attestationResults]; + }) + .recoverOn(self.queue, ^id(NSError *error) { + // If App Attest attestation was rejected then reset the attestation and throw a specific + // error. + FIRAppCheckHTTPError *HTTPError = (FIRAppCheckHTTPError *)error; + if ([HTTPError isKindOfClass:[FIRAppCheckHTTPError class]] && + HTTPError.HTTPResponse.statusCode == 403) { + FIRAppCheckDebugLog(kFIRLoggerAppCheckMessageCodeAttestationRejected, + @"App Attest attestation was rejected by backend. The existing " + @"attestation will be reset."); + // Reset the attestation. + return [self resetAttestation].thenOn(self.queue, ^NSError *(id result) { + // Throw the rejection error. + return [[FIRAppAttestRejectionError alloc] init]; + }); + } + + // Otherwise just re-throw the error. + return error; + }); +} + +/// Resets stored key ID and attestation artifact. +- (FBLPromise *)resetAttestation { + return [self.keyIDStorage setAppAttestKeyID:nil].thenOn(self.queue, ^id(id result) { + return [self.artifactStorage setArtifact:nil forKey:@""]; + }); +} + #pragma mark - Token refresh sequence (assertion) - (FBLPromise *)refreshTokenWithKeyID:(NSString *)keyID diff --git a/FirebaseAppCheck/Sources/Core/APIService/FIRAppCheckAPIService.m b/FirebaseAppCheck/Sources/Core/APIService/FIRAppCheckAPIService.m index a5bd36c5579..5523cf70752 100644 --- a/FirebaseAppCheck/Sources/Core/APIService/FIRAppCheckAPIService.m +++ b/FirebaseAppCheck/Sources/Core/APIService/FIRAppCheckAPIService.m @@ -143,9 +143,10 @@ - (instancetype)initWithURLSession:(NSURLSession *)session NSInteger statusCode = response.HTTPResponse.statusCode; return [FBLPromise do:^id _Nullable { if (statusCode < 200 || statusCode >= 300) { - FIRLogDebug(kFIRLoggerAppCheck, kFIRLoggerAppCheckMessageCodeUnknown, - @"Unexpected API response: %@, body: %@.", response.HTTPResponse, - [[NSString alloc] initWithData:response.HTTPBody encoding:NSUTF8StringEncoding]); + FIRAppCheckDebugLog(kFIRLoggerAppCheckMessageCodeUnexpectedHTTPCode, + @"Unexpected API response: %@, body: %@.", response.HTTPResponse, + [[NSString alloc] initWithData:response.HTTPBody + encoding:NSUTF8StringEncoding]); return [FIRAppCheckErrorUtil APIErrorWithHTTPResponse:response.HTTPResponse data:response.HTTPBody]; } diff --git a/FirebaseAppCheck/Sources/Core/Errors/FIRAppCheckErrorUtil.h b/FirebaseAppCheck/Sources/Core/Errors/FIRAppCheckErrorUtil.h index 5e7fbc6ef3b..07c3005edaa 100644 --- a/FirebaseAppCheck/Sources/Core/Errors/FIRAppCheckErrorUtil.h +++ b/FirebaseAppCheck/Sources/Core/Errors/FIRAppCheckErrorUtil.h @@ -16,6 +16,8 @@ #import +@class FIRAppCheckHTTPError; + NS_ASSUME_NONNULL_BEGIN FOUNDATION_EXTERN NSErrorDomain const kFIRAppCheckErrorDomain NS_SWIFT_NAME(AppCheckErrorDomain); @@ -29,8 +31,8 @@ void FIRAppCheckSetErrorToPointer(NSError *error, NSError **pointer); + (NSError *)cachedTokenNotFound; + (NSError *)cachedTokenExpired; -+ (NSError *)APIErrorWithHTTPResponse:(NSHTTPURLResponse *)HTTPResponse - data:(nullable NSData *)data; ++ (FIRAppCheckHTTPError *)APIErrorWithHTTPResponse:(NSHTTPURLResponse *)HTTPResponse + data:(nullable NSData *)data; + (NSError *)APIErrorWithNetworkError:(NSError *)networkError; diff --git a/FirebaseAppCheck/Sources/Core/Errors/FIRAppCheckErrorUtil.m b/FirebaseAppCheck/Sources/Core/Errors/FIRAppCheckErrorUtil.m index 755b4d187bc..899c61961a9 100644 --- a/FirebaseAppCheck/Sources/Core/Errors/FIRAppCheckErrorUtil.m +++ b/FirebaseAppCheck/Sources/Core/Errors/FIRAppCheckErrorUtil.m @@ -15,6 +15,7 @@ */ #import "FirebaseAppCheck/Sources/Core/Errors/FIRAppCheckErrorUtil.h" +#import "FirebaseAppCheck/Sources/Core/Errors/FIRAppCheckHTTPError.h" NSString *const kFIRAppCheckErrorDomain = @"com.firebase.appCheck"; @@ -34,15 +35,9 @@ + (NSError *)cachedTokenExpired { underlyingError:nil]; } -+ (NSError *)APIErrorWithHTTPResponse:(NSHTTPURLResponse *)HTTPResponse - data:(nullable NSData *)data { - NSString *body = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] ?: @""; - NSString *failureReason = - [NSString stringWithFormat:@"Unexpected API response. HTTP code: %ld, body: \n%@", - (long)HTTPResponse.statusCode, body]; - return [self appCheckErrorWithCode:FIRAppCheckErrorCodeUnknown - failureReason:failureReason - underlyingError:nil]; ++ (FIRAppCheckHTTPError *)APIErrorWithHTTPResponse:(NSHTTPURLResponse *)HTTPResponse + data:(nullable NSData *)data { + return [[FIRAppCheckHTTPError alloc] initWithHTTPResponse:HTTPResponse data:data]; } + (NSError *)APIErrorWithNetworkError:(NSError *)networkError { diff --git a/FirebaseAppCheck/Sources/Core/Errors/FIRAppCheckHTTPError.h b/FirebaseAppCheck/Sources/Core/Errors/FIRAppCheckHTTPError.h new file mode 100644 index 00000000000..b224b69ff34 --- /dev/null +++ b/FirebaseAppCheck/Sources/Core/Errors/FIRAppCheckHTTPError.h @@ -0,0 +1,32 @@ +/* + * Copyright 2021 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 + +NS_ASSUME_NONNULL_BEGIN + +@interface FIRAppCheckHTTPError : NSError + +@property(nonatomic, readonly) NSHTTPURLResponse *HTTPResponse; +@property(nonatomic, readonly, nonnull) NSData *data; + +- (instancetype)init NS_UNAVAILABLE; + +- (instancetype)initWithHTTPResponse:(NSHTTPURLResponse *)HTTPResponse data:(nullable NSData *)data; + +@end + +NS_ASSUME_NONNULL_END diff --git a/FirebaseAppCheck/Sources/Core/Errors/FIRAppCheckHTTPError.m b/FirebaseAppCheck/Sources/Core/Errors/FIRAppCheckHTTPError.m new file mode 100644 index 00000000000..958e7f5c6f3 --- /dev/null +++ b/FirebaseAppCheck/Sources/Core/Errors/FIRAppCheckHTTPError.m @@ -0,0 +1,74 @@ +/* + * Copyright 2021 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 "FirebaseAppCheck/Sources/Core/Errors/FIRAppCheckHTTPError.h" + +#import "FirebaseAppCheck/Sources/Core/Errors/FIRAppCheckErrorUtil.h" + +@implementation FIRAppCheckHTTPError + +- (instancetype)initWithHTTPResponse:(NSHTTPURLResponse *)HTTPResponse + data:(nullable NSData *)data { + NSDictionary *userInfo = [[self class] userInfoWithHTTPResponse:HTTPResponse data:data]; + self = [super initWithDomain:kFIRAppCheckErrorDomain + code:FIRAppCheckErrorCodeUnknown + userInfo:userInfo]; + if (self) { + _HTTPResponse = HTTPResponse; + _data = data; + } + return self; +} + ++ (NSDictionary *)userInfoWithHTTPResponse:(NSHTTPURLResponse *)HTTPResponse + data:(nullable NSData *)data { + NSString *responseString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + NSString *failureReason = + [NSString stringWithFormat:@"The server responded with an error: \n - URL: %@ \n - HTTP " + @"status code: %ld \n - Response body: %@", + HTTPResponse.URL, (long)HTTPResponse.statusCode, responseString]; + return @{NSLocalizedFailureReasonErrorKey : failureReason}; +} + +#pragma mark - NSCopying + +- (id)copyWithZone:(NSZone *)zone { + return [[[self class] alloc] initWithHTTPResponse:self.HTTPResponse data:self.data]; +} + +#pragma mark - NSSecureCoding + +- (nullable instancetype)initWithCoder:(NSCoder *)coder { + NSHTTPURLResponse *HTTPResponse = [coder decodeObjectOfClass:[NSHTTPURLResponse class] + forKey:@"HTTPResponse"]; + if (!HTTPResponse) { + return nil; + } + NSData *data = [coder decodeObjectOfClass:[NSData class] forKey:@"data"]; + + return [self initWithHTTPResponse:HTTPResponse data:data]; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + [coder encodeObject:self.HTTPResponse forKey:@"HTTPResponse"]; + [coder encodeObject:self.data forKey:@"data"]; +} + ++ (BOOL)supportsSecureCoding { + return YES; +} + +@end diff --git a/FirebaseAppCheck/Sources/Core/FIRAppCheck.m b/FirebaseAppCheck/Sources/Core/FIRAppCheck.m index ea5599c4176..99580e2047d 100644 --- a/FirebaseAppCheck/Sources/Core/FIRAppCheck.m +++ b/FirebaseAppCheck/Sources/Core/FIRAppCheck.m @@ -103,7 +103,7 @@ - (nullable instancetype)initWithApp:(FIRApp *)app { id providerFactory = [FIRAppCheck providerFactory]; if (providerFactory == nil) { - FIRLogError(kFIRLoggerAppCheck, kFIRLoggerAppCheckMessageCodeUnknown, + FIRLogError(kFIRLoggerAppCheck, kFIRLoggerAppCheckMessageCodeProviderFactoryIsMissing, @"Cannot instantiate `FIRAppCheck` for app: %@ without a provider factory. " @"Please register a provider factory using " @"`AppCheck.setAppCheckProviderFactory(_ ,forAppName:)` method.", @@ -113,7 +113,7 @@ - (nullable instancetype)initWithApp:(FIRApp *)app { id appCheckProvider = [providerFactory createProviderWithApp:app]; if (appCheckProvider == nil) { - FIRLogError(kFIRLoggerAppCheck, kFIRLoggerAppCheckMessageCodeUnknown, + FIRLogError(kFIRLoggerAppCheck, kFIRLoggerAppCheckMessageCodeProviderIsMissing, @"Cannot instantiate `FIRAppCheck` for app: %@ without an app check provider. " @"Please make sure the provide factory returns a valid app check provider.", app.name); diff --git a/FirebaseAppCheck/Sources/Core/FIRAppCheckLogger.h b/FirebaseAppCheck/Sources/Core/FIRAppCheckLogger.h index 3fb7b6ce9dc..70383889576 100644 --- a/FirebaseAppCheck/Sources/Core/FIRAppCheckLogger.h +++ b/FirebaseAppCheck/Sources/Core/FIRAppCheckLogger.h @@ -20,5 +20,27 @@ extern FIRLoggerService kFIRLoggerAppCheck; -// TODO: use specific codes when stabilized. -extern NSString *const kFIRLoggerAppCheckMessageCodeUnknown; +FOUNDATION_EXPORT NSString *const kFIRLoggerAppCheckMessageCodeUnknown; + +// FIRAppCheck.m +FOUNDATION_EXPORT NSString *const kFIRLoggerAppCheckMessageCodeProviderFactoryIsMissing; +FOUNDATION_EXPORT NSString *const kFIRLoggerAppCheckMessageCodeProviderIsMissing; + +// FIRAppCheckAPIService.m +FOUNDATION_EXPORT NSString *const kFIRLoggerAppCheckMessageCodeUnexpectedHTTPCode; + +// FIRAppCheckDebugProvider.m +FOUNDATION_EXPORT NSString *const kFIRLoggerAppCheckMessageDebugProviderIncompleteFIROptions; +FOUNDATION_EXPORT NSString *const kFIRLoggerAppCheckMessageDebugProviderFailedExchange; + +// FIRAppCheckDebugProviderFactory.m +FOUNDATION_EXPORT NSString *const kFIRLoggerAppCheckMessageCodeDebugToken; + +// FIRDeviceCheckProvider.m +FOUNDATION_EXPORT NSString *const kFIRLoggerAppCheckMessageDeviceCheckProviderIncompleteFIROptions; + +// FIRAppAttestProvider.m +FOUNDATION_EXPORT NSString *const kFIRLoggerAppCheckMessageCodeAppAttestNotSupported; +FOUNDATION_EXPORT NSString *const kFIRLoggerAppCheckMessageCodeAttestationRejected; + +void FIRAppCheckDebugLog(NSString *messageCode, NSString *message, ...); diff --git a/FirebaseAppCheck/Sources/Core/FIRAppCheckLogger.m b/FirebaseAppCheck/Sources/Core/FIRAppCheckLogger.m index 834502b360f..a2c411bad94 100644 --- a/FirebaseAppCheck/Sources/Core/FIRAppCheckLogger.m +++ b/FirebaseAppCheck/Sources/Core/FIRAppCheckLogger.m @@ -16,6 +16,41 @@ #import "FirebaseAppCheck/Sources/Core/FIRAppCheckLogger.h" +#import "FirebaseCore/Sources/Private/FirebaseCoreInternal.h" + +NS_ASSUME_NONNULL_BEGIN + FIRLoggerService kFIRLoggerAppCheck = @"[Firebase/AppCheck]"; NSString *const kFIRLoggerAppCheckMessageCodeUnknown = @"I-FAA001001"; + +// FIRAppCheck.m +NSString *const kFIRLoggerAppCheckMessageCodeProviderFactoryIsMissing = @"I-FAA002001"; +NSString *const kFIRLoggerAppCheckMessageCodeProviderIsMissing = @"I-FAA002002"; + +// FIRAppCheckAPIService.m +NSString *const kFIRLoggerAppCheckMessageCodeUnexpectedHTTPCode = @"I-FAA003001"; + +// FIRAppCheckDebugProvider.m +NSString *const kFIRLoggerAppCheckMessageDebugProviderIncompleteFIROptions = @"I-FAA004001"; +NSString *const kFIRLoggerAppCheckMessageDebugProviderFailedExchange = @"I-FAA004002"; + +// FIRAppCheckDebugProviderFactory.m +NSString *const kFIRLoggerAppCheckMessageCodeDebugToken = @"I-FAA005001"; + +// FIRDeviceCheckProvider.m +NSString *const kFIRLoggerAppCheckMessageDeviceCheckProviderIncompleteFIROptions = @"I-FAA006001"; + +// FIRAppAttestProvider.m +NSString *const kFIRLoggerAppCheckMessageCodeAppAttestNotSupported = @"I-FAA007001"; +NSString *const kFIRLoggerAppCheckMessageCodeAttestationRejected = @"I-FAA007002"; + +#pragma mark - Log functions +void FIRAppCheckDebugLog(NSString *messageCode, NSString *message, ...) { + va_list args_ptr; + va_start(args_ptr, message); + FIRLogBasic(FIRLoggerLevelDebug, kFIRLoggerAppCheck, messageCode, message, args_ptr); + va_end(args_ptr); +} + +NS_ASSUME_NONNULL_END diff --git a/FirebaseAppCheck/Sources/DebugProvider/FIRAppCheckDebugProvider.m b/FirebaseAppCheck/Sources/DebugProvider/FIRAppCheckDebugProvider.m index 922ec8afb8d..87eb1239165 100644 --- a/FirebaseAppCheck/Sources/DebugProvider/FIRAppCheckDebugProvider.m +++ b/FirebaseAppCheck/Sources/DebugProvider/FIRAppCheckDebugProvider.m @@ -53,7 +53,7 @@ - (nullable instancetype)initWithApp:(FIRApp *)app { NSArray *missingOptionsFields = [FIRAppCheckValidator tokenExchangeMissingFieldsInOptions:app.options]; if (missingOptionsFields.count > 0) { - FIRLogError(kFIRLoggerAppCheck, kFIRLoggerAppCheckMessageCodeUnknown, + FIRLogError(kFIRLoggerAppCheck, kFIRLoggerAppCheckMessageDebugProviderIncompleteFIROptions, @"Cannot instantiate `FIRAppCheckDebugProvider` for app: %@. The following " @"`FirebaseOptions` fields are missing: %@", app.name, [missingOptionsFields componentsJoinedByString:@", "]); @@ -119,8 +119,8 @@ - (void)getTokenWithCompletion:(void (^)(FIRAppCheckToken *_Nullable token, return nil; }) .catch(^void(NSError *error) { - FIRLogDebug(kFIRLoggerAppCheck, kFIRLoggerAppCheckMessageCodeUnknown, - @"Failed to exchange debug token to app check token: %@", error); + FIRAppCheckDebugLog(kFIRLoggerAppCheckMessageDebugProviderFailedExchange, + @"Failed to exchange debug token to app check token: %@", error); handler(nil, error); }); } diff --git a/FirebaseAppCheck/Sources/DebugProvider/FIRAppCheckDebugProviderFactory.m b/FirebaseAppCheck/Sources/DebugProvider/FIRAppCheckDebugProviderFactory.m index 0cb1a06f325..e9980fead80 100644 --- a/FirebaseAppCheck/Sources/DebugProvider/FIRAppCheckDebugProviderFactory.m +++ b/FirebaseAppCheck/Sources/DebugProvider/FIRAppCheckDebugProviderFactory.m @@ -27,7 +27,7 @@ @implementation FIRAppCheckDebugProviderFactory FIRAppCheckDebugProvider *provider = [[FIRAppCheckDebugProvider alloc] initWithApp:app]; // Print only locally generated token to avoid a valid token leak on CI. - FIRLogWarning(kFIRLoggerAppCheck, kFIRLoggerAppCheckMessageCodeUnknown, + FIRLogWarning(kFIRLoggerAppCheck, kFIRLoggerAppCheckMessageCodeDebugToken, @"Firebase App Check debug token: '%@'.", [provider localDebugToken]); return provider; diff --git a/FirebaseAppCheck/Sources/DeviceCheckProvider/FIRDeviceCheckProvider.m b/FirebaseAppCheck/Sources/DeviceCheckProvider/FIRDeviceCheckProvider.m index c6c92f9fd41..9603a75b25b 100644 --- a/FirebaseAppCheck/Sources/DeviceCheckProvider/FIRDeviceCheckProvider.m +++ b/FirebaseAppCheck/Sources/DeviceCheckProvider/FIRDeviceCheckProvider.m @@ -65,7 +65,8 @@ - (nullable instancetype)initWithApp:(FIRApp *)app { NSArray *missingOptionsFields = [FIRAppCheckValidator tokenExchangeMissingFieldsInOptions:app.options]; if (missingOptionsFields.count > 0) { - FIRLogError(kFIRLoggerAppCheck, kFIRLoggerAppCheckMessageCodeUnknown, + FIRLogError(kFIRLoggerAppCheck, + kFIRLoggerAppCheckMessageDeviceCheckProviderIncompleteFIROptions, @"Cannot instantiate `FIRDeviceCheckProvider` for app: %@. The following " @"`FirebaseOptions` fields are missing: %@", app.name, [missingOptionsFields componentsJoinedByString:@", "]); diff --git a/FirebaseAppCheck/Tests/Unit/AppAttestProvider/FIRAppAttestAPIServiceTests.m b/FirebaseAppCheck/Tests/Unit/AppAttestProvider/FIRAppAttestAPIServiceTests.m index cce578b7a76..e610665de58 100644 --- a/FirebaseAppCheck/Tests/Unit/AppAttestProvider/FIRAppAttestAPIServiceTests.m +++ b/FirebaseAppCheck/Tests/Unit/AppAttestProvider/FIRAppAttestAPIServiceTests.m @@ -25,6 +25,7 @@ #import "FirebaseAppCheck/Sources/AppAttestProvider/API/FIRAppAttestAttestationResponse.h" #import "FirebaseAppCheck/Sources/Core/APIService/FIRAppCheckAPIService.h" #import "FirebaseAppCheck/Sources/Core/Errors/FIRAppCheckErrorUtil.h" +#import "FirebaseAppCheck/Sources/Core/Errors/FIRAppCheckHTTPError.h" #import "FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRAppCheckToken.h" #import "FirebaseAppCheck/Tests/Unit/Utils/FIRFixtureLoader.h" @@ -98,8 +99,9 @@ - (void)testGetRandomChallengeWhenAPIError { NSData *responseBody = [responseBodyString dataUsingEncoding:NSUTF8StringEncoding]; GULURLSessionDataResponse *invalidAPIResponse = [self APIResponseWithCode:300 responseBody:responseBody]; - NSError *APIError = [FIRAppCheckErrorUtil APIErrorWithHTTPResponse:invalidAPIResponse.HTTPResponse - data:invalidAPIResponse.HTTPBody]; + FIRAppCheckHTTPError *APIError = + [FIRAppCheckErrorUtil APIErrorWithHTTPResponse:invalidAPIResponse.HTTPResponse + data:invalidAPIResponse.HTTPBody]; // 2. Stub API Service Request to return prepared API response. [self stubMockAPIServiceRequestForChallengeRequestWithResponse:APIError]; diff --git a/FirebaseAppCheck/Tests/Unit/AppAttestProvider/FIRAppAttestProviderTests.m b/FirebaseAppCheck/Tests/Unit/AppAttestProvider/FIRAppAttestProviderTests.m index 5e89306c25f..178d94567c9 100644 --- a/FirebaseAppCheck/Tests/Unit/AppAttestProvider/FIRAppAttestProviderTests.m +++ b/FirebaseAppCheck/Tests/Unit/AppAttestProvider/FIRAppAttestProviderTests.m @@ -26,10 +26,13 @@ #import "FirebaseAppCheck/Sources/AppAttestProvider/FIRAppAttestService.h" #import "FirebaseAppCheck/Sources/AppAttestProvider/Storage/FIRAppAttestArtifactStorage.h" #import "FirebaseAppCheck/Sources/AppAttestProvider/Storage/FIRAppAttestKeyIDStorage.h" -#import "FirebaseAppCheck/Sources/Core/Errors/FIRAppCheckErrorUtil.h" #import "FirebaseAppCheck/Sources/Core/Utils/FIRAppCheckCryptoUtils.h" #import "FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRAppCheckToken.h" +#import "FirebaseAppCheck/Sources/AppAttestProvider/Errors/FIRAppAttestRejectionError.h" +#import "FirebaseAppCheck/Sources/Core/Errors/FIRAppCheckErrorUtil.h" +#import "FirebaseAppCheck/Sources/Core/Errors/FIRAppCheckHTTPError.h" + #import "FirebaseCore/Sources/Private/FirebaseCoreInternal.h" // Currently FIRAppAttestProvider is available only on iOS. @@ -419,6 +422,119 @@ - (void)testGetToken_WhenUnregisteredKeyAndKeyAttestationExchangeError { [self verifyAllMocks]; } +#pragma mark Rejected Attestation + +- (void)testGetToken_WhenAttestationIsRejected_ThenAttestationIsResetAndRetriedOnceSuccess { + // 1. Expect App Attest availability to be requested and stored key ID request to fail. + [self expectAppAttestAvailabilityToBeCheckedAndNotExistingStoredKeyRequested]; + + // 2. Expect the App Attest key pair to be generated and attested. + NSString *keyID1 = @"keyID1"; + NSData *attestationData1 = [[NSUUID UUID].UUIDString dataUsingEncoding:NSUTF8StringEncoding]; + [self expectAppAttestKeyGeneratedAndAttestedWithKeyID:keyID1 attestationData:attestationData1]; + + // 3. Expect exchange request to be sent. + FIRAppCheckHTTPError *APIError = [self attestationRejectionHTTPError]; + OCMExpect([self.mockAPIService attestKeyWithAttestation:attestationData1 + keyID:keyID1 + challenge:self.randomChallenge]) + .andReturn([self rejectedPromiseWithError:APIError]); + + // 4. Stored attestation to be reset. + [self expectAttestationReset]; + + // 5. Expect the App Attest key pair to be generated and attested. + NSString *keyID2 = @"keyID2"; + NSData *attestationData2 = [[NSUUID UUID].UUIDString dataUsingEncoding:NSUTF8StringEncoding]; + [self expectAppAttestKeyGeneratedAndAttestedWithKeyID:keyID2 attestationData:attestationData2]; + + // 6. Expect exchange request to be sent. + FIRAppCheckToken *FACToken = [[FIRAppCheckToken alloc] initWithToken:@"FAC token" + expirationDate:[NSDate date]]; + NSData *artifactData = [@"attestation artifact" dataUsingEncoding:NSUTF8StringEncoding]; + __auto_type attestKeyResponse = + [[FIRAppAttestAttestationResponse alloc] initWithArtifact:artifactData token:FACToken]; + OCMExpect([self.mockAPIService attestKeyWithAttestation:attestationData2 + keyID:keyID2 + challenge:self.randomChallenge]) + .andReturn([FBLPromise resolvedWith:attestKeyResponse]); + + // 7. Expect the artifact received from Firebase backend to be saved. + OCMExpect([self.mockArtifactStorage setArtifact:artifactData forKey:keyID2]) + .andReturn([FBLPromise resolvedWith:artifactData]); + + // 8. Call get token. + XCTestExpectation *completionExpectation = + [self expectationWithDescription:@"completionExpectation"]; + [self.provider + getTokenWithCompletion:^(FIRAppCheckToken *_Nullable token, NSError *_Nullable error) { + [completionExpectation fulfill]; + + XCTAssertEqualObjects(token.token, FACToken.token); + XCTAssertEqualObjects(token.expirationDate, FACToken.expirationDate); + XCTAssertNil(error); + }]; + + [self waitForExpectations:@[ completionExpectation ] timeout:0.5]; + + // 8. Verify mocks. + [self verifyAllMocks]; +} + +- (void)testGetToken_WhenAttestationIsRejected_ThenAttestationIsResetAndRetriedOnceError { + // 1. Expect App Attest availability to be requested and stored key ID request to fail. + [self expectAppAttestAvailabilityToBeCheckedAndNotExistingStoredKeyRequested]; + + // 2. Expect the App Attest key pair to be generated and attested. + NSString *keyID1 = @"keyID1"; + NSData *attestationData1 = [[NSUUID UUID].UUIDString dataUsingEncoding:NSUTF8StringEncoding]; + [self expectAppAttestKeyGeneratedAndAttestedWithKeyID:keyID1 attestationData:attestationData1]; + + // 3. Expect exchange request to be sent. + FIRAppCheckHTTPError *APIError = [self attestationRejectionHTTPError]; + OCMExpect([self.mockAPIService attestKeyWithAttestation:attestationData1 + keyID:keyID1 + challenge:self.randomChallenge]) + .andReturn([self rejectedPromiseWithError:APIError]); + + // 4. Stored attestation to be reset. + [self expectAttestationReset]; + + // 5. Expect the App Attest key pair to be generated and attested. + NSString *keyID2 = @"keyID2"; + NSData *attestationData2 = [[NSUUID UUID].UUIDString dataUsingEncoding:NSUTF8StringEncoding]; + [self expectAppAttestKeyGeneratedAndAttestedWithKeyID:keyID2 attestationData:attestationData2]; + + // 6. Expect exchange request to be sent. + OCMExpect([self.mockAPIService attestKeyWithAttestation:attestationData2 + keyID:keyID2 + challenge:self.randomChallenge]) + .andReturn([self rejectedPromiseWithError:APIError]); + + // 7. Stored attestation to be reset. + [self expectAttestationReset]; + + // 8. Don't expect the artifact received from Firebase backend to be saved. + OCMReject([self.mockArtifactStorage setArtifact:OCMOCK_ANY forKey:OCMOCK_ANY]); + + // 9. Call get token. + XCTestExpectation *completionExpectation = + [self expectationWithDescription:@"completionExpectation"]; + [self.provider + getTokenWithCompletion:^(FIRAppCheckToken *_Nullable token, NSError *_Nullable error) { + [completionExpectation fulfill]; + + XCTAssertNil(token); + FIRAppAttestRejectionError *expectedError = [[FIRAppAttestRejectionError alloc] init]; + XCTAssertEqualObjects(error, expectedError); + }]; + + [self waitForExpectations:@[ completionExpectation ] timeout:0.5]; + + // 9. Verify mocks. + [self verifyAllMocks]; +} + #pragma mark - FAC token refresh (assertion) - (void)testGetToken_WhenKeyRegistered_Success { @@ -745,6 +861,16 @@ - (void)verifyAllMocks { OCMVerifyAll(self.mockArtifactStorage); } +- (FIRAppCheckHTTPError *)attestationRejectionHTTPError { + NSHTTPURLResponse *response = + [[NSHTTPURLResponse alloc] initWithURL:[NSURL URLWithString:@"http://localhost"] + statusCode:403 + HTTPVersion:@"HTTP/1.1" + headerFields:nil]; + NSData *responseBody = [@"Could not verify attestation" dataUsingEncoding:NSUTF8StringEncoding]; + return [[FIRAppCheckHTTPError alloc] initWithHTTPResponse:response data:responseBody]; +} + - (void)assertGetToken_WhenKeyRegistered_Success { // 1. Expect FIRAppAttestService.isSupported. [OCMExpect([self.mockAppAttestService isSupported]) andReturnValue:@(YES)]; @@ -797,6 +923,48 @@ - (void)assertGetToken_WhenKeyRegistered_Success { [self verifyAllMocks]; } +- (void)expectAppAttestAvailabilityToBeCheckedAndNotExistingStoredKeyRequested { + // 1. Expect FIRAppAttestService.isSupported. + [OCMExpect([self.mockAppAttestService isSupported]) andReturnValue:@(YES)]; + + // 2. Expect storage getAppAttestKeyID. + FBLPromise *rejectedPromise = [FBLPromise pendingPromise]; + NSError *error = [NSError errorWithDomain:@"testGetToken_WhenNoExistingKey_Success" + code:NSNotFound + userInfo:nil]; + [rejectedPromise reject:error]; + OCMExpect([self.mockStorage getAppAttestKeyID]).andReturn(rejectedPromise); +} + +- (void)expectAppAttestKeyGeneratedAndAttestedWithKeyID:(NSString *)keyID + attestationData:(NSData *)attestationData { + // 1. Expect App Attest key to be generated. + id completionArg = [OCMArg invokeBlockWithArgs:keyID, [NSNull null], nil]; + OCMExpect([self.mockAppAttestService generateKeyWithCompletionHandler:completionArg]); + + // 2. Expect the key ID to be stored. + OCMExpect([self.mockStorage setAppAttestKeyID:keyID]).andReturn([FBLPromise resolvedWith:keyID]); + + // 3. Expect random challenge to be requested. + OCMExpect([self.mockAPIService getRandomChallenge]) + .andReturn([FBLPromise resolvedWith:self.randomChallenge]); + + // 4. Expect the key to be attested with the challenge. + id attestCompletionArg = [OCMArg invokeBlockWithArgs:attestationData, [NSNull null], nil]; + OCMExpect([self.mockAppAttestService attestKey:keyID + clientDataHash:self.randomChallengeHash + completionHandler:attestCompletionArg]); +} + +- (void)expectAttestationReset { + // 1. Expect stored key ID to be reset. + OCMExpect([self.mockStorage setAppAttestKeyID:nil]).andReturn([FBLPromise resolvedWith:nil]); + + // 2. Expect stored attestation artifact to be reset. + OCMExpect([self.mockArtifactStorage setArtifact:nil forKey:@""]) + .andReturn([FBLPromise resolvedWith:nil]); +} + @end #endif // TARGET_OS_IOS diff --git a/FirebaseAppCheck/Tests/Unit/Core/FIRAppCheckTests.m b/FirebaseAppCheck/Tests/Unit/Core/FIRAppCheckTests.m index 5e0458ab31d..0fc0c7a5578 100644 --- a/FirebaseAppCheck/Tests/Unit/Core/FIRAppCheckTests.m +++ b/FirebaseAppCheck/Tests/Unit/Core/FIRAppCheckTests.m @@ -342,8 +342,8 @@ - (void)testGetToken_AppCheckProviderError { XCTAssertNotNil(result); XCTAssertEqualObjects(result.token, kDummyToken); - // TODO: Expect a public domain error to be returned - not the - // internal one. + // TODO: When method is added to public API: expect a public domain + // error to be returned - not the internal one. XCTAssertEqualObjects(result.error, providerError); }]; From 76486afcb8b5f3d9a34e47ebc2ce5072115d3e35 Mon Sep 17 00:00:00 2001 From: Maksym Malyhin Date: Tue, 1 Jun 2021 09:42:18 -0400 Subject: [PATCH 15/37] Add FIRAppAttestProvider.h the umbrella header --- .../Sources/Public/FirebaseAppCheck/FirebaseAppCheck.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FirebaseAppCheck.h b/FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FirebaseAppCheck.h index 234abeebf3e..0de48dbc576 100644 --- a/FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FirebaseAppCheck.h +++ b/FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FirebaseAppCheck.h @@ -26,3 +26,6 @@ // DeviceCheck provider #import "FIRDeviceCheckProvider.h" #import "FIRDeviceCheckProviderFactory.h" + +// App Attest provider. +#import "FIRAppAttestProvider.h" From 67c6d91854ecd8d135dd77160e9b4a02491be693 Mon Sep 17 00:00:00 2001 From: Maksym Malyhin Date: Wed, 2 Jun 2021 16:35:35 -0400 Subject: [PATCH 16/37] Add receivedAtDate property to the FAC token --- .../Sources/Core/FIRAppCheckToken+Internal.h | 38 +++++++++++++++++++ .../Sources/Core/FIRAppCheckToken.m | 15 +++++++- .../FIRAppCheckStoredToken+FIRAppCheckToken.m | 5 ++- .../Core/Storage/FIRAppCheckStoredToken.h | 12 ++++-- .../Core/Storage/FIRAppCheckStoredToken.m | 9 +++-- .../FirebaseAppCheck/FIRAppCheckToken.h | 5 ++- .../Unit/Core/FIRAppCheckStoredTokenTests.m | 6 ++- 7 files changed, 76 insertions(+), 14 deletions(-) create mode 100644 FirebaseAppCheck/Sources/Core/FIRAppCheckToken+Internal.h diff --git a/FirebaseAppCheck/Sources/Core/FIRAppCheckToken+Internal.h b/FirebaseAppCheck/Sources/Core/FIRAppCheckToken+Internal.h new file mode 100644 index 00000000000..d7738a6fc10 --- /dev/null +++ b/FirebaseAppCheck/Sources/Core/FIRAppCheckToken+Internal.h @@ -0,0 +1,38 @@ +/* + * Copyright 2021 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 + +#import "FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRAppCheckToken.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FIRAppCheckToken () + +/// A date when the Firebase App Check token was received in the device's local time. +@property(nonatomic) NSDate *receivedAtDate; + +/// The designated initializer. +/// @param token A Firebase App Check token. +/// @param expirationDate A Firebase App Check token expiration date in the device local time. +/// @param receivedAtDate A date when the Firebase App Check token was received in the device's local time. +- (instancetype)initWithToken:(NSString *)token + expirationDate:(NSDate *)expirationDate + receivedAtDate:(NSDate *)receivedAtDate NS_DESIGNATED_INITIALIZER; + +@end + +NS_ASSUME_NONNULL_END diff --git a/FirebaseAppCheck/Sources/Core/FIRAppCheckToken.m b/FirebaseAppCheck/Sources/Core/FIRAppCheckToken.m index 20d9a830561..35af5adde09 100644 --- a/FirebaseAppCheck/Sources/Core/FIRAppCheckToken.m +++ b/FirebaseAppCheck/Sources/Core/FIRAppCheckToken.m @@ -14,17 +14,28 @@ * limitations under the License. */ -#import "FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRAppCheckToken.h" +#import "FirebaseAppCheck/Sources/Core/FIRAppCheckToken+Internal.h" + +NS_ASSUME_NONNULL_BEGIN @implementation FIRAppCheckToken -- (instancetype)initWithToken:(NSString *)token expirationDate:(NSDate *)expirationDate { +- (instancetype)initWithToken:(NSString *)token + expirationDate:(NSDate *)expirationDate + receivedAtDate:(NSDate *)receivedAtDate { self = [super init]; if (self) { _token = [token copy]; _expirationDate = expirationDate; + _receivedAtDate = receivedAtDate; } return self; } +- (instancetype)initWithToken:(NSString *)token expirationDate:(NSDate *)expirationDate { + return [self initWithToken:token expirationDate:expirationDate receivedAtDate:[NSDate date]]; +} + @end + +NS_ASSUME_NONNULL_END diff --git a/FirebaseAppCheck/Sources/Core/Storage/FIRAppCheckStoredToken+FIRAppCheckToken.m b/FirebaseAppCheck/Sources/Core/Storage/FIRAppCheckStoredToken+FIRAppCheckToken.m index 4fd25df2132..41d351c74aa 100644 --- a/FirebaseAppCheck/Sources/Core/Storage/FIRAppCheckStoredToken+FIRAppCheckToken.m +++ b/FirebaseAppCheck/Sources/Core/Storage/FIRAppCheckStoredToken+FIRAppCheckToken.m @@ -16,17 +16,18 @@ #import "FirebaseAppCheck/Sources/Core/Storage/FIRAppCheckStoredToken+FIRAppCheckToken.h" -#import "FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRAppCheckToken.h" +#import "FirebaseAppCheck/Sources/Core/FIRAppCheckToken+Internal.h" @implementation FIRAppCheckStoredToken (FIRAppCheckToken) - (void)updateWithToken:(FIRAppCheckToken *)token { self.token = token.token; self.expirationDate = token.expirationDate; + self.receivedAtDate = token.receivedAtDate; } - (FIRAppCheckToken *)appCheckToken { - return [[FIRAppCheckToken alloc] initWithToken:self.token expirationDate:self.expirationDate]; + return [[FIRAppCheckToken alloc] initWithToken:self.token expirationDate:self.expirationDate receivedAtDate:self.receivedAtDate]; } @end diff --git a/FirebaseAppCheck/Sources/Core/Storage/FIRAppCheckStoredToken.h b/FirebaseAppCheck/Sources/Core/Storage/FIRAppCheckStoredToken.h index c2c4e2a900d..63f46764009 100644 --- a/FirebaseAppCheck/Sources/Core/Storage/FIRAppCheckStoredToken.h +++ b/FirebaseAppCheck/Sources/Core/Storage/FIRAppCheckStoredToken.h @@ -22,10 +22,14 @@ NS_ASSUME_NONNULL_BEGIN @interface FIRAppCheckStoredToken : NSObject -/// FAA token. -@property(nonatomic, copy) NSString *token; -/// FAA token expiration date in the device local time. -@property(nonatomic, strong) NSDate *expirationDate; +/// The Firebase App Check token. +@property(nonatomic, copy, nullable) NSString *token; + +/// The Firebase App Check token expiration date in the device local time. +@property(nonatomic, strong, nullable) NSDate *expirationDate; + +/// The date when the Firebase App Check token was received in the device's local time. +@property(nonatomic, strong, nullable) NSDate *receivedAtDate; /// The version of local storage. @property(nonatomic, readonly) NSInteger storageVersion; diff --git a/FirebaseAppCheck/Sources/Core/Storage/FIRAppCheckStoredToken.m b/FirebaseAppCheck/Sources/Core/Storage/FIRAppCheckStoredToken.m index e7cf78ac7c0..217f2c77da3 100644 --- a/FirebaseAppCheck/Sources/Core/Storage/FIRAppCheckStoredToken.m +++ b/FirebaseAppCheck/Sources/Core/Storage/FIRAppCheckStoredToken.m @@ -18,9 +18,10 @@ static NSString *const kTokenKey = @"token"; static NSString *const kExpirationDateKey = @"expirationDate"; +static NSString *const kReceivedAtDateKey = @"receivedAtDate"; static NSString *const kStorageVersionKey = @"storageVersion"; -static const NSInteger kStorageVersion = 1; +static const NSInteger kStorageVersion = 2; NS_ASSUME_NONNULL_BEGIN @@ -37,19 +38,21 @@ + (BOOL)supportsSecureCoding { - (void)encodeWithCoder:(NSCoder *)coder { [coder encodeObject:self.token forKey:kTokenKey]; [coder encodeObject:self.expirationDate forKey:kExpirationDateKey]; + [coder encodeObject:self.receivedAtDate forKey:kReceivedAtDateKey]; [coder encodeInteger:self.storageVersion forKey:kStorageVersionKey]; } - (nullable instancetype)initWithCoder:(NSCoder *)coder { self = [super init]; if (self) { - NSInteger storageVersion = [coder decodeIntegerForKey:kStorageVersionKey]; - if (storageVersion > kStorageVersion) { + NSInteger decodedStorageVersion = [coder decodeIntegerForKey:kStorageVersionKey]; + if (decodedStorageVersion > kStorageVersion) { // TODO: Log a message. } _token = [coder decodeObjectOfClass:[NSString class] forKey:kTokenKey]; _expirationDate = [coder decodeObjectOfClass:[NSDate class] forKey:kExpirationDateKey]; + _receivedAtDate = [coder decodeObjectOfClass:[NSDate class] forKey:kReceivedAtDateKey]; } return self; } diff --git a/FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRAppCheckToken.h b/FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRAppCheckToken.h index bc74856f1aa..377d636bc68 100644 --- a/FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRAppCheckToken.h +++ b/FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRAppCheckToken.h @@ -24,16 +24,17 @@ NS_SWIFT_NAME(AppCheckToken) /// A Firebase App Check token. @property(nonatomic, readonly) NSString *token; + /// The App Check token's expiration date in the device's local time. @property(nonatomic, readonly) NSDate *expirationDate; - (instancetype)init NS_UNAVAILABLE; -/// The designated initializer. +/// The default initializer. /// @param token A Firebase App Check token. /// @param expirationDate A Firebase App Check token expiration date in the device local time. - (instancetype)initWithToken:(NSString *)token - expirationDate:(NSDate *)expirationDate NS_DESIGNATED_INITIALIZER; + expirationDate:(NSDate *)expirationDate; @end diff --git a/FirebaseAppCheck/Tests/Unit/Core/FIRAppCheckStoredTokenTests.m b/FirebaseAppCheck/Tests/Unit/Core/FIRAppCheckStoredTokenTests.m index 15233f50758..4b6cf85b6d9 100644 --- a/FirebaseAppCheck/Tests/Unit/Core/FIRAppCheckStoredTokenTests.m +++ b/FirebaseAppCheck/Tests/Unit/Core/FIRAppCheckStoredTokenTests.m @@ -18,7 +18,7 @@ #import "FirebaseAppCheck/Sources/Core/Storage/FIRAppCheckStoredToken+FIRAppCheckToken.h" #import "FirebaseAppCheck/Sources/Core/Storage/FIRAppCheckStoredToken.h" -#import "FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRAppCheckToken.h" +#import "FirebaseAppCheck/Sources/Core/FIRAppCheckToken+Internal.h" #import @@ -32,6 +32,7 @@ - (void)testSecureCoding { FIRAppCheckStoredToken *tokenToArchive = [[FIRAppCheckStoredToken alloc] init]; tokenToArchive.token = @"some_token"; tokenToArchive.expirationDate = [NSDate date]; + tokenToArchive.receivedAtDate = [tokenToArchive.expirationDate dateByAddingTimeInterval:-10]; NSError *error; NSData *archivedToken = [GULSecureCoding archivedDataWithRootObject:tokenToArchive error:&error]; @@ -46,6 +47,7 @@ - (void)testSecureCoding { XCTAssertNil(error); XCTAssertEqualObjects(unarchivedToken.token, tokenToArchive.token); XCTAssertEqualObjects(unarchivedToken.expirationDate, tokenToArchive.expirationDate); + XCTAssertEqualObjects(unarchivedToken.receivedAtDate, tokenToArchive.receivedAtDate); XCTAssertEqual(unarchivedToken.storageVersion, tokenToArchive.storageVersion); } @@ -57,10 +59,12 @@ - (void)testConvertingToAndFromFIRAppCheckToken { [storedToken updateWithToken:originalToken]; XCTAssertEqualObjects(originalToken.token, storedToken.token); XCTAssertEqualObjects(originalToken.expirationDate, storedToken.expirationDate); + XCTAssertEqualObjects(originalToken.receivedAtDate, storedToken.receivedAtDate); FIRAppCheckToken *recoveredToken = [storedToken appCheckToken]; XCTAssertEqualObjects(recoveredToken.token, storedToken.token); XCTAssertEqualObjects(recoveredToken.expirationDate, storedToken.expirationDate); + XCTAssertEqualObjects(recoveredToken.receivedAtDate, storedToken.receivedAtDate); } @end From a7c47924e687cdcc47a42abd2027ec87ca2bd96f Mon Sep 17 00:00:00 2001 From: Maksym Malyhin Date: Wed, 2 Jun 2021 17:12:41 -0400 Subject: [PATCH 17/37] Update tests to check receivedAtDate field where important --- .../Core/APIService/FIRAppCheckToken+APIResponse.m | 3 ++- .../Sources/Core/FIRAppCheckToken+Internal.h | 3 ++- .../Storage/FIRAppCheckStoredToken+FIRAppCheckToken.m | 4 +++- .../Sources/Public/FirebaseAppCheck/FIRAppCheckToken.h | 3 +-- .../Unit/AppAttestProvider/FIRAppAttestAPIServiceTests.m | 6 ++++-- .../Tests/Unit/Core/FIRAppCheckStorageTests.m | 9 ++++++--- .../Tests/Unit/Core/FIRAppCheckStoredTokenTests.m | 5 +++-- .../Unit/DebugProvider/FIRAppCheckDebugProviderTests.m | 6 ++++-- .../DeviceCheckProvider/FIRDeviceCheckProviderTests.m | 6 ++++-- 9 files changed, 29 insertions(+), 16 deletions(-) diff --git a/FirebaseAppCheck/Sources/Core/APIService/FIRAppCheckToken+APIResponse.m b/FirebaseAppCheck/Sources/Core/APIService/FIRAppCheckToken+APIResponse.m index 3fc188144f5..2f063db7555 100644 --- a/FirebaseAppCheck/Sources/Core/APIService/FIRAppCheckToken+APIResponse.m +++ b/FirebaseAppCheck/Sources/Core/APIService/FIRAppCheckToken+APIResponse.m @@ -15,6 +15,7 @@ */ #import "FirebaseAppCheck/Sources/Core/APIService/FIRAppCheckToken+APIResponse.h" +#import "FirebaseAppCheck/Sources/Core/FIRAppCheckToken+Internal.h" #if __has_include() #import @@ -81,7 +82,7 @@ - (nullable instancetype)initWithResponseDict:(NSDictionary *)re NSDate *expirationDate = [requestDate dateByAddingTimeInterval:secondsToLive]; - return [self initWithToken:token expirationDate:expirationDate]; + return [self initWithToken:token expirationDate:expirationDate receivedAtDate:requestDate]; } @end diff --git a/FirebaseAppCheck/Sources/Core/FIRAppCheckToken+Internal.h b/FirebaseAppCheck/Sources/Core/FIRAppCheckToken+Internal.h index d7738a6fc10..48c55d2b6fc 100644 --- a/FirebaseAppCheck/Sources/Core/FIRAppCheckToken+Internal.h +++ b/FirebaseAppCheck/Sources/Core/FIRAppCheckToken+Internal.h @@ -28,7 +28,8 @@ NS_ASSUME_NONNULL_BEGIN /// The designated initializer. /// @param token A Firebase App Check token. /// @param expirationDate A Firebase App Check token expiration date in the device local time. -/// @param receivedAtDate A date when the Firebase App Check token was received in the device's local time. +/// @param receivedAtDate A date when the Firebase App Check token was received in the device's +/// local time. - (instancetype)initWithToken:(NSString *)token expirationDate:(NSDate *)expirationDate receivedAtDate:(NSDate *)receivedAtDate NS_DESIGNATED_INITIALIZER; diff --git a/FirebaseAppCheck/Sources/Core/Storage/FIRAppCheckStoredToken+FIRAppCheckToken.m b/FirebaseAppCheck/Sources/Core/Storage/FIRAppCheckStoredToken+FIRAppCheckToken.m index 41d351c74aa..9135854709e 100644 --- a/FirebaseAppCheck/Sources/Core/Storage/FIRAppCheckStoredToken+FIRAppCheckToken.m +++ b/FirebaseAppCheck/Sources/Core/Storage/FIRAppCheckStoredToken+FIRAppCheckToken.m @@ -27,7 +27,9 @@ - (void)updateWithToken:(FIRAppCheckToken *)token { } - (FIRAppCheckToken *)appCheckToken { - return [[FIRAppCheckToken alloc] initWithToken:self.token expirationDate:self.expirationDate receivedAtDate:self.receivedAtDate]; + return [[FIRAppCheckToken alloc] initWithToken:self.token + expirationDate:self.expirationDate + receivedAtDate:self.receivedAtDate]; } @end diff --git a/FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRAppCheckToken.h b/FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRAppCheckToken.h index 377d636bc68..368b900ae06 100644 --- a/FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRAppCheckToken.h +++ b/FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRAppCheckToken.h @@ -33,8 +33,7 @@ NS_SWIFT_NAME(AppCheckToken) /// The default initializer. /// @param token A Firebase App Check token. /// @param expirationDate A Firebase App Check token expiration date in the device local time. -- (instancetype)initWithToken:(NSString *)token - expirationDate:(NSDate *)expirationDate; +- (instancetype)initWithToken:(NSString *)token expirationDate:(NSDate *)expirationDate; @end diff --git a/FirebaseAppCheck/Tests/Unit/AppAttestProvider/FIRAppAttestAPIServiceTests.m b/FirebaseAppCheck/Tests/Unit/AppAttestProvider/FIRAppAttestAPIServiceTests.m index e610665de58..d0a7ec36750 100644 --- a/FirebaseAppCheck/Tests/Unit/AppAttestProvider/FIRAppAttestAPIServiceTests.m +++ b/FirebaseAppCheck/Tests/Unit/AppAttestProvider/FIRAppAttestAPIServiceTests.m @@ -26,7 +26,7 @@ #import "FirebaseAppCheck/Sources/Core/APIService/FIRAppCheckAPIService.h" #import "FirebaseAppCheck/Sources/Core/Errors/FIRAppCheckErrorUtil.h" #import "FirebaseAppCheck/Sources/Core/Errors/FIRAppCheckHTTPError.h" -#import "FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRAppCheckToken.h" +#import "FirebaseAppCheck/Sources/Core/FIRAppCheckToken+Internal.h" #import "FirebaseAppCheck/Tests/Unit/Utils/FIRFixtureLoader.h" #import "SharedTestUtilities/Date/FIRDateTestUtils.h" @@ -224,7 +224,8 @@ - (void)testGetAppCheckTokenSuccess { error:nil]; // 2.2. Return token from parsed response. FIRAppCheckToken *expectedToken = [[FIRAppCheckToken alloc] initWithToken:@"app_check_token" - expirationDate:[NSDate date]]; + expirationDate:[NSDate date] + receivedAtDate:[NSDate date]]; [self expectTokenWithAPIReponse:validAPIResponse toReturnToken:expectedToken]; // 3. Send request. @@ -240,6 +241,7 @@ - (void)testGetAppCheckTokenSuccess { XCTAssertEqualObjects(promise.value, expectedToken); XCTAssertEqualObjects(promise.value.token, expectedToken.token); XCTAssertEqualObjects(promise.value.expirationDate, expectedToken.expirationDate); + XCTAssertEqualObjects(promise.value.receivedAtDate, expectedToken.receivedAtDate); OCMVerifyAll(self.mockAPIService); } diff --git a/FirebaseAppCheck/Tests/Unit/Core/FIRAppCheckStorageTests.m b/FirebaseAppCheck/Tests/Unit/Core/FIRAppCheckStorageTests.m index 694accfde60..a4bd7bbaff0 100644 --- a/FirebaseAppCheck/Tests/Unit/Core/FIRAppCheckStorageTests.m +++ b/FirebaseAppCheck/Tests/Unit/Core/FIRAppCheckStorageTests.m @@ -21,7 +21,7 @@ #import #import "FBLPromise+Testing.h" -#import "FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRAppCheckToken.h" +#import "FirebaseAppCheck/Sources/Core/FIRAppCheckToken+Internal.h" @interface FIRAppCheckStorageTests : XCTestCase @property(nonatomic) NSString *appName; @@ -47,7 +47,8 @@ - (void)tearDown { - (void)testSetAndGetToken { FIRAppCheckToken *tokenToStore = [[FIRAppCheckToken alloc] initWithToken:@"token" - expirationDate:[NSDate distantPast]]; + expirationDate:[NSDate distantPast] + receivedAtDate:[NSDate date]]; FBLPromise *setPromise = [self.storage setToken:tokenToStore]; XCTAssert(FBLWaitForPromisesWithTimeout(0.5)); @@ -58,6 +59,7 @@ - (void)testSetAndGetToken { XCTAssert(FBLWaitForPromisesWithTimeout(0.5)); XCTAssertEqualObjects(getPromise.value.token, tokenToStore.token); XCTAssertEqualObjects(getPromise.value.expirationDate, tokenToStore.expirationDate); + XCTAssertEqualObjects(getPromise.value.receivedAtDate, tokenToStore.receivedAtDate); XCTAssertNil(getPromise.error); } @@ -76,7 +78,8 @@ - (void)testRemoveToken { - (void)testSetTokenPerApp { // 1. Set token with a storage. FIRAppCheckToken *tokenToStore = [[FIRAppCheckToken alloc] initWithToken:@"token" - expirationDate:[NSDate distantPast]]; + expirationDate:[NSDate distantPast] + receivedAtDate:[NSDate date]]; FBLPromise *setPromise = [self.storage setToken:tokenToStore]; XCTAssert(FBLWaitForPromisesWithTimeout(0.5)); diff --git a/FirebaseAppCheck/Tests/Unit/Core/FIRAppCheckStoredTokenTests.m b/FirebaseAppCheck/Tests/Unit/Core/FIRAppCheckStoredTokenTests.m index 4b6cf85b6d9..8281527904f 100644 --- a/FirebaseAppCheck/Tests/Unit/Core/FIRAppCheckStoredTokenTests.m +++ b/FirebaseAppCheck/Tests/Unit/Core/FIRAppCheckStoredTokenTests.m @@ -16,9 +16,9 @@ #import +#import "FirebaseAppCheck/Sources/Core/FIRAppCheckToken+Internal.h" #import "FirebaseAppCheck/Sources/Core/Storage/FIRAppCheckStoredToken+FIRAppCheckToken.h" #import "FirebaseAppCheck/Sources/Core/Storage/FIRAppCheckStoredToken.h" -#import "FirebaseAppCheck/Sources/Core/FIRAppCheckToken+Internal.h" #import @@ -53,7 +53,8 @@ - (void)testSecureCoding { - (void)testConvertingToAndFromFIRAppCheckToken { FIRAppCheckToken *originalToken = [[FIRAppCheckToken alloc] initWithToken:@"___" - expirationDate:[NSDate date]]; + expirationDate:[NSDate date] + receivedAtDate:[NSDate date]]; FIRAppCheckStoredToken *storedToken = [[FIRAppCheckStoredToken alloc] init]; [storedToken updateWithToken:originalToken]; diff --git a/FirebaseAppCheck/Tests/Unit/DebugProvider/FIRAppCheckDebugProviderTests.m b/FirebaseAppCheck/Tests/Unit/DebugProvider/FIRAppCheckDebugProviderTests.m index 5b80c100db7..a7f185178d0 100644 --- a/FirebaseAppCheck/Tests/Unit/DebugProvider/FIRAppCheckDebugProviderTests.m +++ b/FirebaseAppCheck/Tests/Unit/DebugProvider/FIRAppCheckDebugProviderTests.m @@ -19,9 +19,9 @@ #import #import "FBLPromise+Testing.h" +#import "FirebaseAppCheck/Sources/Core/FIRAppCheckToken+Internal.h" #import "FirebaseAppCheck/Sources/DebugProvider/API/FIRAppCheckDebugProviderAPIService.h" #import "FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRAppCheckDebugProvider.h" -#import "FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRAppCheckToken.h" #import "FirebaseCore/Sources/Private/FirebaseCoreInternal.h" @@ -130,7 +130,8 @@ - (void)testGetTokenSuccess { // 1. Stub API service. NSString *expectedDebugToken = [self.provider currentDebugToken]; FIRAppCheckToken *validToken = [[FIRAppCheckToken alloc] initWithToken:@"valid_token" - expirationDate:[NSDate date]]; + expirationDate:[NSDate date] + receivedAtDate:[NSDate date]]; OCMExpect([self.fakeAPIService appCheckTokenWithDebugToken:expectedDebugToken]) .andReturn([FBLPromise resolvedWith:validToken]); @@ -139,6 +140,7 @@ - (void)testGetTokenSuccess { XCTAssertNil(error); XCTAssertEqualObjects(token.token, validToken.token); XCTAssertEqualObjects(token.expirationDate, validToken.expirationDate); + XCTAssertEqualObjects(token.receivedAtDate, validToken.receivedAtDate); }]; // 3. Verify fakes. diff --git a/FirebaseAppCheck/Tests/Unit/DeviceCheckProvider/FIRDeviceCheckProviderTests.m b/FirebaseAppCheck/Tests/Unit/DeviceCheckProvider/FIRDeviceCheckProviderTests.m index b2776e6f380..6e54d463a36 100644 --- a/FirebaseAppCheck/Tests/Unit/DeviceCheckProvider/FIRDeviceCheckProviderTests.m +++ b/FirebaseAppCheck/Tests/Unit/DeviceCheckProvider/FIRDeviceCheckProviderTests.m @@ -19,9 +19,9 @@ #import #import "FBLPromise+Testing.h" +#import "FirebaseAppCheck/Sources/Core/FIRAppCheckToken+Internal.h" #import "FirebaseAppCheck/Sources/DeviceCheckProvider/API/FIRDeviceCheckAPIService.h" #import "FirebaseAppCheck/Sources/DeviceCheckProvider/FIRDeviceCheckTokenGenerator.h" -#import "FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRAppCheckToken.h" #import "FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRDeviceCheckProvider.h" #import "FirebaseCore/Sources/Private/FirebaseCoreInternal.h" @@ -90,7 +90,8 @@ - (void)testGetTokenSuccess { // 2. Expect FAA token to be requested. FIRAppCheckToken *validToken = [[FIRAppCheckToken alloc] initWithToken:@"valid_token" - expirationDate:[NSDate distantFuture]]; + expirationDate:[NSDate distantFuture] + receivedAtDate:[NSDate date]]; OCMExpect([self.fakeAPIService appCheckTokenWithDeviceToken:deviceToken]) .andReturn([FBLPromise resolvedWith:validToken]); @@ -102,6 +103,7 @@ - (void)testGetTokenSuccess { [completionExpectation fulfill]; XCTAssertEqualObjects(token.token, validToken.token); XCTAssertEqualObjects(token.expirationDate, validToken.expirationDate); + XCTAssertEqualObjects(token.receivedAtDate, validToken.receivedAtDate); XCTAssertNil(error); }]; From 03178550301ca2caf58752601089b3c770a2761a Mon Sep 17 00:00:00 2001 From: Maksym Malyhin Date: Thu, 3 Jun 2021 17:18:59 -0400 Subject: [PATCH 18/37] [WIP] Use FIRAppCheckTokenRefreshResult instead in the refresher API --- FirebaseAppCheck/Sources/Core/FIRAppCheck.m | 2 +- .../FIRAppCheckTokenRefreshResult.h | 60 +++++++++++++++++ .../FIRAppCheckTokenRefreshResult.m | 64 +++++++++++++++++++ .../TokenRefresh/FIRAppCheckTokenRefresher.h | 18 +++--- .../TokenRefresh/FIRAppCheckTokenRefresher.m | 53 ++++++++------- 5 files changed, 163 insertions(+), 34 deletions(-) create mode 100644 FirebaseAppCheck/Sources/Core/TokenRefresh/FIRAppCheckTokenRefreshResult.h create mode 100644 FirebaseAppCheck/Sources/Core/TokenRefresh/FIRAppCheckTokenRefreshResult.m diff --git a/FirebaseAppCheck/Sources/Core/FIRAppCheck.m b/FirebaseAppCheck/Sources/Core/FIRAppCheck.m index 99580e2047d..09010e4d7b8 100644 --- a/FirebaseAppCheck/Sources/Core/FIRAppCheck.m +++ b/FirebaseAppCheck/Sources/Core/FIRAppCheck.m @@ -125,7 +125,7 @@ - (nullable instancetype)initWithApp:(FIRApp *)app { userDefault:[NSUserDefaults standardUserDefaults] mainBundle:[NSBundle mainBundle]]; FIRAppCheckTokenRefresher *tokenRefresher = - [[FIRAppCheckTokenRefresher alloc] initWithTokenExpirationDate:[NSDate date] + [[FIRAppCheckTokenRefresher alloc] initWithRefreshResult:[NSDate date] tokenExpirationThreshold:kTokenExpirationThreshold settings:settings]; diff --git a/FirebaseAppCheck/Sources/Core/TokenRefresh/FIRAppCheckTokenRefreshResult.h b/FirebaseAppCheck/Sources/Core/TokenRefresh/FIRAppCheckTokenRefreshResult.h new file mode 100644 index 00000000000..db814b82e78 --- /dev/null +++ b/FirebaseAppCheck/Sources/Core/TokenRefresh/FIRAppCheckTokenRefreshResult.h @@ -0,0 +1,60 @@ +/* + * Copyright 2021 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 + +NS_ASSUME_NONNULL_BEGIN + +/// Represents possible results of a Firebase App Check token refresh attempt that matter for `FIRAppCheckTokenRefresher`. +typedef NS_ENUM(NSInteger, FIRAppCheckTokenRefreshStatus) { + // The token has not been refreshed. + FIRAppCheckTokenRefreshStatusNever, + + // The token was successfully refreshed. + FIRAppCheckTokenRefreshStatusSuccess, + + // The token refresh failed. + FIRAppCheckTokenRefreshStatusFailure +}; + +/// An object to pass the possible results of a Firebase App Check token refresh attempt and supplementary data. +@interface FIRAppCheckTokenRefreshResult : NSObject + +/// Status of the refresh. +@property(nonatomic, readonly) FIRAppCheckTokenRefreshStatus status; + +/// A date when the new Firebase App Check token is expiring. +@property(nonatomic, readonly, nullable) NSDate *tokenExpirationDate; + +/// A date when the new Firebase App Check token was received from the server. +@property(nonatomic, readonly, nullable) NSDate *tokenReceivedAtDate; + +- (instancetype)init NS_UNAVAILABLE; + +/// Initializes the instance with `FIRAppCheckTokenRefreshStatusNever`. +- (instancetype)initWithStatusNever; + +/// Initializes the instance with `FIRAppCheckTokenRefreshStatusFailure`. +- (instancetype)initWithStatusFailure; + +/// Initializes the instance with `FIRAppCheckTokenRefreshStatusFailure`. +/// @param tokenExpirationDate See `tokenExpirationDate` property. +/// @param tokenReceivedAtDate See `tokenReceivedAtDate` property. +- (instancetype)initWithStatusSuccessAndExpirationDate:(NSDate *)tokenExpirationDate receivedAtDate:(NSDate *)tokenReceivedAtDate; + +@end + +NS_ASSUME_NONNULL_END diff --git a/FirebaseAppCheck/Sources/Core/TokenRefresh/FIRAppCheckTokenRefreshResult.m b/FirebaseAppCheck/Sources/Core/TokenRefresh/FIRAppCheckTokenRefreshResult.m new file mode 100644 index 00000000000..edaf242dd47 --- /dev/null +++ b/FirebaseAppCheck/Sources/Core/TokenRefresh/FIRAppCheckTokenRefreshResult.m @@ -0,0 +1,64 @@ +/* + * Copyright 2021 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 "FirebaseAppCheck/Sources/Core/TokenRefresh/FIRAppCheckTokenRefreshResult.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FIRAppCheckTokenRefreshResult () + +- (instancetype)initWithStatus:(FIRAppCheckTokenRefreshStatus)status + expirationDate:(nullable NSDate *)tokenExpirationDate + receivedAtDate:(nullable NSDate *)tokenReceivedAtDate NS_DESIGNATED_INITIALIZER; + +@end + +@implementation FIRAppCheckTokenRefreshResult + +- (instancetype)initWithStatus:(FIRAppCheckTokenRefreshStatus)status + expirationDate:(nullable NSDate *)tokenExpirationDate + receivedAtDate:(nullable NSDate *)tokenReceivedAtDate { + self = [super init]; + if (self) { + _status = status; + _tokenExpirationDate = tokenExpirationDate; + _tokenReceivedAtDate = tokenReceivedAtDate; + } + return self; +} + +- (instancetype)initWithStatusNever { + return [self initWithStatus:FIRAppCheckTokenRefreshStatusNever + expirationDate:nil + receivedAtDate:nil]; +} + +- (instancetype)initWithStatusFailure { + return [self initWithStatus:FIRAppCheckTokenRefreshStatusFailure + expirationDate:nil + receivedAtDate:nil]; +} + +- (instancetype)initWithStatusSuccessAndExpirationDate:(NSDate *)tokenExpirationDate + receivedAtDate:(NSDate *)tokenReceivedAtDate { + return [self initWithStatus:FIRAppCheckTokenRefreshStatusSuccess + expirationDate:tokenExpirationDate + receivedAtDate:tokenReceivedAtDate]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/FirebaseAppCheck/Sources/Core/TokenRefresh/FIRAppCheckTokenRefresher.h b/FirebaseAppCheck/Sources/Core/TokenRefresh/FIRAppCheckTokenRefresher.h index 5aee56c828d..c8b32555deb 100644 --- a/FirebaseAppCheck/Sources/Core/TokenRefresh/FIRAppCheckTokenRefresher.h +++ b/FirebaseAppCheck/Sources/Core/TokenRefresh/FIRAppCheckTokenRefresher.h @@ -19,15 +19,14 @@ #import "FirebaseAppCheck/Sources/Core/TokenRefresh/FIRAppCheckTimer.h" @protocol FIRAppCheckSettingsProtocol; +@class FIRAppCheckTokenRefreshResult; NS_ASSUME_NONNULL_BEGIN /** The block to be called on the token refresh completion. - * @param success If refresh was successful. - * @param tokenExpirationDate The date when the token will expire. + * @param refreshResult The refresh result. */ -typedef void (^FIRAppCheckTokenRefreshCompletion)(BOOL success, - NSDate *_Nullable tokenExpirationDate); +typedef void (^FIRAppCheckTokenRefreshCompletion)(FIRAppCheckTokenRefreshResult *refreshResult); /** The block that will be called by `FIRAppCheckTokenRefresher` to trigger the token refresh. * @param completion The block that the client must call when the token refresh was completed. @@ -42,8 +41,8 @@ typedef void (^FIRAppCheckTokenRefreshBlock)(FIRAppCheckTokenRefreshCompletion c /// Updates the next refresh date based on the new token expiration date. This method should be /// called when the token update was initiated not by the refresher. -/// @param tokenExpirationDate A new token expiration date. -- (void)updateTokenExpirationDate:(NSDate *)tokenExpirationDate; +/// @param refreshResult A result of a refresh attempt. +- (void)updateWithRefreshResult:(FIRAppCheckTokenRefreshResult *)refreshResult; @end @@ -54,19 +53,18 @@ typedef void (^FIRAppCheckTokenRefreshBlock)(FIRAppCheckTokenRefreshCompletion c - (instancetype)init NS_UNAVAILABLE; /// The designated initializer. -/// @param tokenExpirationDate The initial token expiration date when known. Pass current date or -/// date in the past to trigger refresh once `tokenRefreshHandler` is set. +/// @param refreshResult A previous token refresh attempt result. /// @param tokenExpirationThreshold The token refresh will be triggered `tokenExpirationThreshold` /// seconds before the actual token expiration time. /// @param settings An object that handles Firebase app check settings. -- (instancetype)initWithTokenExpirationDate:(NSDate *)tokenExpirationDate +- (instancetype)initWithRefreshResult:(FIRAppCheckTokenRefreshResult *)refreshResult tokenExpirationThreshold:(NSTimeInterval)tokenExpirationThreshold timerProvider:(FIRTimerProvider)timerProvider settings:(id)settings NS_DESIGNATED_INITIALIZER; /// A convenience initializer with a timer provider returning an instance of `FIRAppCheckTimer`. -- (instancetype)initWithTokenExpirationDate:(NSDate *)tokenExpirationDate +- (instancetype)initWithRefreshResult:(FIRAppCheckTokenRefreshResult *)refreshResult tokenExpirationThreshold:(NSTimeInterval)tokenExpirationThreshold settings:(id)settings; diff --git a/FirebaseAppCheck/Sources/Core/TokenRefresh/FIRAppCheckTokenRefresher.m b/FirebaseAppCheck/Sources/Core/TokenRefresh/FIRAppCheckTokenRefresher.m index 07d7bd9a5d9..19d9529575f 100644 --- a/FirebaseAppCheck/Sources/Core/TokenRefresh/FIRAppCheckTokenRefresher.m +++ b/FirebaseAppCheck/Sources/Core/TokenRefresh/FIRAppCheckTokenRefresher.m @@ -18,6 +18,7 @@ #import "FirebaseAppCheck/Sources/Core/FIRAppCheckSettings.h" #import "FirebaseAppCheck/Sources/Core/TokenRefresh/FIRAppCheckTimer.h" +#import "FirebaseAppCheck/Sources/Core/TokenRefresh/FIRAppCheckTokenRefreshResult.h" NS_ASSUME_NONNULL_BEGIN @@ -34,7 +35,8 @@ @interface FIRAppCheckTokenRefresher () @property(atomic, nullable) id timer; @property(atomic) NSUInteger retryCount; -@property(nonatomic, nullable) NSDate *initialTokenExpirationDate; +/// Initial refresh result to be used when `tokenRefreshHandler` has been sent. +@property(nonatomic, nullable) FIRAppCheckTokenRefreshResult *initialRefreshResult; @property(nonatomic, readonly) NSTimeInterval tokenExpirationThreshold; @end @@ -43,7 +45,7 @@ @implementation FIRAppCheckTokenRefresher @synthesize tokenRefreshHandler = _tokenRefreshHandler; -- (instancetype)initWithTokenExpirationDate:(NSDate *)tokenExpirationDate +- (instancetype)initWithRefreshResult:(FIRAppCheckTokenRefreshResult *)refreshResult tokenExpirationThreshold:(NSTimeInterval)tokenExpirationThreshold timerProvider:(FIRTimerProvider)timerProvider settings:(id)settings { @@ -52,17 +54,17 @@ - (instancetype)initWithTokenExpirationDate:(NSDate *)tokenExpirationDate _refreshQueue = dispatch_queue_create("com.firebase.FIRAppCheckTokenRefresher", DISPATCH_QUEUE_SERIAL); _tokenExpirationThreshold = tokenExpirationThreshold; - _initialTokenExpirationDate = tokenExpirationDate; + _initialRefreshResult = refreshResult; _timerProvider = timerProvider; _settings = settings; } return self; } -- (instancetype)initWithTokenExpirationDate:(NSDate *)tokenExpirationDate +- (instancetype)initWithRefreshResult:(FIRAppCheckTokenRefreshResult *)refreshResult tokenExpirationThreshold:(NSTimeInterval)tokenExpirationThreshold settings:(id)settings { - return [self initWithTokenExpirationDate:tokenExpirationDate + return [self initWithRefreshResult:refreshResult tokenExpirationThreshold:tokenExpirationThreshold timerProvider:[FIRAppCheckTimer timerProvider] settings:settings]; @@ -77,11 +79,11 @@ - (void)setTokenRefreshHandler:(FIRAppCheckTokenRefreshBlock)tokenRefreshHandler _tokenRefreshHandler = tokenRefreshHandler; // Check if handler is being set for the first time and if yes then schedule first refresh. - if (tokenRefreshHandler && self.initialTokenExpirationDate && + if (tokenRefreshHandler && self.initialRefreshResult && self.settings.isTokenAutoRefreshEnabled) { - NSDate *initialTokenExpirationDate = self.initialTokenExpirationDate; - self.initialTokenExpirationDate = nil; - [self scheduleWithTokenExpirationDate:initialTokenExpirationDate]; + FIRAppCheckTokenRefreshResult *initialTokenRefreshResult = self.initialRefreshResult; + self.initialRefreshResult = nil; + [self scheduleWithTokenRefreshResult:initialTokenRefreshResult]; } } } @@ -92,9 +94,9 @@ - (FIRAppCheckTokenRefreshBlock)tokenRefreshHandler { } } -- (void)updateTokenExpirationDate:(NSDate *)tokenExpirationDate { +- (void)updateWithRefreshResult:(FIRAppCheckTokenRefreshResult *)refreshResult { if (self.settings.isTokenAutoRefreshEnabled) { - [self scheduleWithTokenExpirationDate:tokenExpirationDate]; + [self scheduleWithTokenRefreshResult:refreshResult]; } } @@ -108,24 +110,29 @@ - (void)refresh { } __auto_type __weak weakSelf = self; - self.tokenRefreshHandler(^(BOOL success, NSDate *_Nullable tokenExpirationDate) { + self.tokenRefreshHandler(^(FIRAppCheckTokenRefreshResult *refreshResult) { __auto_type strongSelf = weakSelf; - [strongSelf tokenRefreshedWithSuccess:success tokenExpirationDate:tokenExpirationDate]; + [strongSelf tokenRefreshedWithResult:refreshResult]; }); } -- (void)tokenRefreshedWithSuccess:(BOOL)success tokenExpirationDate:(NSDate *)tokenExpirationDate { - if (success) { - self.retryCount = 0; - } else { - self.retryCount += 1; +- (void)tokenRefreshedWithResult:(FIRAppCheckTokenRefreshResult *)refreshResult { + switch (refreshResult.status) { + case FIRAppCheckTokenRefreshStatusNever: + case FIRAppCheckTokenRefreshStatusSuccess: + self.retryCount = 0; + break; + + case FIRAppCheckTokenRefreshStatusFailure: + self.retryCount += 1; + break; } - [self scheduleWithTokenExpirationDate:tokenExpirationDate ?: [NSDate date]]; + [self scheduleWithTokenRefreshResult:refreshResult]; } -- (void)scheduleWithTokenExpirationDate:(NSDate *)tokenExpirationDate { - NSDate *refreshDate = [self nextRefreshDateWithTokenExpirationDate:tokenExpirationDate]; +- (void)scheduleWithTokenRefreshResult:(FIRAppCheckTokenRefreshResult *)refreshResult { + NSDate *refreshDate = [self nextRefreshDateWithTokenRefreshResult:refreshResult]; [self scheduleRefreshAtDate:refreshDate]; } @@ -155,9 +162,9 @@ - (void)cancelTimer { #pragma mark - Backoff -- (NSDate *)nextRefreshDateWithTokenExpirationDate:(NSDate *)tokenExpirationDate { +- (NSDate *)nextRefreshDateWithTokenRefreshResult:(FIRAppCheckTokenRefreshResult *)refreshResult { NSDate *targetRefreshDate = - [tokenExpirationDate dateByAddingTimeInterval:-self.tokenExpirationThreshold]; + [refreshResult.tokenExpirationDate dateByAddingTimeInterval:-self.tokenExpirationThreshold]; NSTimeInterval scheduleIn = [targetRefreshDate timeIntervalSinceNow]; NSTimeInterval backoffTime = [[self class] backoffTimeForRetryCount:self.retryCount]; From 8c40b6d1c3adf11bd88fb27738b68cbaa0d9bc08 Mon Sep 17 00:00:00 2001 From: Maksym Malyhin Date: Fri, 4 Jun 2021 10:29:56 -0400 Subject: [PATCH 19/37] [WIP] Fix refgresher usage --- FirebaseAppCheck/Sources/Core/FIRAppCheck.m | 15 ++++++++++----- .../Tests/Unit/Core/FIRAppCheckIntegrationTests.m | 2 +- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/FirebaseAppCheck/Sources/Core/FIRAppCheck.m b/FirebaseAppCheck/Sources/Core/FIRAppCheck.m index 09010e4d7b8..28cd703d722 100644 --- a/FirebaseAppCheck/Sources/Core/FIRAppCheck.m +++ b/FirebaseAppCheck/Sources/Core/FIRAppCheck.m @@ -24,7 +24,7 @@ #import "FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRAppCheckProvider.h" #import "FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRAppCheckProviderFactory.h" -#import "FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRAppCheckToken.h" +#import "FirebaseAppCheck/Sources/Core/FIRAppCheckToken+Internal.h" #import "FirebaseAppCheck/Sources/Core/Errors/FIRAppCheckErrorUtil.h" #import "FirebaseAppCheck/Sources/Core/FIRAppCheckLogger.h" @@ -32,6 +32,7 @@ #import "FirebaseAppCheck/Sources/Core/FIRAppCheckTokenResult.h" #import "FirebaseAppCheck/Sources/Core/Storage/FIRAppCheckStorage.h" #import "FirebaseAppCheck/Sources/Core/TokenRefresh/FIRAppCheckTokenRefresher.h" +#import "FirebaseAppCheck/Sources/Core/TokenRefresh/FIRAppCheckTokenRefreshResult.h" #import "FirebaseAppCheck/Sources/Interop/FIRAppCheckInterop.h" #import "FirebaseAppCheck/Sources/Interop/FIRAppCheckTokenResultInterop.h" @@ -124,8 +125,9 @@ - (nullable instancetype)initWithApp:(FIRApp *)app { [[FIRAppCheckSettings alloc] initWithApp:app userDefault:[NSUserDefaults standardUserDefaults] mainBundle:[NSBundle mainBundle]]; + FIRAppCheckTokenRefreshResult *refreshResult = [[FIRAppCheckTokenRefreshResult alloc] initWithStatusNever]; FIRAppCheckTokenRefresher *tokenRefresher = - [[FIRAppCheckTokenRefresher alloc] initWithRefreshResult:[NSDate date] + [[FIRAppCheckTokenRefresher alloc] initWithRefreshResult:refreshResult tokenExpirationThreshold:kTokenExpirationThreshold settings:settings]; @@ -306,7 +308,8 @@ - (nonnull NSString *)notificationTokenKey { // 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. - [self.tokenRefresher updateTokenExpirationDate:token.expirationDate]; + __auto_type refreshResult = [[FIRAppCheckTokenRefreshResult alloc] initWithStatusSuccessAndExpirationDate:token.expirationDate receivedAtDate:token.receivedAtDate]; + [self.tokenRefresher updateWithRefreshResult:refreshResult]; [self postTokenUpdateNotificationWithToken:token]; return token; }); @@ -317,11 +320,13 @@ - (nonnull NSString *)notificationTokenKey { - (void)periodicTokenRefreshWithCompletion:(FIRAppCheckTokenRefreshCompletion)completion { [self retrieveOrRefreshTokenForcingRefresh:NO] .then(^id _Nullable(FIRAppCheckToken *_Nullable token) { - completion(YES, token.expirationDate); + __auto_type refreshResult = [[FIRAppCheckTokenRefreshResult alloc] initWithStatusSuccessAndExpirationDate:token.expirationDate receivedAtDate:token.receivedAtDate]; + completion(refreshResult); return nil; }) .catch(^(NSError *error) { - completion(NO, nil); + __auto_type refreshResult = [[FIRAppCheckTokenRefreshResult alloc] initWithStatusFailure]; + completion(refreshResult); }); } diff --git a/FirebaseAppCheck/Tests/Unit/Core/FIRAppCheckIntegrationTests.m b/FirebaseAppCheck/Tests/Unit/Core/FIRAppCheckIntegrationTests.m index 4e43b2e9b45..7118345cb64 100644 --- a/FirebaseAppCheck/Tests/Unit/Core/FIRAppCheckIntegrationTests.m +++ b/FirebaseAppCheck/Tests/Unit/Core/FIRAppCheckIntegrationTests.m @@ -204,7 +204,7 @@ - (void)usageExample { - (void)disableTokenRefresher { self.mockTokenRefresher = OCMClassMock([FIRAppCheckTokenRefresher class]); OCMStub([self.mockTokenRefresher alloc]).andReturn(self.mockTokenRefresher); - OCMStub([self.mockTokenRefresher initWithTokenExpirationDate:[OCMArg any] + OCMStub([self.mockTokenRefresher initWithRefreshResult:[OCMArg any] tokenExpirationThreshold:5 * 60 settings:[OCMArg any]]) .andReturn(self.mockTokenRefresher); From e0e570caf363cd0c4aee83b18f0729cb9a90cee9 Mon Sep 17 00:00:00 2001 From: Maksym Malyhin Date: Fri, 4 Jun 2021 11:58:29 -0400 Subject: [PATCH 20/37] Update tests with new API, keep old logic --- .../Tests/Unit/Core/FIRAppCheckTests.m | 23 ++++++---- .../Core/FIRAppCheckTokenRefresherTests.m | 46 +++++++++++-------- 2 files changed, 40 insertions(+), 29 deletions(-) diff --git a/FirebaseAppCheck/Tests/Unit/Core/FIRAppCheckTests.m b/FirebaseAppCheck/Tests/Unit/Core/FIRAppCheckTests.m index 0fc0c7a5578..11302c915ac 100644 --- a/FirebaseAppCheck/Tests/Unit/Core/FIRAppCheckTests.m +++ b/FirebaseAppCheck/Tests/Unit/Core/FIRAppCheckTests.m @@ -32,6 +32,7 @@ #import "FirebaseAppCheck/Sources/Core/FIRAppCheckTokenResult.h" #import "FirebaseAppCheck/Sources/Core/Storage/FIRAppCheckStorage.h" #import "FirebaseAppCheck/Sources/Core/TokenRefresh/FIRAppCheckTokenRefresher.h" +#import "FirebaseAppCheck/Sources/Core/TokenRefresh/FIRAppCheckTokenRefreshResult.h" #import "FirebaseCore/Sources/Private/FirebaseCoreInternal.h" @@ -114,9 +115,10 @@ - (void)testInitWithApp { id mockTokenRefresher = OCMClassMock([FIRAppCheckTokenRefresher class]); OCMExpect([mockTokenRefresher alloc]).andReturn(mockTokenRefresher); - id refresherDateValidator = [OCMArg checkWithBlock:^BOOL(NSDate *tokenExpirationDate) { - NSTimeInterval accuracy = 1; - XCTAssertLessThanOrEqual(ABS([tokenExpirationDate timeIntervalSinceNow]), accuracy); + id refresherDateValidator = [OCMArg checkWithBlock:^BOOL(FIRAppCheckTokenRefreshResult *refreshResult) { + XCTAssertEqual(refreshResult.status, FIRAppCheckTokenRefreshStatusNever); + XCTAssertEqual(refreshResult.tokenExpirationDate, nil); + XCTAssertEqual(refreshResult.tokenReceivedAtDate, nil); return YES; }]; @@ -125,7 +127,7 @@ - (void)testInitWithApp { return YES; }]; - OCMExpect([mockTokenRefresher initWithTokenExpirationDate:refresherDateValidator + OCMExpect([mockTokenRefresher initWithRefreshResult:refresherDateValidator tokenExpirationThreshold:5 * 60 settings:settingsValidator]) .andReturn(mockTokenRefresher); @@ -381,10 +383,10 @@ - (void)testTokenRefreshTriggeredAndRefreshSuccess { } XCTestExpectation *completionExpectation = [self expectationWithDescription:@"completion"]; - self.tokenRefreshHandler(^(BOOL success, NSDate *_Nullable tokenExpirationDate) { + self.tokenRefreshHandler(^(FIRAppCheckTokenRefreshResult *refreshResult) { [completionExpectation fulfill]; - XCTAssertEqual(tokenExpirationDate, expirationDate); - XCTAssertTrue(success); + XCTAssertEqualObjects(refreshResult.tokenExpirationDate, expirationDate); + XCTAssertEqual(refreshResult.status, FIRAppCheckTokenRefreshStatusSuccess); }); [self waitForExpectations:@[ notificationExpectation, completionExpectation ] timeout:0.5]; @@ -415,10 +417,11 @@ - (void)testTokenRefreshTriggeredAndRefreshError { } XCTestExpectation *completionExpectation = [self expectationWithDescription:@"completion"]; - self.tokenRefreshHandler(^(BOOL success, NSDate *_Nullable tokenExpirationDate) { + self.tokenRefreshHandler(^(FIRAppCheckTokenRefreshResult *refreshResult) { [completionExpectation fulfill]; - XCTAssertNil(tokenExpirationDate); - XCTAssertFalse(success); + XCTAssertEqual(refreshResult.status, FIRAppCheckTokenRefreshStatusFailure); + XCTAssertNil(refreshResult.tokenExpirationDate); + XCTAssertNil(refreshResult.tokenReceivedAtDate); }); [self waitForExpectations:@[ notificationExpectation, completionExpectation ] timeout:0.5]; diff --git a/FirebaseAppCheck/Tests/Unit/Core/FIRAppCheckTokenRefresherTests.m b/FirebaseAppCheck/Tests/Unit/Core/FIRAppCheckTokenRefresherTests.m index 747aba25da0..d7d4c6b109a 100644 --- a/FirebaseAppCheck/Tests/Unit/Core/FIRAppCheckTokenRefresherTests.m +++ b/FirebaseAppCheck/Tests/Unit/Core/FIRAppCheckTokenRefresherTests.m @@ -20,6 +20,7 @@ #import "FirebaseAppCheck/Sources/Core/FIRAppCheckSettings.h" #import "FirebaseAppCheck/Sources/Core/TokenRefresh/FIRAppCheckTokenRefresher.h" +#import "FirebaseAppCheck/Sources/Core/TokenRefresh/FIRAppCheckTokenRefreshResult.h" #import "FirebaseAppCheck/Tests/Unit/Utils/FIRFakeTimer.h" @interface FIRAppCheckTokenRefresherTests : XCTestCase @@ -28,7 +29,7 @@ @interface FIRAppCheckTokenRefresherTests : XCTestCase @property(nonatomic) OCMockObject *mockSettings; -@property(nonatomic) NSDate *initialTokenExpirationDate; +@property(nonatomic) FIRAppCheckTokenRefreshResult *initialTokenRefreshResult; @property(nonatomic) NSTimeInterval tokenExpirationThreshold; @end @@ -38,7 +39,10 @@ @implementation FIRAppCheckTokenRefresherTests - (void)setUp { self.mockSettings = OCMProtocolMock(@protocol(FIRAppCheckSettingsProtocol)); self.fakeTimer = [[FIRFakeTimer alloc] init]; - self.initialTokenExpirationDate = [NSDate dateWithTimeIntervalSinceNow:1000]; + + NSDate *receivedAtDate = [NSDate date]; + self.initialTokenRefreshResult = [[FIRAppCheckTokenRefreshResult alloc] initWithStatusSuccessAndExpirationDate:[receivedAtDate dateByAddingTimeInterval:1000] receivedAtDate:receivedAtDate]; + self.tokenExpirationThreshold = 1 * 60; } @@ -58,7 +62,7 @@ - (void)testInitialRefreshWhenAutoRefreshAllowed { // 2. Expect timer to be scheduled. NSDate *expectedTimerFireDate = - [self.initialTokenExpirationDate dateByAddingTimeInterval:-self.tokenExpirationThreshold]; + [self.initialTokenRefreshResult.tokenExpirationDate dateByAddingTimeInterval:-self.tokenExpirationThreshold]; XCTestExpectation *timerCreateExpectation = [self expectationWithDescription:@"create timer"]; __auto_type weakSelf = self; @@ -69,7 +73,7 @@ - (void)testInitialRefreshWhenAutoRefreshAllowed { }; // 3. Expect refresh handler to be called. - NSDate *refreshedTokenExpirationDate = [expectedTimerFireDate dateByAddingTimeInterval:60 * 60]; + __auto_type refreshResult = [[FIRAppCheckTokenRefreshResult alloc] initWithStatusSuccessAndExpirationDate:[expectedTimerFireDate dateByAddingTimeInterval:60 * 60] receivedAtDate:expectedTimerFireDate]; XCTestExpectation *initialRefreshExpectation = [self expectationWithDescription:@"initial refresh"]; XCTestExpectation *noEarlyRefreshExpectation = @@ -80,7 +84,7 @@ - (void)testInitialRefreshWhenAutoRefreshAllowed { [noEarlyRefreshExpectation fulfill]; // Call completion. - completion(YES, refreshedTokenExpirationDate); + completion(refreshResult); }; // 4. Check if the handler is not fired before the timer. @@ -135,7 +139,8 @@ - (void)testNextRefreshOnRefreshSuccess { FIRAppCheckTokenRefresher *refresher = [self createRefresher]; NSDate *refreshedTokenExpirationDate = - [self.initialTokenExpirationDate dateByAddingTimeInterval:60 * 60]; + [self.initialTokenRefreshResult.tokenExpirationDate dateByAddingTimeInterval:60 * 60]; + __auto_type refreshResult = [[FIRAppCheckTokenRefreshResult alloc] initWithStatusSuccessAndExpirationDate:refreshedTokenExpirationDate receivedAtDate:self.initialTokenRefreshResult.tokenExpirationDate]; // 1. Expect checking if auto-refresh allowed before scheduling initial refresh. [[[self.mockSettings expect] andReturnValue:@(YES)] isTokenAutoRefreshEnabled]; @@ -149,7 +154,7 @@ - (void)testNextRefreshOnRefreshSuccess { // Call completion in a while. dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ - completion(YES, refreshedTokenExpirationDate); + completion(refreshResult); }); }; @@ -195,7 +200,8 @@ - (void)testBackoff { // Call completion in a while. dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ - completion(NO, nil); + __auto_type refreshFailure = [[FIRAppCheckTokenRefreshResult alloc] initWithStatusFailure]; + completion(refreshFailure); }); }; @@ -238,7 +244,7 @@ - (void)testNoInitialRefreshWhenAutoRefreshIsNotAllowed { // 2. Don't expect timer to be scheduled. NSDate *expectedTimerFireDate = - [self.initialTokenExpirationDate dateByAddingTimeInterval:-self.tokenExpirationThreshold]; + [self.initialTokenRefreshResult.tokenExpirationDate dateByAddingTimeInterval:-self.tokenExpirationThreshold]; XCTestExpectation *timerCreateExpectation = [self expectationWithDescription:@"create timer"]; timerCreateExpectation.inverted = YES; @@ -250,7 +256,7 @@ - (void)testNoInitialRefreshWhenAutoRefreshIsNotAllowed { }; // 3. Don't expect refresh handler to be called. - NSDate *refreshedTokenExpirationDate = [expectedTimerFireDate dateByAddingTimeInterval:60 * 60]; + __auto_type refreshResult = [[FIRAppCheckTokenRefreshResult alloc] initWithStatusSuccessAndExpirationDate:[expectedTimerFireDate dateByAddingTimeInterval:60 * 60] receivedAtDate:expectedTimerFireDate]; XCTestExpectation *refreshExpectation = [self expectationWithDescription:@"refresh"]; refreshExpectation.inverted = YES; @@ -258,7 +264,7 @@ - (void)testNoInitialRefreshWhenAutoRefreshIsNotAllowed { [refreshExpectation fulfill]; // Call completion. - completion(YES, refreshedTokenExpirationDate); + completion(refreshResult); }; // 4. Check if the handler is not fired before the timer. @@ -275,7 +281,7 @@ - (void)testNoRefreshWhenAutoRefreshWasDisabledAfterInit { // 2. Expect timer to be scheduled. NSDate *expectedTimerFireDate = - [self.initialTokenExpirationDate dateByAddingTimeInterval:-self.tokenExpirationThreshold]; + [self.initialTokenRefreshResult.tokenExpirationDate dateByAddingTimeInterval:-self.tokenExpirationThreshold]; XCTestExpectation *timerCreateExpectation = [self expectationWithDescription:@"create timer"]; __auto_type weakSelf = self; @@ -286,14 +292,14 @@ - (void)testNoRefreshWhenAutoRefreshWasDisabledAfterInit { }; // 3. Expect refresh handler to be called. - NSDate *refreshedTokenExpirationDate = [expectedTimerFireDate dateByAddingTimeInterval:60 * 60]; + __auto_type refreshResult = [[FIRAppCheckTokenRefreshResult alloc] initWithStatusSuccessAndExpirationDate:[expectedTimerFireDate dateByAddingTimeInterval:60 * 60] receivedAtDate:expectedTimerFireDate]; XCTestExpectation *noRefreshExpectation = [self expectationWithDescription:@"initial refresh"]; noRefreshExpectation.inverted = YES; refresher.tokenRefreshHandler = ^(FIRAppCheckTokenRefreshCompletion _Nonnull completion) { [noRefreshExpectation fulfill]; // Call completion. - completion(YES, refreshedTokenExpirationDate); + completion(refreshResult); }; // 4. Check if the handler is not fired before the timer. @@ -315,7 +321,8 @@ - (void)testNoRefreshWhenAutoRefreshWasDisabledAfterInit { - (void)testUpdateTokenExpirationDateWhenAutoRefreshIsAllowed { FIRAppCheckTokenRefresher *refresher = [self createRefresher]; - NSDate *newExpirationDate = [self.initialTokenExpirationDate dateByAddingTimeInterval:10 * 60]; + NSDate *newExpirationDate = [self.initialTokenRefreshResult.tokenExpirationDate dateByAddingTimeInterval:10 * 60]; + __auto_type newRefreshResult = [[FIRAppCheckTokenRefreshResult alloc] initWithStatusSuccessAndExpirationDate:newExpirationDate receivedAtDate:self.initialTokenRefreshResult.tokenExpirationDate]; // 1. Expect checking if auto-refresh allowed before scheduling initial refresh. [[[self.mockSettings expect] andReturnValue:@(YES)] isTokenAutoRefreshEnabled]; @@ -333,7 +340,7 @@ - (void)testUpdateTokenExpirationDateWhenAutoRefreshIsAllowed { }; // 3. Update token expiration date. - [refresher updateTokenExpirationDate:newExpirationDate]; + [refresher updateWithRefreshResult:newRefreshResult]; // 4. Wait for timer to be created. [self waitForExpectations:@[ timerCreateExpectation ] timeout:1]; @@ -344,7 +351,8 @@ - (void)testUpdateTokenExpirationDateWhenAutoRefreshIsAllowed { - (void)testUpdateTokenExpirationDateWhenAutoRefreshIsNotAllowed { FIRAppCheckTokenRefresher *refresher = [self createRefresher]; - NSDate *newExpirationDate = [self.initialTokenExpirationDate dateByAddingTimeInterval:10 * 60]; + NSDate *newExpirationDate = [self.initialTokenRefreshResult.tokenExpirationDate dateByAddingTimeInterval:10 * 60]; + __auto_type newRefreshResult = [[FIRAppCheckTokenRefreshResult alloc] initWithStatusSuccessAndExpirationDate:newExpirationDate receivedAtDate:self.initialTokenRefreshResult.tokenExpirationDate]; // 1. Expect checking if auto-refresh allowed before scheduling initial refresh. [[[self.mockSettings expect] andReturnValue:@(NO)] isTokenAutoRefreshEnabled]; @@ -363,7 +371,7 @@ - (void)testUpdateTokenExpirationDateWhenAutoRefreshIsNotAllowed { }; // 3. Update token expiration date. - [refresher updateTokenExpirationDate:newExpirationDate]; + [refresher updateWithRefreshResult:newRefreshResult]; // 4. Wait for timer to be created. [self waitForExpectations:@[ timerCreateExpectation ] timeout:1]; @@ -383,7 +391,7 @@ - (void)fireTimer { - (FIRAppCheckTokenRefresher *)createRefresher { return [[FIRAppCheckTokenRefresher alloc] - initWithTokenExpirationDate:self.initialTokenExpirationDate + initWithRefreshResult:self.initialTokenRefreshResult tokenExpirationThreshold:self.tokenExpirationThreshold timerProvider:[self.fakeTimer fakeTimerProvider] settings:self.mockSettings]; From 6b846fcc0d3188c729cc571ea2916eac33b0a29b Mon Sep 17 00:00:00 2001 From: Maksym Malyhin Date: Mon, 7 Jun 2021 16:28:45 -0400 Subject: [PATCH 21/37] Update tests with new logic --- .../Core/FIRAppCheckTokenRefresherTests.m | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/FirebaseAppCheck/Tests/Unit/Core/FIRAppCheckTokenRefresherTests.m b/FirebaseAppCheck/Tests/Unit/Core/FIRAppCheckTokenRefresherTests.m index d7d4c6b109a..e81714932f3 100644 --- a/FirebaseAppCheck/Tests/Unit/Core/FIRAppCheckTokenRefresherTests.m +++ b/FirebaseAppCheck/Tests/Unit/Core/FIRAppCheckTokenRefresherTests.m @@ -61,8 +61,7 @@ - (void)testInitialRefreshWhenAutoRefreshAllowed { [[[self.mockSettings expect] andReturnValue:@(YES)] isTokenAutoRefreshEnabled]; // 2. Expect timer to be scheduled. - NSDate *expectedTimerFireDate = - [self.initialTokenRefreshResult.tokenExpirationDate dateByAddingTimeInterval:-self.tokenExpirationThreshold]; + NSDate *expectedTimerFireDate = [self expectedRefreshDateWithReceivedDate:self.initialTokenRefreshResult.tokenReceivedAtDate expirationDate:self.initialTokenRefreshResult.tokenExpirationDate]; XCTestExpectation *timerCreateExpectation = [self expectationWithDescription:@"create timer"]; __auto_type weakSelf = self; @@ -160,7 +159,7 @@ - (void)testNextRefreshOnRefreshSuccess { // 3. Expect for new timer to be created. NSDate *expectedFireDate = - [refreshedTokenExpirationDate dateByAddingTimeInterval:-self.tokenExpirationThreshold]; + [self expectedRefreshDateWithReceivedDate:refreshResult.tokenReceivedAtDate expirationDate:refreshResult.tokenExpirationDate]; XCTestExpectation *createTimerExpectation = [self expectationWithDescription:@"create timer"]; self.fakeTimer.createHandler = ^(NSDate *_Nonnull fireDate) { [createTimerExpectation fulfill]; @@ -328,8 +327,7 @@ - (void)testUpdateTokenExpirationDateWhenAutoRefreshIsAllowed { [[[self.mockSettings expect] andReturnValue:@(YES)] isTokenAutoRefreshEnabled]; // 2. Expect timer to be scheduled. - NSDate *expectedTimerFireDate = - [newExpirationDate dateByAddingTimeInterval:-self.tokenExpirationThreshold]; + NSDate *expectedTimerFireDate = [self expectedRefreshDateWithReceivedDate:newRefreshResult.tokenReceivedAtDate expirationDate:newRefreshResult.tokenExpirationDate]; XCTestExpectation *timerCreateExpectation = [self expectationWithDescription:@"create timer"]; __auto_type weakSelf = self; @@ -397,4 +395,20 @@ - (FIRAppCheckTokenRefresher *)createRefresher { settings:self.mockSettings]; } +- (NSDate *)expectedRefreshDateWithReceivedDate:(NSDate *)receivedDate expirationDate:(NSDate *)expirationDate { + NSTimeInterval timeToLive = [expirationDate timeIntervalSinceDate:receivedDate]; + XCTAssertGreaterThanOrEqual(timeToLive, 0); + + NSTimeInterval timeToRefresh = timeToLive / 2 + 5 * 60; // 50% or TTL + 5 min + + NSTimeInterval minimalAutoRefreshInterval = 60; // 1 min + timeToRefresh = MAX(timeToRefresh, minimalAutoRefreshInterval); + + NSDate *refreshDate = [receivedDate dateByAddingTimeInterval:timeToRefresh]; + + NSDate *now = [NSDate date]; + + return [refreshDate laterDate:now]; +} + @end From 847fb982ec254f4642cb05673f3ebe55fe84d58e Mon Sep 17 00:00:00 2001 From: Maksym Malyhin Date: Mon, 7 Jun 2021 17:08:43 -0400 Subject: [PATCH 22/37] WIP --- .../TokenRefresh/FIRAppCheckTokenRefresher.m | 29 ++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/FirebaseAppCheck/Sources/Core/TokenRefresh/FIRAppCheckTokenRefresher.m b/FirebaseAppCheck/Sources/Core/TokenRefresh/FIRAppCheckTokenRefresher.m index 19d9529575f..4e169053947 100644 --- a/FirebaseAppCheck/Sources/Core/TokenRefresh/FIRAppCheckTokenRefresher.m +++ b/FirebaseAppCheck/Sources/Core/TokenRefresh/FIRAppCheckTokenRefresher.m @@ -25,6 +25,11 @@ static const NSTimeInterval kInitialBackoffTimeInterval = 30; static const NSTimeInterval kMaximumBackoffTimeInterval = 16 * 60; +static const NSTimeInterval kMinimumAutoRefreshTimeInterval = 60; // 1 min. + +/// How much time in advance to auto-refresh token before it's expiration. E.g. 0.5 means that the token will be refreshed half way through it's intended time to live. +static const double kAutoRefreshFraction = 0.5; + @interface FIRAppCheckTokenRefresher () @property(nonatomic, readonly) dispatch_queue_t refreshQueue; @@ -163,10 +168,31 @@ - (void)cancelTimer { #pragma mark - Backoff - (NSDate *)nextRefreshDateWithTokenRefreshResult:(FIRAppCheckTokenRefreshResult *)refreshResult { + + switch (refreshResult.status) { + case FIRAppCheckTokenRefreshStatusSuccess: + { + NSTimeInterval timeToLive = [refreshResult.tokenExpirationDate timeIntervalSinceDate:refreshResult.tokenReceivedAtDate]; + timeToLive = MAX(timeToLive, 0); + + // Refresh in 50% of TTL + 5 min. + NSTimeInterval targetRefreshSinceReceivedDate = timeToLive * kAutoRefreshFraction + 5 * 60; + NSDate *targetRefreshDate = + [refreshResult.tokenReceivedAtDate dateByAddingTimeInterval:targetRefreshSinceReceivedDate]; + } + break; + + default: + break; + } + + + NSDate *targetRefreshDate = - [refreshResult.tokenExpirationDate dateByAddingTimeInterval:-self.tokenExpirationThreshold]; + [refreshResult.tokenReceivedAtDate dateByAddingTimeInterval:targetRefreshSinceReceivedDate]; NSTimeInterval scheduleIn = [targetRefreshDate timeIntervalSinceNow]; + // Check NSTimeInterval backoffTime = [[self class] backoffTimeForRetryCount:self.retryCount]; if (scheduleIn >= backoffTime) { return targetRefreshDate; @@ -175,6 +201,7 @@ - (NSDate *)nextRefreshDateWithTokenRefreshResult:(FIRAppCheckTokenRefreshResult } } + + (NSTimeInterval)backoffTimeForRetryCount:(NSInteger)retryCount { if (retryCount == 0) { // No backoff for the first attempt. From 813c671a88a100f4e821e8025599e8b80191c524 Mon Sep 17 00:00:00 2001 From: Maksym Malyhin Date: Tue, 8 Jun 2021 10:40:16 -0400 Subject: [PATCH 23/37] WIP --- .../FIRAppCheckTokenRefreshResult.h | 2 + .../TokenRefresh/FIRAppCheckTokenRefresher.m | 38 ++++++++++--------- 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/FirebaseAppCheck/Sources/Core/TokenRefresh/FIRAppCheckTokenRefreshResult.h b/FirebaseAppCheck/Sources/Core/TokenRefresh/FIRAppCheckTokenRefreshResult.h index db814b82e78..fcda15a40b0 100644 --- a/FirebaseAppCheck/Sources/Core/TokenRefresh/FIRAppCheckTokenRefreshResult.h +++ b/FirebaseAppCheck/Sources/Core/TokenRefresh/FIRAppCheckTokenRefreshResult.h @@ -26,6 +26,8 @@ typedef NS_ENUM(NSInteger, FIRAppCheckTokenRefreshStatus) { // The token was successfully refreshed. FIRAppCheckTokenRefreshStatusSuccess, +// FIRAppCheckTokenRefreshStatusSuccess, + // The token refresh failed. FIRAppCheckTokenRefreshStatusFailure }; diff --git a/FirebaseAppCheck/Sources/Core/TokenRefresh/FIRAppCheckTokenRefresher.m b/FirebaseAppCheck/Sources/Core/TokenRefresh/FIRAppCheckTokenRefresher.m index 4e169053947..2177da25a2c 100644 --- a/FirebaseAppCheck/Sources/Core/TokenRefresh/FIRAppCheckTokenRefresher.m +++ b/FirebaseAppCheck/Sources/Core/TokenRefresh/FIRAppCheckTokenRefresher.m @@ -168,36 +168,38 @@ - (void)cancelTimer { #pragma mark - Backoff - (NSDate *)nextRefreshDateWithTokenRefreshResult:(FIRAppCheckTokenRefreshResult *)refreshResult { - switch (refreshResult.status) { - case FIRAppCheckTokenRefreshStatusSuccess: - { + case FIRAppCheckTokenRefreshStatusSuccess: { NSTimeInterval timeToLive = [refreshResult.tokenExpirationDate timeIntervalSinceDate:refreshResult.tokenReceivedAtDate]; timeToLive = MAX(timeToLive, 0); // Refresh in 50% of TTL + 5 min. NSTimeInterval targetRefreshSinceReceivedDate = timeToLive * kAutoRefreshFraction + 5 * 60; NSDate *targetRefreshDate = - [refreshResult.tokenReceivedAtDate dateByAddingTimeInterval:targetRefreshSinceReceivedDate]; + [refreshResult.tokenReceivedAtDate dateByAddingTimeInterval:targetRefreshSinceReceivedDate]; + + // Don't schedule later than expiration date. + NSDate *refreshDate = [targetRefreshDate earlierDate:refreshResult.tokenExpirationDate]; + + // Don't schedule an update earlier than in 1 min from now. + if ([refreshDate timeIntervalSinceNow] < kMinimumAutoRefreshTimeInterval) { + refreshDate = [NSDate dateWithTimeIntervalSinceNow:kMinimumAutoRefreshTimeInterval]; + } + return refreshDate; } break; - default: + case FIRAppCheckTokenRefreshStatusFailure: { + // Refresh after a timeout. + NSTimeInterval backoffTime = [[self class] backoffTimeForRetryCount:self.retryCount]; + return [NSDate dateWithTimeIntervalSinceNow:backoffTime]; + } break; - } - - - NSDate *targetRefreshDate = - [refreshResult.tokenReceivedAtDate dateByAddingTimeInterval:targetRefreshSinceReceivedDate]; - NSTimeInterval scheduleIn = [targetRefreshDate timeIntervalSinceNow]; - - // Check - NSTimeInterval backoffTime = [[self class] backoffTimeForRetryCount:self.retryCount]; - if (scheduleIn >= backoffTime) { - return targetRefreshDate; - } else { - return [NSDate dateWithTimeIntervalSinceNow:backoffTime]; + case FIRAppCheckTokenRefreshStatusNever: + // Refresh ASAP. + return [NSDate date]; + break; } } From 5987d2e16d32db86a9b351fc11b92dacd2c1e7cb Mon Sep 17 00:00:00 2001 From: Maksym Malyhin Date: Wed, 9 Jun 2021 11:44:14 -0400 Subject: [PATCH 24/37] Initial refresh tests and fixes. --- .../TokenRefresh/FIRAppCheckTokenRefresher.m | 15 ++-- .../Core/FIRAppCheckTokenRefresherTests.m | 80 +++++++++++++------ 2 files changed, 64 insertions(+), 31 deletions(-) diff --git a/FirebaseAppCheck/Sources/Core/TokenRefresh/FIRAppCheckTokenRefresher.m b/FirebaseAppCheck/Sources/Core/TokenRefresh/FIRAppCheckTokenRefresher.m index 2177da25a2c..71434bec2fb 100644 --- a/FirebaseAppCheck/Sources/Core/TokenRefresh/FIRAppCheckTokenRefresher.m +++ b/FirebaseAppCheck/Sources/Core/TokenRefresh/FIRAppCheckTokenRefresher.m @@ -84,8 +84,7 @@ - (void)setTokenRefreshHandler:(FIRAppCheckTokenRefreshBlock)tokenRefreshHandler _tokenRefreshHandler = tokenRefreshHandler; // Check if handler is being set for the first time and if yes then schedule first refresh. - if (tokenRefreshHandler && self.initialRefreshResult && - self.settings.isTokenAutoRefreshEnabled) { + if (tokenRefreshHandler && self.initialRefreshResult) { FIRAppCheckTokenRefreshResult *initialTokenRefreshResult = self.initialRefreshResult; self.initialRefreshResult = nil; [self scheduleWithTokenRefreshResult:initialTokenRefreshResult]; @@ -137,8 +136,11 @@ - (void)tokenRefreshedWithResult:(FIRAppCheckTokenRefreshResult *)refreshResult } - (void)scheduleWithTokenRefreshResult:(FIRAppCheckTokenRefreshResult *)refreshResult { - NSDate *refreshDate = [self nextRefreshDateWithTokenRefreshResult:refreshResult]; - [self scheduleRefreshAtDate:refreshDate]; + // Schedule the refresh only when allowed. + if (self.settings.isTokenAutoRefreshEnabled) { + NSDate *refreshDate = [self nextRefreshDateWithTokenRefreshResult:refreshResult]; + [self scheduleRefreshAtDate:refreshDate]; + } } - (void)scheduleRefreshAtDate:(NSDate *)refreshDate { @@ -165,8 +167,6 @@ - (void)cancelTimer { [self.timer invalidate]; } -#pragma mark - Backoff - - (NSDate *)nextRefreshDateWithTokenRefreshResult:(FIRAppCheckTokenRefreshResult *)refreshResult { switch (refreshResult.status) { case FIRAppCheckTokenRefreshStatusSuccess: { @@ -181,7 +181,7 @@ - (NSDate *)nextRefreshDateWithTokenRefreshResult:(FIRAppCheckTokenRefreshResult // Don't schedule later than expiration date. NSDate *refreshDate = [targetRefreshDate earlierDate:refreshResult.tokenExpirationDate]; - // Don't schedule an update earlier than in 1 min from now. + // Don't schedule a refresh earlier than in 1 min from now. if ([refreshDate timeIntervalSinceNow] < kMinimumAutoRefreshTimeInterval) { refreshDate = [NSDate dateWithTimeIntervalSinceNow:kMinimumAutoRefreshTimeInterval]; } @@ -203,6 +203,7 @@ - (NSDate *)nextRefreshDateWithTokenRefreshResult:(FIRAppCheckTokenRefreshResult } } +#pragma mark - Backoff + (NSTimeInterval)backoffTimeForRetryCount:(NSInteger)retryCount { if (retryCount == 0) { diff --git a/FirebaseAppCheck/Tests/Unit/Core/FIRAppCheckTokenRefresherTests.m b/FirebaseAppCheck/Tests/Unit/Core/FIRAppCheckTokenRefresherTests.m index e81714932f3..83bb1330931 100644 --- a/FirebaseAppCheck/Tests/Unit/Core/FIRAppCheckTokenRefresherTests.m +++ b/FirebaseAppCheck/Tests/Unit/Core/FIRAppCheckTokenRefresherTests.m @@ -21,6 +21,7 @@ #import "FirebaseAppCheck/Sources/Core/FIRAppCheckSettings.h" #import "FirebaseAppCheck/Sources/Core/TokenRefresh/FIRAppCheckTokenRefresher.h" #import "FirebaseAppCheck/Sources/Core/TokenRefresh/FIRAppCheckTokenRefreshResult.h" +#import "SharedTestUtilities/Date/FIRDateTestUtils.h" #import "FirebaseAppCheck/Tests/Unit/Utils/FIRFakeTimer.h" @interface FIRAppCheckTokenRefresherTests : XCTestCase @@ -40,8 +41,7 @@ - (void)setUp { self.mockSettings = OCMProtocolMock(@protocol(FIRAppCheckSettingsProtocol)); self.fakeTimer = [[FIRFakeTimer alloc] init]; - NSDate *receivedAtDate = [NSDate date]; - self.initialTokenRefreshResult = [[FIRAppCheckTokenRefreshResult alloc] initWithStatusSuccessAndExpirationDate:[receivedAtDate dateByAddingTimeInterval:1000] receivedAtDate:receivedAtDate]; + self.initialTokenRefreshResult = [[FIRAppCheckTokenRefreshResult alloc] initWithStatusNever]; self.tokenExpirationThreshold = 1 * 60; } @@ -55,47 +55,77 @@ - (void)tearDown { #pragma mark - Auto refresh is allowed - (void)testInitialRefreshWhenAutoRefreshAllowed { + __auto_type weakSelf = self; + + self.initialTokenRefreshResult = [[FIRAppCheckTokenRefreshResult alloc] initWithStatusNever]; FIRAppCheckTokenRefresher *refresher = [self createRefresher]; - // 1. Expect checking if auto-refresh allowed before scheduling initial refresh. + // 1. Expect checking if auto-refresh allowed before scheduling the initial refresh. [[[self.mockSettings expect] andReturnValue:@(YES)] isTokenAutoRefreshEnabled]; - // 2. Expect timer to be scheduled. - NSDate *expectedTimerFireDate = [self expectedRefreshDateWithReceivedDate:self.initialTokenRefreshResult.tokenReceivedAtDate expirationDate:self.initialTokenRefreshResult.tokenExpirationDate]; - XCTestExpectation *timerCreateExpectation = [self expectationWithDescription:@"create timer"]; - - __auto_type weakSelf = self; + // 2. Don't expect the timer to be scheduled for the first refresh as the refresh should be triggered straight away. + XCTestExpectation *initialTimerCreatedExpectation = [self expectationWithDescription:@"initial refresh timer created"]; + initialTimerCreatedExpectation.inverted = YES; self.fakeTimer.createHandler = ^(NSDate *_Nonnull fireDate) { weakSelf.fakeTimer.createHandler = nil; - XCTAssertEqualObjects(fireDate, expectedTimerFireDate); - [timerCreateExpectation fulfill]; + [initialTimerCreatedExpectation fulfill]; }; - // 3. Expect refresh handler to be called. - __auto_type refreshResult = [[FIRAppCheckTokenRefreshResult alloc] initWithStatusSuccessAndExpirationDate:[expectedTimerFireDate dateByAddingTimeInterval:60 * 60] receivedAtDate:expectedTimerFireDate]; + // 3. Expect checking if auto-refresh allowed before triggering the initial refresh. + [[[self.mockSettings expect] andReturnValue:@(YES)] isTokenAutoRefreshEnabled]; + + // 4. Expect initial refresh handler to be called. + __block FIRAppCheckTokenRefreshCompletion initialRefreshCompletion; XCTestExpectation *initialRefreshExpectation = [self expectationWithDescription:@"initial refresh"]; - XCTestExpectation *noEarlyRefreshExpectation = - [self expectationWithDescription:@"no early refresh"]; - noEarlyRefreshExpectation.inverted = YES; refresher.tokenRefreshHandler = ^(FIRAppCheckTokenRefreshCompletion _Nonnull completion) { + // Save completion to be called later. + initialRefreshCompletion = completion; + [initialRefreshExpectation fulfill]; - [noEarlyRefreshExpectation fulfill]; + }; - // Call completion. - completion(refreshResult); + NSDate *initialTokenExpirationDate = [NSDate dateWithTimeIntervalSinceNow:60 * 60]; + NSDate *initialTokenReceivedDate = [NSDate date]; + __auto_type initialRefreshResult = [[FIRAppCheckTokenRefreshResult alloc] initWithStatusSuccessAndExpirationDate:initialTokenExpirationDate receivedAtDate:initialTokenReceivedDate]; + + [self waitForExpectations:@[ initialTimerCreatedExpectation, initialRefreshExpectation ] timeout:1]; + + // 5. Expect checking if auto-refresh allowed before scheduling next refresh. + [[[self.mockSettings expect] andReturnValue:@(YES)] isTokenAutoRefreshEnabled]; + + // 6. Expect a next refresh timer to be scheduled on initial refresh completion. + NSDate *expectedRefreshDate = [self expectedRefreshDateWithReceivedDate:initialTokenReceivedDate expirationDate:initialTokenExpirationDate]; + XCTestExpectation *nextTimerCreateExpectation = [self expectationWithDescription:@"next refresh create timer"]; + self.fakeTimer.createHandler = ^(NSDate *_Nonnull fireDate) { + weakSelf.fakeTimer.createHandler = nil; + XCTAssertEqualObjects(fireDate, expectedRefreshDate); + [nextTimerCreateExpectation fulfill]; }; - // 4. Check if the handler is not fired before the timer. - [self waitForExpectations:@[ timerCreateExpectation, noEarlyRefreshExpectation ] timeout:1]; + // 7. Call initial refresh completion and wait for next refresh timer to be scheduled. + initialRefreshCompletion(initialRefreshResult); + [self waitForExpectations:@[nextTimerCreateExpectation] timeout:0.5]; - // 5. Expect checking if auto-refresh allowed before refreshing. + // 8. Expect checking if auto-refresh allowed before triggering the next refresh. [[[self.mockSettings expect] andReturnValue:@(YES)] isTokenAutoRefreshEnabled]; - // 6. Fire the timer and wait for completion. + // 9. Expect refresh handler to be called for the next refresh. + __auto_type nextRefreshResult = [[FIRAppCheckTokenRefreshResult alloc] initWithStatusSuccessAndExpirationDate:[expectedRefreshDate dateByAddingTimeInterval:60*60] receivedAtDate:expectedRefreshDate]; + XCTestExpectation *nextRefreshExpectation = + [self expectationWithDescription:@"next refresh"]; + refresher.tokenRefreshHandler = ^(FIRAppCheckTokenRefreshCompletion _Nonnull completion) { + [nextRefreshExpectation fulfill]; + + // Call completion. + completion(nextRefreshResult); + }; + + // 10. Fire the timer. [self fireTimer]; - [self waitForExpectations:@[ initialRefreshExpectation ] timeout:0.5]; + // 11. Wait for the next refresh handler to be called. + [self waitForExpectations:@[ nextRefreshExpectation ] timeout:1]; OCMVerifyAll(self.mockSettings); } @@ -233,6 +263,8 @@ - (void)testBackoff { OCMVerifyAll(self.mockSettings); } +//- (void)test + #pragma mark - Auto refresh is not allowed - (void)testNoInitialRefreshWhenAutoRefreshIsNotAllowed { @@ -280,7 +312,7 @@ - (void)testNoRefreshWhenAutoRefreshWasDisabledAfterInit { // 2. Expect timer to be scheduled. NSDate *expectedTimerFireDate = - [self.initialTokenRefreshResult.tokenExpirationDate dateByAddingTimeInterval:-self.tokenExpirationThreshold]; + [self expectedRefreshDateWithReceivedDate:self.initialTokenRefreshResult.tokenReceivedAtDate expirationDate:self.initialTokenRefreshResult.tokenExpirationDate]; XCTestExpectation *timerCreateExpectation = [self expectationWithDescription:@"create timer"]; __auto_type weakSelf = self; From 8894e82a38ba68736e70fed48dc2b2fd9ca31b9a Mon Sep 17 00:00:00 2001 From: Maksym Malyhin Date: Wed, 9 Jun 2021 11:44:47 -0400 Subject: [PATCH 25/37] Add #import where needed. --- .../AppAttestProvider/DCAppAttestService+FIRAppAttestService.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/FirebaseAppCheck/Sources/AppAttestProvider/DCAppAttestService+FIRAppAttestService.h b/FirebaseAppCheck/Sources/AppAttestProvider/DCAppAttestService+FIRAppAttestService.h index 98fdbdc31b7..4bc269ff163 100644 --- a/FirebaseAppCheck/Sources/AppAttestProvider/DCAppAttestService+FIRAppAttestService.h +++ b/FirebaseAppCheck/Sources/AppAttestProvider/DCAppAttestService+FIRAppAttestService.h @@ -14,6 +14,8 @@ * limitations under the License. */ +#import + // Currently DCAppAttestService is available on iOS only. #if TARGET_OS_IOS && !TARGET_OS_MACCATALYST // Catalyst should be possible with Xcode 12.5+ From 4391ce9fe2995e87629d30938c71690166e5a791 Mon Sep 17 00:00:00 2001 From: Maksym Malyhin Date: Wed, 9 Jun 2021 12:14:11 -0400 Subject: [PATCH 26/37] Formatting --- FirebaseAppCheck/Sources/Core/FIRAppCheck.m | 19 ++-- .../FIRAppCheckTokenRefreshResult.h | 11 ++- .../TokenRefresh/FIRAppCheckTokenRefresher.h | 10 +- .../TokenRefresh/FIRAppCheckTokenRefresher.m | 36 +++---- .../Unit/Core/FIRAppCheckIntegrationTests.m | 4 +- .../Tests/Unit/Core/FIRAppCheckTests.m | 19 ++-- .../Core/FIRAppCheckTokenRefresherTests.m | 94 ++++++++++++------- 7 files changed, 115 insertions(+), 78 deletions(-) diff --git a/FirebaseAppCheck/Sources/Core/FIRAppCheck.m b/FirebaseAppCheck/Sources/Core/FIRAppCheck.m index 28cd703d722..0a5f8d3a97e 100644 --- a/FirebaseAppCheck/Sources/Core/FIRAppCheck.m +++ b/FirebaseAppCheck/Sources/Core/FIRAppCheck.m @@ -22,17 +22,17 @@ #import "FBLPromises.h" #endif +#import "FirebaseAppCheck/Sources/Core/FIRAppCheckToken+Internal.h" #import "FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRAppCheckProvider.h" #import "FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRAppCheckProviderFactory.h" -#import "FirebaseAppCheck/Sources/Core/FIRAppCheckToken+Internal.h" #import "FirebaseAppCheck/Sources/Core/Errors/FIRAppCheckErrorUtil.h" #import "FirebaseAppCheck/Sources/Core/FIRAppCheckLogger.h" #import "FirebaseAppCheck/Sources/Core/FIRAppCheckSettings.h" #import "FirebaseAppCheck/Sources/Core/FIRAppCheckTokenResult.h" #import "FirebaseAppCheck/Sources/Core/Storage/FIRAppCheckStorage.h" -#import "FirebaseAppCheck/Sources/Core/TokenRefresh/FIRAppCheckTokenRefresher.h" #import "FirebaseAppCheck/Sources/Core/TokenRefresh/FIRAppCheckTokenRefreshResult.h" +#import "FirebaseAppCheck/Sources/Core/TokenRefresh/FIRAppCheckTokenRefresher.h" #import "FirebaseAppCheck/Sources/Interop/FIRAppCheckInterop.h" #import "FirebaseAppCheck/Sources/Interop/FIRAppCheckTokenResultInterop.h" @@ -125,11 +125,12 @@ - (nullable instancetype)initWithApp:(FIRApp *)app { [[FIRAppCheckSettings alloc] initWithApp:app userDefault:[NSUserDefaults standardUserDefaults] mainBundle:[NSBundle mainBundle]]; - FIRAppCheckTokenRefreshResult *refreshResult = [[FIRAppCheckTokenRefreshResult alloc] initWithStatusNever]; + FIRAppCheckTokenRefreshResult *refreshResult = + [[FIRAppCheckTokenRefreshResult alloc] initWithStatusNever]; FIRAppCheckTokenRefresher *tokenRefresher = [[FIRAppCheckTokenRefresher alloc] initWithRefreshResult:refreshResult - tokenExpirationThreshold:kTokenExpirationThreshold - settings:settings]; + tokenExpirationThreshold:kTokenExpirationThreshold + settings:settings]; FIRAppCheckStorage *storage = [[FIRAppCheckStorage alloc] initWithAppName:app.name appID:app.options.googleAppID @@ -308,7 +309,9 @@ - (nonnull NSString *)notificationTokenKey { // 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 = [[FIRAppCheckTokenRefreshResult alloc] initWithStatusSuccessAndExpirationDate:token.expirationDate receivedAtDate:token.receivedAtDate]; + __auto_type refreshResult = [[FIRAppCheckTokenRefreshResult alloc] + initWithStatusSuccessAndExpirationDate:token.expirationDate + receivedAtDate:token.receivedAtDate]; [self.tokenRefresher updateWithRefreshResult:refreshResult]; [self postTokenUpdateNotificationWithToken:token]; return token; @@ -320,7 +323,9 @@ - (nonnull NSString *)notificationTokenKey { - (void)periodicTokenRefreshWithCompletion:(FIRAppCheckTokenRefreshCompletion)completion { [self retrieveOrRefreshTokenForcingRefresh:NO] .then(^id _Nullable(FIRAppCheckToken *_Nullable token) { - __auto_type refreshResult = [[FIRAppCheckTokenRefreshResult alloc] initWithStatusSuccessAndExpirationDate:token.expirationDate receivedAtDate:token.receivedAtDate]; + __auto_type refreshResult = [[FIRAppCheckTokenRefreshResult alloc] + initWithStatusSuccessAndExpirationDate:token.expirationDate + receivedAtDate:token.receivedAtDate]; completion(refreshResult); return nil; }) diff --git a/FirebaseAppCheck/Sources/Core/TokenRefresh/FIRAppCheckTokenRefreshResult.h b/FirebaseAppCheck/Sources/Core/TokenRefresh/FIRAppCheckTokenRefreshResult.h index fcda15a40b0..612a8d3efd1 100644 --- a/FirebaseAppCheck/Sources/Core/TokenRefresh/FIRAppCheckTokenRefreshResult.h +++ b/FirebaseAppCheck/Sources/Core/TokenRefresh/FIRAppCheckTokenRefreshResult.h @@ -18,7 +18,8 @@ NS_ASSUME_NONNULL_BEGIN -/// Represents possible results of a Firebase App Check token refresh attempt that matter for `FIRAppCheckTokenRefresher`. +/// Represents possible results of a Firebase App Check token refresh attempt that matter for +/// `FIRAppCheckTokenRefresher`. typedef NS_ENUM(NSInteger, FIRAppCheckTokenRefreshStatus) { // The token has not been refreshed. FIRAppCheckTokenRefreshStatusNever, @@ -26,13 +27,14 @@ typedef NS_ENUM(NSInteger, FIRAppCheckTokenRefreshStatus) { // The token was successfully refreshed. FIRAppCheckTokenRefreshStatusSuccess, -// FIRAppCheckTokenRefreshStatusSuccess, + // FIRAppCheckTokenRefreshStatusSuccess, // The token refresh failed. FIRAppCheckTokenRefreshStatusFailure }; -/// An object to pass the possible results of a Firebase App Check token refresh attempt and supplementary data. +/// An object to pass the possible results of a Firebase App Check token refresh attempt and +/// supplementary data. @interface FIRAppCheckTokenRefreshResult : NSObject /// Status of the refresh. @@ -55,7 +57,8 @@ typedef NS_ENUM(NSInteger, FIRAppCheckTokenRefreshStatus) { /// Initializes the instance with `FIRAppCheckTokenRefreshStatusFailure`. /// @param tokenExpirationDate See `tokenExpirationDate` property. /// @param tokenReceivedAtDate See `tokenReceivedAtDate` property. -- (instancetype)initWithStatusSuccessAndExpirationDate:(NSDate *)tokenExpirationDate receivedAtDate:(NSDate *)tokenReceivedAtDate; +- (instancetype)initWithStatusSuccessAndExpirationDate:(NSDate *)tokenExpirationDate + receivedAtDate:(NSDate *)tokenReceivedAtDate; @end diff --git a/FirebaseAppCheck/Sources/Core/TokenRefresh/FIRAppCheckTokenRefresher.h b/FirebaseAppCheck/Sources/Core/TokenRefresh/FIRAppCheckTokenRefresher.h index c8b32555deb..005e3bc0411 100644 --- a/FirebaseAppCheck/Sources/Core/TokenRefresh/FIRAppCheckTokenRefresher.h +++ b/FirebaseAppCheck/Sources/Core/TokenRefresh/FIRAppCheckTokenRefresher.h @@ -58,15 +58,15 @@ typedef void (^FIRAppCheckTokenRefreshBlock)(FIRAppCheckTokenRefreshCompletion c /// seconds before the actual token expiration time. /// @param settings An object that handles Firebase app check settings. - (instancetype)initWithRefreshResult:(FIRAppCheckTokenRefreshResult *)refreshResult - tokenExpirationThreshold:(NSTimeInterval)tokenExpirationThreshold - timerProvider:(FIRTimerProvider)timerProvider - settings:(id)settings + tokenExpirationThreshold:(NSTimeInterval)tokenExpirationThreshold + timerProvider:(FIRTimerProvider)timerProvider + settings:(id)settings NS_DESIGNATED_INITIALIZER; /// A convenience initializer with a timer provider returning an instance of `FIRAppCheckTimer`. - (instancetype)initWithRefreshResult:(FIRAppCheckTokenRefreshResult *)refreshResult - tokenExpirationThreshold:(NSTimeInterval)tokenExpirationThreshold - settings:(id)settings; + tokenExpirationThreshold:(NSTimeInterval)tokenExpirationThreshold + settings:(id)settings; @end diff --git a/FirebaseAppCheck/Sources/Core/TokenRefresh/FIRAppCheckTokenRefresher.m b/FirebaseAppCheck/Sources/Core/TokenRefresh/FIRAppCheckTokenRefresher.m index 71434bec2fb..8ecbcbc65cb 100644 --- a/FirebaseAppCheck/Sources/Core/TokenRefresh/FIRAppCheckTokenRefresher.m +++ b/FirebaseAppCheck/Sources/Core/TokenRefresh/FIRAppCheckTokenRefresher.m @@ -25,9 +25,10 @@ static const NSTimeInterval kInitialBackoffTimeInterval = 30; static const NSTimeInterval kMaximumBackoffTimeInterval = 16 * 60; -static const NSTimeInterval kMinimumAutoRefreshTimeInterval = 60; // 1 min. +static const NSTimeInterval kMinimumAutoRefreshTimeInterval = 60; // 1 min. -/// How much time in advance to auto-refresh token before it's expiration. E.g. 0.5 means that the token will be refreshed half way through it's intended time to live. +/// How much time in advance to auto-refresh token before it's expiration. E.g. 0.5 means that the +/// token will be refreshed half way through it's intended time to live. static const double kAutoRefreshFraction = 0.5; @interface FIRAppCheckTokenRefresher () @@ -40,7 +41,7 @@ @interface FIRAppCheckTokenRefresher () @property(atomic, nullable) id timer; @property(atomic) NSUInteger retryCount; -/// Initial refresh result to be used when `tokenRefreshHandler` has been sent. +/// Initial refresh result to be used when `tokenRefreshHandler` has been sent. @property(nonatomic, nullable) FIRAppCheckTokenRefreshResult *initialRefreshResult; @property(nonatomic, readonly) NSTimeInterval tokenExpirationThreshold; @@ -51,9 +52,9 @@ @implementation FIRAppCheckTokenRefresher @synthesize tokenRefreshHandler = _tokenRefreshHandler; - (instancetype)initWithRefreshResult:(FIRAppCheckTokenRefreshResult *)refreshResult - tokenExpirationThreshold:(NSTimeInterval)tokenExpirationThreshold - timerProvider:(FIRTimerProvider)timerProvider - settings:(id)settings { + tokenExpirationThreshold:(NSTimeInterval)tokenExpirationThreshold + timerProvider:(FIRTimerProvider)timerProvider + settings:(id)settings { self = [super init]; if (self) { _refreshQueue = @@ -67,12 +68,12 @@ - (instancetype)initWithRefreshResult:(FIRAppCheckTokenRefreshResult *)refreshRe } - (instancetype)initWithRefreshResult:(FIRAppCheckTokenRefreshResult *)refreshResult - tokenExpirationThreshold:(NSTimeInterval)tokenExpirationThreshold - settings:(id)settings { + tokenExpirationThreshold:(NSTimeInterval)tokenExpirationThreshold + settings:(id)settings { return [self initWithRefreshResult:refreshResult - tokenExpirationThreshold:tokenExpirationThreshold - timerProvider:[FIRAppCheckTimer timerProvider] - settings:settings]; + tokenExpirationThreshold:tokenExpirationThreshold + timerProvider:[FIRAppCheckTimer timerProvider] + settings:settings]; } - (void)dealloc { @@ -170,13 +171,14 @@ - (void)cancelTimer { - (NSDate *)nextRefreshDateWithTokenRefreshResult:(FIRAppCheckTokenRefreshResult *)refreshResult { switch (refreshResult.status) { case FIRAppCheckTokenRefreshStatusSuccess: { - NSTimeInterval timeToLive = [refreshResult.tokenExpirationDate timeIntervalSinceDate:refreshResult.tokenReceivedAtDate]; + NSTimeInterval timeToLive = [refreshResult.tokenExpirationDate + timeIntervalSinceDate:refreshResult.tokenReceivedAtDate]; timeToLive = MAX(timeToLive, 0); // Refresh in 50% of TTL + 5 min. NSTimeInterval targetRefreshSinceReceivedDate = timeToLive * kAutoRefreshFraction + 5 * 60; - NSDate *targetRefreshDate = - [refreshResult.tokenReceivedAtDate dateByAddingTimeInterval:targetRefreshSinceReceivedDate]; + NSDate *targetRefreshDate = [refreshResult.tokenReceivedAtDate + dateByAddingTimeInterval:targetRefreshSinceReceivedDate]; // Don't schedule later than expiration date. NSDate *refreshDate = [targetRefreshDate earlierDate:refreshResult.tokenExpirationDate]; @@ -186,15 +188,13 @@ - (NSDate *)nextRefreshDateWithTokenRefreshResult:(FIRAppCheckTokenRefreshResult refreshDate = [NSDate dateWithTimeIntervalSinceNow:kMinimumAutoRefreshTimeInterval]; } return refreshDate; - } - break; + } break; case FIRAppCheckTokenRefreshStatusFailure: { // Refresh after a timeout. NSTimeInterval backoffTime = [[self class] backoffTimeForRetryCount:self.retryCount]; return [NSDate dateWithTimeIntervalSinceNow:backoffTime]; - } - break; + } break; case FIRAppCheckTokenRefreshStatusNever: // Refresh ASAP. diff --git a/FirebaseAppCheck/Tests/Unit/Core/FIRAppCheckIntegrationTests.m b/FirebaseAppCheck/Tests/Unit/Core/FIRAppCheckIntegrationTests.m index 7118345cb64..bd560a17d84 100644 --- a/FirebaseAppCheck/Tests/Unit/Core/FIRAppCheckIntegrationTests.m +++ b/FirebaseAppCheck/Tests/Unit/Core/FIRAppCheckIntegrationTests.m @@ -205,8 +205,8 @@ - (void)disableTokenRefresher { self.mockTokenRefresher = OCMClassMock([FIRAppCheckTokenRefresher class]); OCMStub([self.mockTokenRefresher alloc]).andReturn(self.mockTokenRefresher); OCMStub([self.mockTokenRefresher initWithRefreshResult:[OCMArg any] - tokenExpirationThreshold:5 * 60 - settings:[OCMArg any]]) + tokenExpirationThreshold:5 * 60 + settings:[OCMArg any]]) .andReturn(self.mockTokenRefresher); } diff --git a/FirebaseAppCheck/Tests/Unit/Core/FIRAppCheckTests.m b/FirebaseAppCheck/Tests/Unit/Core/FIRAppCheckTests.m index 11302c915ac..96b0a970201 100644 --- a/FirebaseAppCheck/Tests/Unit/Core/FIRAppCheckTests.m +++ b/FirebaseAppCheck/Tests/Unit/Core/FIRAppCheckTests.m @@ -31,8 +31,8 @@ #import "FirebaseAppCheck/Sources/Core/FIRAppCheckSettings.h" #import "FirebaseAppCheck/Sources/Core/FIRAppCheckTokenResult.h" #import "FirebaseAppCheck/Sources/Core/Storage/FIRAppCheckStorage.h" -#import "FirebaseAppCheck/Sources/Core/TokenRefresh/FIRAppCheckTokenRefresher.h" #import "FirebaseAppCheck/Sources/Core/TokenRefresh/FIRAppCheckTokenRefreshResult.h" +#import "FirebaseAppCheck/Sources/Core/TokenRefresh/FIRAppCheckTokenRefresher.h" #import "FirebaseCore/Sources/Private/FirebaseCoreInternal.h" @@ -115,12 +115,13 @@ - (void)testInitWithApp { id mockTokenRefresher = OCMClassMock([FIRAppCheckTokenRefresher class]); OCMExpect([mockTokenRefresher alloc]).andReturn(mockTokenRefresher); - id refresherDateValidator = [OCMArg checkWithBlock:^BOOL(FIRAppCheckTokenRefreshResult *refreshResult) { - XCTAssertEqual(refreshResult.status, FIRAppCheckTokenRefreshStatusNever); - XCTAssertEqual(refreshResult.tokenExpirationDate, nil); - XCTAssertEqual(refreshResult.tokenReceivedAtDate, nil); - return YES; - }]; + id refresherDateValidator = + [OCMArg checkWithBlock:^BOOL(FIRAppCheckTokenRefreshResult *refreshResult) { + XCTAssertEqual(refreshResult.status, FIRAppCheckTokenRefreshStatusNever); + XCTAssertEqual(refreshResult.tokenExpirationDate, nil); + XCTAssertEqual(refreshResult.tokenReceivedAtDate, nil); + return YES; + }]; id settingsValidator = [OCMArg checkWithBlock:^BOOL(id obj) { XCTAssert([obj isKindOfClass:[FIRAppCheckSettings class]]); @@ -128,8 +129,8 @@ - (void)testInitWithApp { }]; OCMExpect([mockTokenRefresher initWithRefreshResult:refresherDateValidator - tokenExpirationThreshold:5 * 60 - settings:settingsValidator]) + tokenExpirationThreshold:5 * 60 + settings:settingsValidator]) .andReturn(mockTokenRefresher); OCMExpect([mockTokenRefresher setTokenRefreshHandler:[OCMArg any]]); diff --git a/FirebaseAppCheck/Tests/Unit/Core/FIRAppCheckTokenRefresherTests.m b/FirebaseAppCheck/Tests/Unit/Core/FIRAppCheckTokenRefresherTests.m index 83bb1330931..0126554dc68 100644 --- a/FirebaseAppCheck/Tests/Unit/Core/FIRAppCheckTokenRefresherTests.m +++ b/FirebaseAppCheck/Tests/Unit/Core/FIRAppCheckTokenRefresherTests.m @@ -19,10 +19,10 @@ #import #import "FirebaseAppCheck/Sources/Core/FIRAppCheckSettings.h" -#import "FirebaseAppCheck/Sources/Core/TokenRefresh/FIRAppCheckTokenRefresher.h" #import "FirebaseAppCheck/Sources/Core/TokenRefresh/FIRAppCheckTokenRefreshResult.h" -#import "SharedTestUtilities/Date/FIRDateTestUtils.h" +#import "FirebaseAppCheck/Sources/Core/TokenRefresh/FIRAppCheckTokenRefresher.h" #import "FirebaseAppCheck/Tests/Unit/Utils/FIRFakeTimer.h" +#import "SharedTestUtilities/Date/FIRDateTestUtils.h" @interface FIRAppCheckTokenRefresherTests : XCTestCase @@ -63,8 +63,10 @@ - (void)testInitialRefreshWhenAutoRefreshAllowed { // 1. Expect checking if auto-refresh allowed before scheduling the initial refresh. [[[self.mockSettings expect] andReturnValue:@(YES)] isTokenAutoRefreshEnabled]; - // 2. Don't expect the timer to be scheduled for the first refresh as the refresh should be triggered straight away. - XCTestExpectation *initialTimerCreatedExpectation = [self expectationWithDescription:@"initial refresh timer created"]; + // 2. Don't expect the timer to be scheduled for the first refresh as the refresh should be + // triggered straight away. + XCTestExpectation *initialTimerCreatedExpectation = + [self expectationWithDescription:@"initial refresh timer created"]; initialTimerCreatedExpectation.inverted = YES; self.fakeTimer.createHandler = ^(NSDate *_Nonnull fireDate) { weakSelf.fakeTimer.createHandler = nil; @@ -87,16 +89,22 @@ - (void)testInitialRefreshWhenAutoRefreshAllowed { NSDate *initialTokenExpirationDate = [NSDate dateWithTimeIntervalSinceNow:60 * 60]; NSDate *initialTokenReceivedDate = [NSDate date]; - __auto_type initialRefreshResult = [[FIRAppCheckTokenRefreshResult alloc] initWithStatusSuccessAndExpirationDate:initialTokenExpirationDate receivedAtDate:initialTokenReceivedDate]; + __auto_type initialRefreshResult = [[FIRAppCheckTokenRefreshResult alloc] + initWithStatusSuccessAndExpirationDate:initialTokenExpirationDate + receivedAtDate:initialTokenReceivedDate]; - [self waitForExpectations:@[ initialTimerCreatedExpectation, initialRefreshExpectation ] timeout:1]; + [self waitForExpectations:@[ initialTimerCreatedExpectation, initialRefreshExpectation ] + timeout:1]; // 5. Expect checking if auto-refresh allowed before scheduling next refresh. [[[self.mockSettings expect] andReturnValue:@(YES)] isTokenAutoRefreshEnabled]; // 6. Expect a next refresh timer to be scheduled on initial refresh completion. - NSDate *expectedRefreshDate = [self expectedRefreshDateWithReceivedDate:initialTokenReceivedDate expirationDate:initialTokenExpirationDate]; - XCTestExpectation *nextTimerCreateExpectation = [self expectationWithDescription:@"next refresh create timer"]; + NSDate *expectedRefreshDate = + [self expectedRefreshDateWithReceivedDate:initialTokenReceivedDate + expirationDate:initialTokenExpirationDate]; + XCTestExpectation *nextTimerCreateExpectation = + [self expectationWithDescription:@"next refresh create timer"]; self.fakeTimer.createHandler = ^(NSDate *_Nonnull fireDate) { weakSelf.fakeTimer.createHandler = nil; XCTAssertEqualObjects(fireDate, expectedRefreshDate); @@ -105,15 +113,16 @@ - (void)testInitialRefreshWhenAutoRefreshAllowed { // 7. Call initial refresh completion and wait for next refresh timer to be scheduled. initialRefreshCompletion(initialRefreshResult); - [self waitForExpectations:@[nextTimerCreateExpectation] timeout:0.5]; + [self waitForExpectations:@[ nextTimerCreateExpectation ] timeout:0.5]; // 8. Expect checking if auto-refresh allowed before triggering the next refresh. [[[self.mockSettings expect] andReturnValue:@(YES)] isTokenAutoRefreshEnabled]; // 9. Expect refresh handler to be called for the next refresh. - __auto_type nextRefreshResult = [[FIRAppCheckTokenRefreshResult alloc] initWithStatusSuccessAndExpirationDate:[expectedRefreshDate dateByAddingTimeInterval:60*60] receivedAtDate:expectedRefreshDate]; - XCTestExpectation *nextRefreshExpectation = - [self expectationWithDescription:@"next refresh"]; + __auto_type nextRefreshResult = [[FIRAppCheckTokenRefreshResult alloc] + initWithStatusSuccessAndExpirationDate:[expectedRefreshDate dateByAddingTimeInterval:60 * 60] + receivedAtDate:expectedRefreshDate]; + XCTestExpectation *nextRefreshExpectation = [self expectationWithDescription:@"next refresh"]; refresher.tokenRefreshHandler = ^(FIRAppCheckTokenRefreshCompletion _Nonnull completion) { [nextRefreshExpectation fulfill]; @@ -169,7 +178,9 @@ - (void)testNextRefreshOnRefreshSuccess { NSDate *refreshedTokenExpirationDate = [self.initialTokenRefreshResult.tokenExpirationDate dateByAddingTimeInterval:60 * 60]; - __auto_type refreshResult = [[FIRAppCheckTokenRefreshResult alloc] initWithStatusSuccessAndExpirationDate:refreshedTokenExpirationDate receivedAtDate:self.initialTokenRefreshResult.tokenExpirationDate]; + __auto_type refreshResult = [[FIRAppCheckTokenRefreshResult alloc] + initWithStatusSuccessAndExpirationDate:refreshedTokenExpirationDate + receivedAtDate:self.initialTokenRefreshResult.tokenExpirationDate]; // 1. Expect checking if auto-refresh allowed before scheduling initial refresh. [[[self.mockSettings expect] andReturnValue:@(YES)] isTokenAutoRefreshEnabled]; @@ -189,7 +200,8 @@ - (void)testNextRefreshOnRefreshSuccess { // 3. Expect for new timer to be created. NSDate *expectedFireDate = - [self expectedRefreshDateWithReceivedDate:refreshResult.tokenReceivedAtDate expirationDate:refreshResult.tokenExpirationDate]; + [self expectedRefreshDateWithReceivedDate:refreshResult.tokenReceivedAtDate + expirationDate:refreshResult.tokenExpirationDate]; XCTestExpectation *createTimerExpectation = [self expectationWithDescription:@"create timer"]; self.fakeTimer.createHandler = ^(NSDate *_Nonnull fireDate) { [createTimerExpectation fulfill]; @@ -229,7 +241,8 @@ - (void)testBackoff { // Call completion in a while. dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ - __auto_type refreshFailure = [[FIRAppCheckTokenRefreshResult alloc] initWithStatusFailure]; + __auto_type refreshFailure = + [[FIRAppCheckTokenRefreshResult alloc] initWithStatusFailure]; completion(refreshFailure); }); }; @@ -274,8 +287,8 @@ - (void)testNoInitialRefreshWhenAutoRefreshIsNotAllowed { [[[self.mockSettings expect] andReturnValue:@(NO)] isTokenAutoRefreshEnabled]; // 2. Don't expect timer to be scheduled. - NSDate *expectedTimerFireDate = - [self.initialTokenRefreshResult.tokenExpirationDate dateByAddingTimeInterval:-self.tokenExpirationThreshold]; + NSDate *expectedTimerFireDate = [self.initialTokenRefreshResult.tokenExpirationDate + dateByAddingTimeInterval:-self.tokenExpirationThreshold]; XCTestExpectation *timerCreateExpectation = [self expectationWithDescription:@"create timer"]; timerCreateExpectation.inverted = YES; @@ -287,7 +300,10 @@ - (void)testNoInitialRefreshWhenAutoRefreshIsNotAllowed { }; // 3. Don't expect refresh handler to be called. - __auto_type refreshResult = [[FIRAppCheckTokenRefreshResult alloc] initWithStatusSuccessAndExpirationDate:[expectedTimerFireDate dateByAddingTimeInterval:60 * 60] receivedAtDate:expectedTimerFireDate]; + __auto_type refreshResult = [[FIRAppCheckTokenRefreshResult alloc] + initWithStatusSuccessAndExpirationDate:[expectedTimerFireDate + dateByAddingTimeInterval:60 * 60] + receivedAtDate:expectedTimerFireDate]; XCTestExpectation *refreshExpectation = [self expectationWithDescription:@"refresh"]; refreshExpectation.inverted = YES; @@ -312,7 +328,8 @@ - (void)testNoRefreshWhenAutoRefreshWasDisabledAfterInit { // 2. Expect timer to be scheduled. NSDate *expectedTimerFireDate = - [self expectedRefreshDateWithReceivedDate:self.initialTokenRefreshResult.tokenReceivedAtDate expirationDate:self.initialTokenRefreshResult.tokenExpirationDate]; + [self expectedRefreshDateWithReceivedDate:self.initialTokenRefreshResult.tokenReceivedAtDate + expirationDate:self.initialTokenRefreshResult.tokenExpirationDate]; XCTestExpectation *timerCreateExpectation = [self expectationWithDescription:@"create timer"]; __auto_type weakSelf = self; @@ -323,7 +340,10 @@ - (void)testNoRefreshWhenAutoRefreshWasDisabledAfterInit { }; // 3. Expect refresh handler to be called. - __auto_type refreshResult = [[FIRAppCheckTokenRefreshResult alloc] initWithStatusSuccessAndExpirationDate:[expectedTimerFireDate dateByAddingTimeInterval:60 * 60] receivedAtDate:expectedTimerFireDate]; + __auto_type refreshResult = [[FIRAppCheckTokenRefreshResult alloc] + initWithStatusSuccessAndExpirationDate:[expectedTimerFireDate + dateByAddingTimeInterval:60 * 60] + receivedAtDate:expectedTimerFireDate]; XCTestExpectation *noRefreshExpectation = [self expectationWithDescription:@"initial refresh"]; noRefreshExpectation.inverted = YES; refresher.tokenRefreshHandler = ^(FIRAppCheckTokenRefreshCompletion _Nonnull completion) { @@ -352,14 +372,19 @@ - (void)testNoRefreshWhenAutoRefreshWasDisabledAfterInit { - (void)testUpdateTokenExpirationDateWhenAutoRefreshIsAllowed { FIRAppCheckTokenRefresher *refresher = [self createRefresher]; - NSDate *newExpirationDate = [self.initialTokenRefreshResult.tokenExpirationDate dateByAddingTimeInterval:10 * 60]; - __auto_type newRefreshResult = [[FIRAppCheckTokenRefreshResult alloc] initWithStatusSuccessAndExpirationDate:newExpirationDate receivedAtDate:self.initialTokenRefreshResult.tokenExpirationDate]; + NSDate *newExpirationDate = + [self.initialTokenRefreshResult.tokenExpirationDate dateByAddingTimeInterval:10 * 60]; + __auto_type newRefreshResult = [[FIRAppCheckTokenRefreshResult alloc] + initWithStatusSuccessAndExpirationDate:newExpirationDate + receivedAtDate:self.initialTokenRefreshResult.tokenExpirationDate]; // 1. Expect checking if auto-refresh allowed before scheduling initial refresh. [[[self.mockSettings expect] andReturnValue:@(YES)] isTokenAutoRefreshEnabled]; // 2. Expect timer to be scheduled. - NSDate *expectedTimerFireDate = [self expectedRefreshDateWithReceivedDate:newRefreshResult.tokenReceivedAtDate expirationDate:newRefreshResult.tokenExpirationDate]; + NSDate *expectedTimerFireDate = + [self expectedRefreshDateWithReceivedDate:newRefreshResult.tokenReceivedAtDate + expirationDate:newRefreshResult.tokenExpirationDate]; XCTestExpectation *timerCreateExpectation = [self expectationWithDescription:@"create timer"]; __auto_type weakSelf = self; @@ -381,8 +406,11 @@ - (void)testUpdateTokenExpirationDateWhenAutoRefreshIsAllowed { - (void)testUpdateTokenExpirationDateWhenAutoRefreshIsNotAllowed { FIRAppCheckTokenRefresher *refresher = [self createRefresher]; - NSDate *newExpirationDate = [self.initialTokenRefreshResult.tokenExpirationDate dateByAddingTimeInterval:10 * 60]; - __auto_type newRefreshResult = [[FIRAppCheckTokenRefreshResult alloc] initWithStatusSuccessAndExpirationDate:newExpirationDate receivedAtDate:self.initialTokenRefreshResult.tokenExpirationDate]; + NSDate *newExpirationDate = + [self.initialTokenRefreshResult.tokenExpirationDate dateByAddingTimeInterval:10 * 60]; + __auto_type newRefreshResult = [[FIRAppCheckTokenRefreshResult alloc] + initWithStatusSuccessAndExpirationDate:newExpirationDate + receivedAtDate:self.initialTokenRefreshResult.tokenExpirationDate]; // 1. Expect checking if auto-refresh allowed before scheduling initial refresh. [[[self.mockSettings expect] andReturnValue:@(NO)] isTokenAutoRefreshEnabled]; @@ -420,20 +448,20 @@ - (void)fireTimer { } - (FIRAppCheckTokenRefresher *)createRefresher { - return [[FIRAppCheckTokenRefresher alloc] - initWithRefreshResult:self.initialTokenRefreshResult - tokenExpirationThreshold:self.tokenExpirationThreshold - timerProvider:[self.fakeTimer fakeTimerProvider] - settings:self.mockSettings]; + return [[FIRAppCheckTokenRefresher alloc] initWithRefreshResult:self.initialTokenRefreshResult + tokenExpirationThreshold:self.tokenExpirationThreshold + timerProvider:[self.fakeTimer fakeTimerProvider] + settings:self.mockSettings]; } -- (NSDate *)expectedRefreshDateWithReceivedDate:(NSDate *)receivedDate expirationDate:(NSDate *)expirationDate { +- (NSDate *)expectedRefreshDateWithReceivedDate:(NSDate *)receivedDate + expirationDate:(NSDate *)expirationDate { NSTimeInterval timeToLive = [expirationDate timeIntervalSinceDate:receivedDate]; XCTAssertGreaterThanOrEqual(timeToLive, 0); - NSTimeInterval timeToRefresh = timeToLive / 2 + 5 * 60; // 50% or TTL + 5 min + NSTimeInterval timeToRefresh = timeToLive / 2 + 5 * 60; // 50% or TTL + 5 min - NSTimeInterval minimalAutoRefreshInterval = 60; // 1 min + NSTimeInterval minimalAutoRefreshInterval = 60; // 1 min timeToRefresh = MAX(timeToRefresh, minimalAutoRefreshInterval); NSDate *refreshDate = [receivedDate dateByAddingTimeInterval:timeToRefresh]; From 93836e346a9c6786776cac95710b544b13b045b1 Mon Sep 17 00:00:00 2001 From: Maksym Malyhin Date: Thu, 10 Jun 2021 18:19:13 -0400 Subject: [PATCH 27/37] Cleanup and test updates --- FirebaseAppCheck/Sources/Core/FIRAppCheck.m | 1 - .../TokenRefresh/FIRAppCheckTokenRefresher.h | 3 - .../TokenRefresh/FIRAppCheckTokenRefresher.m | 35 +++----- .../Unit/Core/FIRAppCheckIntegrationTests.m | 1 - .../Tests/Unit/Core/FIRAppCheckTests.m | 1 - .../Core/FIRAppCheckTokenRefresherTests.m | 81 ++++++++++++------- 6 files changed, 66 insertions(+), 56 deletions(-) diff --git a/FirebaseAppCheck/Sources/Core/FIRAppCheck.m b/FirebaseAppCheck/Sources/Core/FIRAppCheck.m index 0a5f8d3a97e..b275828a99b 100644 --- a/FirebaseAppCheck/Sources/Core/FIRAppCheck.m +++ b/FirebaseAppCheck/Sources/Core/FIRAppCheck.m @@ -129,7 +129,6 @@ - (nullable instancetype)initWithApp:(FIRApp *)app { [[FIRAppCheckTokenRefreshResult alloc] initWithStatusNever]; FIRAppCheckTokenRefresher *tokenRefresher = [[FIRAppCheckTokenRefresher alloc] initWithRefreshResult:refreshResult - tokenExpirationThreshold:kTokenExpirationThreshold settings:settings]; FIRAppCheckStorage *storage = [[FIRAppCheckStorage alloc] initWithAppName:app.name diff --git a/FirebaseAppCheck/Sources/Core/TokenRefresh/FIRAppCheckTokenRefresher.h b/FirebaseAppCheck/Sources/Core/TokenRefresh/FIRAppCheckTokenRefresher.h index 005e3bc0411..97ce94538be 100644 --- a/FirebaseAppCheck/Sources/Core/TokenRefresh/FIRAppCheckTokenRefresher.h +++ b/FirebaseAppCheck/Sources/Core/TokenRefresh/FIRAppCheckTokenRefresher.h @@ -54,18 +54,15 @@ typedef void (^FIRAppCheckTokenRefreshBlock)(FIRAppCheckTokenRefreshCompletion c /// The designated initializer. /// @param refreshResult A previous token refresh attempt result. -/// @param tokenExpirationThreshold The token refresh will be triggered `tokenExpirationThreshold` /// seconds before the actual token expiration time. /// @param settings An object that handles Firebase app check settings. - (instancetype)initWithRefreshResult:(FIRAppCheckTokenRefreshResult *)refreshResult - tokenExpirationThreshold:(NSTimeInterval)tokenExpirationThreshold timerProvider:(FIRTimerProvider)timerProvider settings:(id)settings NS_DESIGNATED_INITIALIZER; /// A convenience initializer with a timer provider returning an instance of `FIRAppCheckTimer`. - (instancetype)initWithRefreshResult:(FIRAppCheckTokenRefreshResult *)refreshResult - tokenExpirationThreshold:(NSTimeInterval)tokenExpirationThreshold settings:(id)settings; @end diff --git a/FirebaseAppCheck/Sources/Core/TokenRefresh/FIRAppCheckTokenRefresher.m b/FirebaseAppCheck/Sources/Core/TokenRefresh/FIRAppCheckTokenRefresher.m index 8ecbcbc65cb..0b0ab969600 100644 --- a/FirebaseAppCheck/Sources/Core/TokenRefresh/FIRAppCheckTokenRefresher.m +++ b/FirebaseAppCheck/Sources/Core/TokenRefresh/FIRAppCheckTokenRefresher.m @@ -43,7 +43,6 @@ @interface FIRAppCheckTokenRefresher () /// Initial refresh result to be used when `tokenRefreshHandler` has been sent. @property(nonatomic, nullable) FIRAppCheckTokenRefreshResult *initialRefreshResult; -@property(nonatomic, readonly) NSTimeInterval tokenExpirationThreshold; @end @@ -52,14 +51,12 @@ @implementation FIRAppCheckTokenRefresher @synthesize tokenRefreshHandler = _tokenRefreshHandler; - (instancetype)initWithRefreshResult:(FIRAppCheckTokenRefreshResult *)refreshResult - tokenExpirationThreshold:(NSTimeInterval)tokenExpirationThreshold timerProvider:(FIRTimerProvider)timerProvider settings:(id)settings { self = [super init]; if (self) { _refreshQueue = dispatch_queue_create("com.firebase.FIRAppCheckTokenRefresher", DISPATCH_QUEUE_SERIAL); - _tokenExpirationThreshold = tokenExpirationThreshold; _initialRefreshResult = refreshResult; _timerProvider = timerProvider; _settings = settings; @@ -68,10 +65,8 @@ - (instancetype)initWithRefreshResult:(FIRAppCheckTokenRefreshResult *)refreshRe } - (instancetype)initWithRefreshResult:(FIRAppCheckTokenRefreshResult *)refreshResult - tokenExpirationThreshold:(NSTimeInterval)tokenExpirationThreshold settings:(id)settings { return [self initWithRefreshResult:refreshResult - tokenExpirationThreshold:tokenExpirationThreshold timerProvider:[FIRAppCheckTimer timerProvider] settings:settings]; } @@ -100,9 +95,18 @@ - (FIRAppCheckTokenRefreshBlock)tokenRefreshHandler { } - (void)updateWithRefreshResult:(FIRAppCheckTokenRefreshResult *)refreshResult { - if (self.settings.isTokenAutoRefreshEnabled) { - [self scheduleWithTokenRefreshResult:refreshResult]; + switch (refreshResult.status) { + case FIRAppCheckTokenRefreshStatusNever: + case FIRAppCheckTokenRefreshStatusSuccess: + self.retryCount = 0; + break; + + case FIRAppCheckTokenRefreshStatusFailure: + self.retryCount += 1; + break; } + + [self scheduleWithTokenRefreshResult:refreshResult]; } - (void)refresh { @@ -117,25 +121,10 @@ - (void)refresh { __auto_type __weak weakSelf = self; self.tokenRefreshHandler(^(FIRAppCheckTokenRefreshResult *refreshResult) { __auto_type strongSelf = weakSelf; - [strongSelf tokenRefreshedWithResult:refreshResult]; + [strongSelf updateWithRefreshResult:refreshResult]; }); } -- (void)tokenRefreshedWithResult:(FIRAppCheckTokenRefreshResult *)refreshResult { - switch (refreshResult.status) { - case FIRAppCheckTokenRefreshStatusNever: - case FIRAppCheckTokenRefreshStatusSuccess: - self.retryCount = 0; - break; - - case FIRAppCheckTokenRefreshStatusFailure: - self.retryCount += 1; - break; - } - - [self scheduleWithTokenRefreshResult:refreshResult]; -} - - (void)scheduleWithTokenRefreshResult:(FIRAppCheckTokenRefreshResult *)refreshResult { // Schedule the refresh only when allowed. if (self.settings.isTokenAutoRefreshEnabled) { diff --git a/FirebaseAppCheck/Tests/Unit/Core/FIRAppCheckIntegrationTests.m b/FirebaseAppCheck/Tests/Unit/Core/FIRAppCheckIntegrationTests.m index bd560a17d84..e2552903297 100644 --- a/FirebaseAppCheck/Tests/Unit/Core/FIRAppCheckIntegrationTests.m +++ b/FirebaseAppCheck/Tests/Unit/Core/FIRAppCheckIntegrationTests.m @@ -205,7 +205,6 @@ - (void)disableTokenRefresher { self.mockTokenRefresher = OCMClassMock([FIRAppCheckTokenRefresher class]); OCMStub([self.mockTokenRefresher alloc]).andReturn(self.mockTokenRefresher); OCMStub([self.mockTokenRefresher initWithRefreshResult:[OCMArg any] - tokenExpirationThreshold:5 * 60 settings:[OCMArg any]]) .andReturn(self.mockTokenRefresher); } diff --git a/FirebaseAppCheck/Tests/Unit/Core/FIRAppCheckTests.m b/FirebaseAppCheck/Tests/Unit/Core/FIRAppCheckTests.m index 96b0a970201..0977eda286b 100644 --- a/FirebaseAppCheck/Tests/Unit/Core/FIRAppCheckTests.m +++ b/FirebaseAppCheck/Tests/Unit/Core/FIRAppCheckTests.m @@ -129,7 +129,6 @@ - (void)testInitWithApp { }]; OCMExpect([mockTokenRefresher initWithRefreshResult:refresherDateValidator - tokenExpirationThreshold:5 * 60 settings:settingsValidator]) .andReturn(mockTokenRefresher); OCMExpect([mockTokenRefresher setTokenRefreshHandler:[OCMArg any]]); diff --git a/FirebaseAppCheck/Tests/Unit/Core/FIRAppCheckTokenRefresherTests.m b/FirebaseAppCheck/Tests/Unit/Core/FIRAppCheckTokenRefresherTests.m index 0126554dc68..c464b65d3f2 100644 --- a/FirebaseAppCheck/Tests/Unit/Core/FIRAppCheckTokenRefresherTests.m +++ b/FirebaseAppCheck/Tests/Unit/Core/FIRAppCheckTokenRefresherTests.m @@ -31,7 +31,6 @@ @interface FIRAppCheckTokenRefresherTests : XCTestCase @property(nonatomic) OCMockObject *mockSettings; @property(nonatomic) FIRAppCheckTokenRefreshResult *initialTokenRefreshResult; -@property(nonatomic) NSTimeInterval tokenExpirationThreshold; @end @@ -41,9 +40,8 @@ - (void)setUp { self.mockSettings = OCMProtocolMock(@protocol(FIRAppCheckSettingsProtocol)); self.fakeTimer = [[FIRFakeTimer alloc] init]; - self.initialTokenRefreshResult = [[FIRAppCheckTokenRefreshResult alloc] initWithStatusNever]; - - self.tokenExpirationThreshold = 1 * 60; + NSDate *receivedAtDate = [NSDate date]; + self.initialTokenRefreshResult = [[FIRAppCheckTokenRefreshResult alloc] initWithStatusSuccessAndExpirationDate:[receivedAtDate dateByAddingTimeInterval:1000] receivedAtDate:receivedAtDate]; } - (void)tearDown { @@ -185,7 +183,10 @@ - (void)testNextRefreshOnRefreshSuccess { // 1. Expect checking if auto-refresh allowed before scheduling initial refresh. [[[self.mockSettings expect] andReturnValue:@(YES)] isTokenAutoRefreshEnabled]; - // 2. Expect refresh handler. + // 2. Expect checking if auto-refresh allowed before calling the refresh handler. + [[[self.mockSettings expect] andReturnValue:@(YES)] isTokenAutoRefreshEnabled]; + + // 3. Expect refresh handler. XCTestExpectation *initialRefreshExpectation = [self expectationWithDescription:@"initial refresh"]; refresher.tokenRefreshHandler = ^(FIRAppCheckTokenRefreshCompletion _Nonnull completion) { @@ -198,7 +199,7 @@ - (void)testNextRefreshOnRefreshSuccess { }); }; - // 3. Expect for new timer to be created. + // 4. Expect for new timer to be created. NSDate *expectedFireDate = [self expectedRefreshDateWithReceivedDate:refreshResult.tokenReceivedAtDate expirationDate:refreshResult.tokenExpirationDate]; @@ -208,10 +209,10 @@ - (void)testNextRefreshOnRefreshSuccess { XCTAssertEqualObjects(fireDate, expectedFireDate); }; - // 4. Expect checking if auto-refresh allowed before refreshing. + // 5. Expect checking if auto-refresh allowed before refreshing. [[[self.mockSettings expect] andReturnValue:@(YES)] isTokenAutoRefreshEnabled]; - // 5. Fire initial timer and wait for expectations. + // 6. Fire initial timer and wait for expectations. [self fireTimer]; [self waitForExpectations:@[ initialRefreshExpectation, createTimerExpectation ] @@ -232,7 +233,10 @@ - (void)testBackoff { [[[self.mockSettings expect] andReturnValue:@(YES)] isTokenAutoRefreshEnabled]; for (NSInteger i = 0; i < 10; i++) { - // 2. Expect refresh handler. + // 2. Expect checking if auto-refresh allowed before calling the refresh handler. + [[[self.mockSettings expect] andReturnValue:@(YES)] isTokenAutoRefreshEnabled]; + + // 3. Expect refresh handler. XCTestExpectation *initialRefreshExpectation = [self expectationWithDescription:@"initial refresh"]; refresher.tokenRefreshHandler = ^(FIRAppCheckTokenRefreshCompletion _Nonnull completion) { @@ -247,7 +251,7 @@ - (void)testBackoff { }); }; - // 3. Expect for new timer to be created. + // 4. Expect for new timer to be created. // No backoff initially, 1st backoff 30sec, double backoff on each next attempt until 16min. expectedBackoffTime = expectedBackoffTime == 0 ? 30 : expectedBackoffTime * 2; expectedBackoffTime = MIN(expectedBackoffTime, maximumBackoffTime); @@ -262,10 +266,10 @@ - (void)testBackoff { XCTAssertLessThan(ABS([expectedFireDate timeIntervalSinceDate:fireDate]), 2); }; - // 4. Expect checking if auto-refresh allowed before refreshing. + // 5. Expect checking if auto-refresh allowed before refreshing. [[[self.mockSettings expect] andReturnValue:@(YES)] isTokenAutoRefreshEnabled]; - // 5. Fire initial timer and wait for expectations. + // 6. Fire initial timer and wait for expectations. [self fireTimer]; [self waitForExpectations:@[ initialRefreshExpectation, createTimerExpectation ] @@ -287,23 +291,19 @@ - (void)testNoInitialRefreshWhenAutoRefreshIsNotAllowed { [[[self.mockSettings expect] andReturnValue:@(NO)] isTokenAutoRefreshEnabled]; // 2. Don't expect timer to be scheduled. - NSDate *expectedTimerFireDate = [self.initialTokenRefreshResult.tokenExpirationDate - dateByAddingTimeInterval:-self.tokenExpirationThreshold]; XCTestExpectation *timerCreateExpectation = [self expectationWithDescription:@"create timer"]; timerCreateExpectation.inverted = YES; __auto_type weakSelf = self; self.fakeTimer.createHandler = ^(NSDate *_Nonnull fireDate) { weakSelf.fakeTimer.createHandler = nil; - XCTAssertEqualObjects(fireDate, expectedTimerFireDate); [timerCreateExpectation fulfill]; }; // 3. Don't expect refresh handler to be called. __auto_type refreshResult = [[FIRAppCheckTokenRefreshResult alloc] - initWithStatusSuccessAndExpirationDate:[expectedTimerFireDate - dateByAddingTimeInterval:60 * 60] - receivedAtDate:expectedTimerFireDate]; + initWithStatusSuccessAndExpirationDate:[NSDate dateWithTimeIntervalSinceNow:60 * 60] + receivedAtDate:[NSDate date]]; XCTestExpectation *refreshExpectation = [self expectationWithDescription:@"refresh"]; refreshExpectation.inverted = YES; @@ -369,7 +369,7 @@ - (void)testNoRefreshWhenAutoRefreshWasDisabledAfterInit { #pragma mark - Update token expiration -- (void)testUpdateTokenExpirationDateWhenAutoRefreshIsAllowed { +- (void)testUpdateWithRefreshResultWhenAutoRefreshIsAllowed { FIRAppCheckTokenRefresher *refresher = [self createRefresher]; NSDate *newExpirationDate = @@ -403,28 +403,56 @@ - (void)testUpdateTokenExpirationDateWhenAutoRefreshIsAllowed { OCMVerifyAll(self.mockSettings); } -- (void)testUpdateTokenExpirationDateWhenAutoRefreshIsNotAllowed { +- (void)testUpdateWithRefreshResultWhenAutoRefreshIsNotAllowed { FIRAppCheckTokenRefresher *refresher = [self createRefresher]; - NSDate *newExpirationDate = - [self.initialTokenRefreshResult.tokenExpirationDate dateByAddingTimeInterval:10 * 60]; __auto_type newRefreshResult = [[FIRAppCheckTokenRefreshResult alloc] - initWithStatusSuccessAndExpirationDate:newExpirationDate + initWithStatusSuccessAndExpirationDate:[NSDate dateWithTimeIntervalSinceNow:60 * 60] receivedAtDate:self.initialTokenRefreshResult.tokenExpirationDate]; // 1. Expect checking if auto-refresh allowed before scheduling initial refresh. [[[self.mockSettings expect] andReturnValue:@(NO)] isTokenAutoRefreshEnabled]; // 2. Don't expect timer to be scheduled. - NSDate *expectedTimerFireDate = - [newExpirationDate dateByAddingTimeInterval:-self.tokenExpirationThreshold]; XCTestExpectation *timerCreateExpectation = [self expectationWithDescription:@"create timer"]; timerCreateExpectation.inverted = YES; __auto_type weakSelf = self; self.fakeTimer.createHandler = ^(NSDate *_Nonnull fireDate) { weakSelf.fakeTimer.createHandler = nil; - XCTAssertEqualObjects(fireDate, expectedTimerFireDate); + [timerCreateExpectation fulfill]; + }; + + // 3. Update token expiration date. + [refresher updateWithRefreshResult:newRefreshResult]; + + // 4. Wait for timer to be created. + [self waitForExpectations:@[ timerCreateExpectation ] timeout:1]; + + OCMVerifyAll(self.mockSettings); +} + +- (void)testUpdateWithRefreshResult_WhenTokenExpiresLessThanIn1Minute { + FIRAppCheckTokenRefresher *refresher = [self createRefresher]; + + NSDate *newExpirationDate = + [NSDate dateWithTimeIntervalSinceNow: 0.5 * 60]; + __auto_type newRefreshResult = [[FIRAppCheckTokenRefreshResult alloc] + initWithStatusSuccessAndExpirationDate:newExpirationDate + receivedAtDate:[NSDate date]]; + + // 1. Expect checking if auto-refresh allowed before scheduling initial refresh. + [[[self.mockSettings expect] andReturnValue:@(YES)] isTokenAutoRefreshEnabled]; + + // 2. Expect timer to be scheduled in at least 1 minute. + XCTestExpectation *timerCreateExpectation = [self expectationWithDescription:@"create timer"]; + + __auto_type weakSelf = self; + self.fakeTimer.createHandler = ^(NSDate *_Nonnull fireDate) { + weakSelf.fakeTimer.createHandler = nil; + + // 1 minute is the minimal interval between successful refreshes. + XCTAssert([FIRDateTestUtils isDate:fireDate approximatelyEqualCurrentPlusTimeInterval:60 precision:1]); [timerCreateExpectation fulfill]; }; @@ -449,7 +477,6 @@ - (void)fireTimer { - (FIRAppCheckTokenRefresher *)createRefresher { return [[FIRAppCheckTokenRefresher alloc] initWithRefreshResult:self.initialTokenRefreshResult - tokenExpirationThreshold:self.tokenExpirationThreshold timerProvider:[self.fakeTimer fakeTimerProvider] settings:self.mockSettings]; } From 5276c56f80104cbce3b7b2322685b2c43239c56f Mon Sep 17 00:00:00 2001 From: Maksym Malyhin Date: Thu, 10 Jun 2021 18:23:45 -0400 Subject: [PATCH 28/37] style --- FirebaseAppCheck/Sources/Core/FIRAppCheck.m | 3 +-- .../Tests/Unit/Core/FIRAppCheckIntegrationTests.m | 3 +-- .../Tests/Unit/Core/FIRAppCheckTokenRefresherTests.m | 11 +++++++---- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/FirebaseAppCheck/Sources/Core/FIRAppCheck.m b/FirebaseAppCheck/Sources/Core/FIRAppCheck.m index b275828a99b..d27b996bf4b 100644 --- a/FirebaseAppCheck/Sources/Core/FIRAppCheck.m +++ b/FirebaseAppCheck/Sources/Core/FIRAppCheck.m @@ -128,8 +128,7 @@ - (nullable instancetype)initWithApp:(FIRApp *)app { FIRAppCheckTokenRefreshResult *refreshResult = [[FIRAppCheckTokenRefreshResult alloc] initWithStatusNever]; FIRAppCheckTokenRefresher *tokenRefresher = - [[FIRAppCheckTokenRefresher alloc] initWithRefreshResult:refreshResult - settings:settings]; + [[FIRAppCheckTokenRefresher alloc] initWithRefreshResult:refreshResult settings:settings]; FIRAppCheckStorage *storage = [[FIRAppCheckStorage alloc] initWithAppName:app.name appID:app.options.googleAppID diff --git a/FirebaseAppCheck/Tests/Unit/Core/FIRAppCheckIntegrationTests.m b/FirebaseAppCheck/Tests/Unit/Core/FIRAppCheckIntegrationTests.m index e2552903297..183296a7bf3 100644 --- a/FirebaseAppCheck/Tests/Unit/Core/FIRAppCheckIntegrationTests.m +++ b/FirebaseAppCheck/Tests/Unit/Core/FIRAppCheckIntegrationTests.m @@ -204,8 +204,7 @@ - (void)usageExample { - (void)disableTokenRefresher { self.mockTokenRefresher = OCMClassMock([FIRAppCheckTokenRefresher class]); OCMStub([self.mockTokenRefresher alloc]).andReturn(self.mockTokenRefresher); - OCMStub([self.mockTokenRefresher initWithRefreshResult:[OCMArg any] - settings:[OCMArg any]]) + OCMStub([self.mockTokenRefresher initWithRefreshResult:[OCMArg any] settings:[OCMArg any]]) .andReturn(self.mockTokenRefresher); } diff --git a/FirebaseAppCheck/Tests/Unit/Core/FIRAppCheckTokenRefresherTests.m b/FirebaseAppCheck/Tests/Unit/Core/FIRAppCheckTokenRefresherTests.m index c464b65d3f2..005f740862f 100644 --- a/FirebaseAppCheck/Tests/Unit/Core/FIRAppCheckTokenRefresherTests.m +++ b/FirebaseAppCheck/Tests/Unit/Core/FIRAppCheckTokenRefresherTests.m @@ -41,7 +41,9 @@ - (void)setUp { self.fakeTimer = [[FIRFakeTimer alloc] init]; NSDate *receivedAtDate = [NSDate date]; - self.initialTokenRefreshResult = [[FIRAppCheckTokenRefreshResult alloc] initWithStatusSuccessAndExpirationDate:[receivedAtDate dateByAddingTimeInterval:1000] receivedAtDate:receivedAtDate]; + self.initialTokenRefreshResult = [[FIRAppCheckTokenRefreshResult alloc] + initWithStatusSuccessAndExpirationDate:[receivedAtDate dateByAddingTimeInterval:1000] + receivedAtDate:receivedAtDate]; } - (void)tearDown { @@ -435,8 +437,7 @@ - (void)testUpdateWithRefreshResultWhenAutoRefreshIsNotAllowed { - (void)testUpdateWithRefreshResult_WhenTokenExpiresLessThanIn1Minute { FIRAppCheckTokenRefresher *refresher = [self createRefresher]; - NSDate *newExpirationDate = - [NSDate dateWithTimeIntervalSinceNow: 0.5 * 60]; + NSDate *newExpirationDate = [NSDate dateWithTimeIntervalSinceNow:0.5 * 60]; __auto_type newRefreshResult = [[FIRAppCheckTokenRefreshResult alloc] initWithStatusSuccessAndExpirationDate:newExpirationDate receivedAtDate:[NSDate date]]; @@ -452,7 +453,9 @@ - (void)testUpdateWithRefreshResult_WhenTokenExpiresLessThanIn1Minute { weakSelf.fakeTimer.createHandler = nil; // 1 minute is the minimal interval between successful refreshes. - XCTAssert([FIRDateTestUtils isDate:fireDate approximatelyEqualCurrentPlusTimeInterval:60 precision:1]); + XCTAssert([FIRDateTestUtils isDate:fireDate + approximatelyEqualCurrentPlusTimeInterval:60 + precision:1]); [timerCreateExpectation fulfill]; }; From 40fc64b87be68185b74c1e50bd0d365f265f1249 Mon Sep 17 00:00:00 2001 From: Maksym Malyhin Date: Thu, 10 Jun 2021 18:32:40 -0400 Subject: [PATCH 29/37] Comments --- .../Tests/Unit/Core/FIRAppCheckTokenRefresherTests.m | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/FirebaseAppCheck/Tests/Unit/Core/FIRAppCheckTokenRefresherTests.m b/FirebaseAppCheck/Tests/Unit/Core/FIRAppCheckTokenRefresherTests.m index 005f740862f..df5e905acbb 100644 --- a/FirebaseAppCheck/Tests/Unit/Core/FIRAppCheckTokenRefresherTests.m +++ b/FirebaseAppCheck/Tests/Unit/Core/FIRAppCheckTokenRefresherTests.m @@ -380,7 +380,7 @@ - (void)testUpdateWithRefreshResultWhenAutoRefreshIsAllowed { initWithStatusSuccessAndExpirationDate:newExpirationDate receivedAtDate:self.initialTokenRefreshResult.tokenExpirationDate]; - // 1. Expect checking if auto-refresh allowed before scheduling initial refresh. + // 1. Expect checking if auto-refresh allowed before scheduling refresh. [[[self.mockSettings expect] andReturnValue:@(YES)] isTokenAutoRefreshEnabled]; // 2. Expect timer to be scheduled. @@ -442,7 +442,7 @@ - (void)testUpdateWithRefreshResult_WhenTokenExpiresLessThanIn1Minute { initWithStatusSuccessAndExpirationDate:newExpirationDate receivedAtDate:[NSDate date]]; - // 1. Expect checking if auto-refresh allowed before scheduling initial refresh. + // 1. Expect checking if auto-refresh allowed before scheduling refresh. [[[self.mockSettings expect] andReturnValue:@(YES)] isTokenAutoRefreshEnabled]; // 2. Expect timer to be scheduled in at least 1 minute. From f4a7c7f56aa01b189bad6daccc5e0c2c8e46ae1b Mon Sep 17 00:00:00 2001 From: Maksym Malyhin Date: Thu, 10 Jun 2021 18:41:53 -0400 Subject: [PATCH 30/37] Cleanup and comments --- .../Sources/Core/TokenRefresh/FIRAppCheckTokenRefreshResult.h | 2 -- .../Sources/Core/TokenRefresh/FIRAppCheckTokenRefresher.h | 1 - .../Sources/Core/TokenRefresh/FIRAppCheckTokenRefresher.m | 2 +- 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/FirebaseAppCheck/Sources/Core/TokenRefresh/FIRAppCheckTokenRefreshResult.h b/FirebaseAppCheck/Sources/Core/TokenRefresh/FIRAppCheckTokenRefreshResult.h index 612a8d3efd1..f67ca798f2e 100644 --- a/FirebaseAppCheck/Sources/Core/TokenRefresh/FIRAppCheckTokenRefreshResult.h +++ b/FirebaseAppCheck/Sources/Core/TokenRefresh/FIRAppCheckTokenRefreshResult.h @@ -27,8 +27,6 @@ typedef NS_ENUM(NSInteger, FIRAppCheckTokenRefreshStatus) { // The token was successfully refreshed. FIRAppCheckTokenRefreshStatusSuccess, - // FIRAppCheckTokenRefreshStatusSuccess, - // The token refresh failed. FIRAppCheckTokenRefreshStatusFailure }; diff --git a/FirebaseAppCheck/Sources/Core/TokenRefresh/FIRAppCheckTokenRefresher.h b/FirebaseAppCheck/Sources/Core/TokenRefresh/FIRAppCheckTokenRefresher.h index 97ce94538be..26bdca9acfc 100644 --- a/FirebaseAppCheck/Sources/Core/TokenRefresh/FIRAppCheckTokenRefresher.h +++ b/FirebaseAppCheck/Sources/Core/TokenRefresh/FIRAppCheckTokenRefresher.h @@ -54,7 +54,6 @@ typedef void (^FIRAppCheckTokenRefreshBlock)(FIRAppCheckTokenRefreshCompletion c /// The designated initializer. /// @param refreshResult A previous token refresh attempt result. -/// seconds before the actual token expiration time. /// @param settings An object that handles Firebase app check settings. - (instancetype)initWithRefreshResult:(FIRAppCheckTokenRefreshResult *)refreshResult timerProvider:(FIRTimerProvider)timerProvider diff --git a/FirebaseAppCheck/Sources/Core/TokenRefresh/FIRAppCheckTokenRefresher.m b/FirebaseAppCheck/Sources/Core/TokenRefresh/FIRAppCheckTokenRefresher.m index 0b0ab969600..be52ace97e5 100644 --- a/FirebaseAppCheck/Sources/Core/TokenRefresh/FIRAppCheckTokenRefresher.m +++ b/FirebaseAppCheck/Sources/Core/TokenRefresh/FIRAppCheckTokenRefresher.m @@ -180,7 +180,7 @@ - (NSDate *)nextRefreshDateWithTokenRefreshResult:(FIRAppCheckTokenRefreshResult } break; case FIRAppCheckTokenRefreshStatusFailure: { - // Refresh after a timeout. + // Repeat refresh attempt later. NSTimeInterval backoffTime = [[self class] backoffTimeForRetryCount:self.retryCount]; return [NSDate dateWithTimeIntervalSinceNow:backoffTime]; } break; From a9812ca479737876f49111b3dbd0c4c956b86fca Mon Sep 17 00:00:00 2001 From: Maksym Malyhin Date: Thu, 10 Jun 2021 18:49:08 -0400 Subject: [PATCH 31/37] Fix catalyst --- .../AppAttestProvider/DCAppAttestService+FIRAppAttestService.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FirebaseAppCheck/Sources/AppAttestProvider/DCAppAttestService+FIRAppAttestService.m b/FirebaseAppCheck/Sources/AppAttestProvider/DCAppAttestService+FIRAppAttestService.m index 412221d4b2b..f47848724c9 100644 --- a/FirebaseAppCheck/Sources/AppAttestProvider/DCAppAttestService+FIRAppAttestService.m +++ b/FirebaseAppCheck/Sources/AppAttestProvider/DCAppAttestService+FIRAppAttestService.m @@ -17,7 +17,7 @@ #import "FirebaseAppCheck/Sources/AppAttestProvider/DCAppAttestService+FIRAppAttestService.h" // Currently DCAppAttestService is available on iOS only. -#if TARGET_OS_IOS +#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST // Catalyst should be possible with Xcode 12.5+ @implementation DCAppAttestService (FIRAppAttestService) From 4713d0804b7d3485581558fa6e16c3c2effac0f4 Mon Sep 17 00:00:00 2001 From: Maksym Malyhin Date: Thu, 10 Jun 2021 18:53:09 -0400 Subject: [PATCH 32/37] Changelog --- FirebaseAppCheck/CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/FirebaseAppCheck/CHANGELOG.md b/FirebaseAppCheck/CHANGELOG.md index f63a3726530..4b1b5fc9e71 100644 --- a/FirebaseAppCheck/CHANGELOG.md +++ b/FirebaseAppCheck/CHANGELOG.md @@ -1,4 +1,5 @@ -# 8.1.0 -- M97 +# 8.1.0 -- M98 - [added] Apple's App Attest attestation provider support. (#8133) +- [changed] Token auto-refresh optimizations. (#8232) # v8.0.0 -- M95 - [added] Firebase abuse reduction support SDK. (#7928, #7937, #7948) \ No newline at end of file From 9825b6fc97cbdc0011f5c82466eccb08eb9d3c4d Mon Sep 17 00:00:00 2001 From: Maksym Malyhin Date: Fri, 11 Jun 2021 09:14:01 -0400 Subject: [PATCH 33/37] Changelog version fix --- FirebaseAppCheck/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FirebaseAppCheck/CHANGELOG.md b/FirebaseAppCheck/CHANGELOG.md index 4b1b5fc9e71..612465303b3 100644 --- a/FirebaseAppCheck/CHANGELOG.md +++ b/FirebaseAppCheck/CHANGELOG.md @@ -1,4 +1,4 @@ -# 8.1.0 -- M98 +# 8.2.0 -- M98 - [added] Apple's App Attest attestation provider support. (#8133) - [changed] Token auto-refresh optimizations. (#8232) # v8.0.0 -- M95 From 3ec8edc7fe4f8713e233f98df63ca4689e10be49 Mon Sep 17 00:00:00 2001 From: Maksym Malyhin Date: Fri, 11 Jun 2021 15:16:06 -0400 Subject: [PATCH 34/37] Typo --- .../Sources/Core/TokenRefresh/FIRAppCheckTokenRefreshResult.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FirebaseAppCheck/Sources/Core/TokenRefresh/FIRAppCheckTokenRefreshResult.h b/FirebaseAppCheck/Sources/Core/TokenRefresh/FIRAppCheckTokenRefreshResult.h index f67ca798f2e..8d899efa61f 100644 --- a/FirebaseAppCheck/Sources/Core/TokenRefresh/FIRAppCheckTokenRefreshResult.h +++ b/FirebaseAppCheck/Sources/Core/TokenRefresh/FIRAppCheckTokenRefreshResult.h @@ -52,7 +52,7 @@ typedef NS_ENUM(NSInteger, FIRAppCheckTokenRefreshStatus) { /// Initializes the instance with `FIRAppCheckTokenRefreshStatusFailure`. - (instancetype)initWithStatusFailure; -/// Initializes the instance with `FIRAppCheckTokenRefreshStatusFailure`. +/// Initializes the instance with `FIRAppCheckTokenRefreshStatusSuccess`. /// @param tokenExpirationDate See `tokenExpirationDate` property. /// @param tokenReceivedAtDate See `tokenReceivedAtDate` property. - (instancetype)initWithStatusSuccessAndExpirationDate:(NSDate *)tokenExpirationDate From 26241c6039cdbf808e2c21454581816398c4203a Mon Sep 17 00:00:00 2001 From: Maksym Malyhin Date: Fri, 11 Jun 2021 15:17:02 -0400 Subject: [PATCH 35/37] Cleanup --- .../Tests/Unit/Core/FIRAppCheckTokenRefresherTests.m | 2 -- 1 file changed, 2 deletions(-) diff --git a/FirebaseAppCheck/Tests/Unit/Core/FIRAppCheckTokenRefresherTests.m b/FirebaseAppCheck/Tests/Unit/Core/FIRAppCheckTokenRefresherTests.m index df5e905acbb..38e1ff3ec1e 100644 --- a/FirebaseAppCheck/Tests/Unit/Core/FIRAppCheckTokenRefresherTests.m +++ b/FirebaseAppCheck/Tests/Unit/Core/FIRAppCheckTokenRefresherTests.m @@ -282,8 +282,6 @@ - (void)testBackoff { OCMVerifyAll(self.mockSettings); } -//- (void)test - #pragma mark - Auto refresh is not allowed - (void)testNoInitialRefreshWhenAutoRefreshIsNotAllowed { From f9d140d5964544e6f11fd51ae97198e91b8d68e8 Mon Sep 17 00:00:00 2001 From: Maksym Malyhin Date: Fri, 11 Jun 2021 15:17:50 -0400 Subject: [PATCH 36/37] Typo --- .../Tests/Unit/Core/FIRAppCheckTokenRefresherTests.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FirebaseAppCheck/Tests/Unit/Core/FIRAppCheckTokenRefresherTests.m b/FirebaseAppCheck/Tests/Unit/Core/FIRAppCheckTokenRefresherTests.m index 38e1ff3ec1e..2ffa18441d3 100644 --- a/FirebaseAppCheck/Tests/Unit/Core/FIRAppCheckTokenRefresherTests.m +++ b/FirebaseAppCheck/Tests/Unit/Core/FIRAppCheckTokenRefresherTests.m @@ -487,7 +487,7 @@ - (NSDate *)expectedRefreshDateWithReceivedDate:(NSDate *)receivedDate NSTimeInterval timeToLive = [expirationDate timeIntervalSinceDate:receivedDate]; XCTAssertGreaterThanOrEqual(timeToLive, 0); - NSTimeInterval timeToRefresh = timeToLive / 2 + 5 * 60; // 50% or TTL + 5 min + NSTimeInterval timeToRefresh = timeToLive / 2 + 5 * 60; // 50% of TTL + 5 min NSTimeInterval minimalAutoRefreshInterval = 60; // 1 min timeToRefresh = MAX(timeToRefresh, minimalAutoRefreshInterval); From c6aa5ca585f6716a85cea2e34eacd9b22c6c5133 Mon Sep 17 00:00:00 2001 From: Maksym Malyhin Date: Fri, 11 Jun 2021 15:32:49 -0400 Subject: [PATCH 37/37] Imports order --- FirebaseAppCheck/Sources/Core/FIRAppCheck.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FirebaseAppCheck/Sources/Core/FIRAppCheck.m b/FirebaseAppCheck/Sources/Core/FIRAppCheck.m index d27b996bf4b..4e73929f5a3 100644 --- a/FirebaseAppCheck/Sources/Core/FIRAppCheck.m +++ b/FirebaseAppCheck/Sources/Core/FIRAppCheck.m @@ -22,13 +22,13 @@ #import "FBLPromises.h" #endif -#import "FirebaseAppCheck/Sources/Core/FIRAppCheckToken+Internal.h" #import "FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRAppCheckProvider.h" #import "FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRAppCheckProviderFactory.h" #import "FirebaseAppCheck/Sources/Core/Errors/FIRAppCheckErrorUtil.h" #import "FirebaseAppCheck/Sources/Core/FIRAppCheckLogger.h" #import "FirebaseAppCheck/Sources/Core/FIRAppCheckSettings.h" +#import "FirebaseAppCheck/Sources/Core/FIRAppCheckToken+Internal.h" #import "FirebaseAppCheck/Sources/Core/FIRAppCheckTokenResult.h" #import "FirebaseAppCheck/Sources/Core/Storage/FIRAppCheckStorage.h" #import "FirebaseAppCheck/Sources/Core/TokenRefresh/FIRAppCheckTokenRefreshResult.h" pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy