diff --git a/.github/workflows/app_check_core.yml b/.github/workflows/app_check_core.yml index e89b0d3..f28b90d 100644 --- a/.github/workflows/app_check_core.yml +++ b/.github/workflows/app_check_core.yml @@ -21,7 +21,7 @@ jobs: - os: macos-15 xcode: Xcode_16.4 - os: macos-26 - xcode: Xcode_26.1 + xcode: Xcode_26.2 runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v3 diff --git a/AppCheckCore/Sources/DebugProvider/GACAppCheckDebugProvider.m b/AppCheckCore/Sources/DebugProvider/GACAppCheckDebugProvider.m index 16f3aa6..31bc12c 100644 --- a/AppCheckCore/Sources/DebugProvider/GACAppCheckDebugProvider.m +++ b/AppCheckCore/Sources/DebugProvider/GACAppCheckDebugProvider.m @@ -36,18 +36,34 @@ static NSString *const kFirebaseDebugTokenEnvKey = @"FIRAAppCheckDebugToken"; static NSString *const kDebugTokenUserDefaultsKey = @"GACAppCheckDebugToken"; +// The base key for registration status. +// NOTE: Do not use this key directly. Use `registeredUserDefaultsKeyForServiceName:resourceName:` +// to obtain the namespaced key. +static NSString *const kDebugTokenRegisteredUserDefaultsKey = @"GACAppCheckDebugTokenRegistered"; + @interface GACAppCheckDebugProvider () @property(nonatomic, readonly) id APIService; @property(nonatomic, readonly, nullable, copy) NSString *debugTokenEnvValue; +@property(nonatomic, readonly, copy) NSString *registeredUserDefaultsKey; + ++ (NSString *)registeredUserDefaultsKeyForServiceName:(NSString *)serviceName + resourceName:(NSString *)resourceName; @end +static NSString *_Nullable EnvironmentVariableDebugToken(NSString *registeredUserDefaultsKey); + @implementation GACAppCheckDebugProvider -- (instancetype)initWithAPIService:(id)APIService { +- (instancetype)initWithAPIService:(id)APIService + serviceName:(NSString *)serviceName + resourceName:(NSString *)resourceName { self = [super init]; if (self) { _APIService = APIService; - _debugTokenEnvValue = EnvironmentVariableDebugToken(); + _registeredUserDefaultsKey = + [[[self class] registeredUserDefaultsKeyForServiceName:serviceName + resourceName:resourceName] copy]; + _debugTokenEnvValue = EnvironmentVariableDebugToken(_registeredUserDefaultsKey); } return self; } @@ -70,7 +86,9 @@ - (instancetype)initWithServiceName:(NSString *)serviceName [[GACAppCheckDebugProviderAPIService alloc] initWithAPIService:APIService resourceName:resourceName]; - return [self initWithAPIService:debugAPIService]; + return [self initWithAPIService:debugAPIService + serviceName:serviceName + resourceName:resourceName]; } - (NSString *)currentDebugToken { @@ -108,6 +126,7 @@ - (void)getTokenWithLimitedUse:(BOOL)limitedUse return [self.APIService appCheckTokenWithDebugToken:debugToken limitedUse:limitedUse]; }) .then(^id(GACAppCheckToken *appCheckToken) { + [[GULUserDefaults standardUserDefaults] setBool:YES forKey:self.registeredUserDefaultsKey]; handler(appCheckToken, nil); return nil; }) @@ -115,6 +134,10 @@ - (void)getTokenWithLimitedUse:(BOOL)limitedUse NSString *logMessage = [NSString stringWithFormat:@"Failed to exchange debug token to app check token: %@", error]; GACAppCheckLogDebug(GACLoggerAppCheckMessageDebugProviderFailedExchange, logMessage); + if (error.code != GACAppCheckErrorCodeServerUnreachable) { + [[GULUserDefaults standardUserDefaults] + removeObjectForKey:self.registeredUserDefaultsKey]; + } handler(nil, error); }); } @@ -133,7 +156,19 @@ - (void)getTokenWithLimitedUse:(BOOL)limitedUse return token; } -static NSString *_Nullable EnvironmentVariableDebugToken(void) { ++ (NSString *)registeredUserDefaultsKeyForServiceName:(NSString *)serviceName + resourceName:(NSString *)resourceName { + NSString *safeServiceName = serviceName.length > 0 ? serviceName : @"default"; + NSString *safeResourceName = [resourceName stringByReplacingOccurrencesOfString:@"/" + withString:@"_"]; + if (safeResourceName.length == 0) { + safeResourceName = @"default"; + } + return [NSString stringWithFormat:@"%@_%@_%@", kDebugTokenRegisteredUserDefaultsKey, + safeServiceName, safeResourceName]; +} + +static NSString *_Nullable EnvironmentVariableDebugToken(NSString *registeredUserDefaultsKey) { NSDictionary *environment = [[NSProcessInfo processInfo] environment]; NSString *envVariableValue = environment[kDebugTokenEnvKey]; NSString *firebaseEnvVariableValue = environment[kFirebaseDebugTokenEnvKey]; @@ -175,9 +210,14 @@ - (void)getTokenWithLimitedUse:(BOOL)limitedUse return firebaseEnvVariableValue; } else { - // Print only a locally generated token to avoid a valid token leak on CI. - GACAppCheckLog(GACLoggerAppCheckMessageLocalDebugToken, GACAppCheckLogLevelWarning, - [NSString stringWithFormat:@"App Check debug token: '%@'.", LocalDebugToken()]); + BOOL isRegistered = + [[GULUserDefaults standardUserDefaults] boolForKey:registeredUserDefaultsKey]; + if (!isRegistered) { + // Print only a locally generated token to avoid a valid token leak on CI. + GACAppCheckLog( + GACLoggerAppCheckMessageLocalDebugToken, GACAppCheckLogLevelWarning, + [NSString stringWithFormat:@"App Check debug token: '%@'.", LocalDebugToken()]); + } return nil; } diff --git a/AppCheckCore/Tests/Unit/DebugProvider/GACAppCheckDebugProviderTests.m b/AppCheckCore/Tests/Unit/DebugProvider/GACAppCheckDebugProviderTests.m index d67ae3c..28bcce2 100644 --- a/AppCheckCore/Tests/Unit/DebugProvider/GACAppCheckDebugProviderTests.m +++ b/AppCheckCore/Tests/Unit/DebugProvider/GACAppCheckDebugProviderTests.m @@ -22,15 +22,24 @@ #import "AppCheckCore/Sources/DebugProvider/API/GACAppCheckDebugProviderAPIService.h" #import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckDebugProvider.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckErrors.h" #import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckToken.h" static NSString *const kDebugTokenEnvKey = @"AppCheckDebugToken"; static NSString *const kFirebaseDebugTokenEnvKey = @"FIRAAppCheckDebugToken"; static NSString *const kDebugTokenUserDefaultsKey = @"GACAppCheckDebugToken"; +static NSString *const kDebugTokenRegisteredUserDefaultsKey = @"GACAppCheckDebugTokenRegistered"; @interface GACAppCheckDebugProvider (Tests) -- (instancetype)initWithAPIService:(id)APIService; +- (instancetype)initWithAPIService:(id)APIService + serviceName:(NSString *)serviceName + resourceName:(NSString *)resourceName; + ++ (NSString *)registeredUserDefaultsKeyForServiceName:(NSString *)serviceName + resourceName:(NSString *)resourceName; + +@property(nonatomic, readonly, copy) NSString *registeredUserDefaultsKey; @end @@ -51,7 +60,10 @@ - (void)setUp { self.processInfoMock = OCMPartialMock([NSProcessInfo processInfo]); self.fakeAPIService = OCMProtocolMock(@protocol(GACAppCheckDebugProviderAPIServiceProtocol)); - self.provider = [[GACAppCheckDebugProvider alloc] initWithAPIService:self.fakeAPIService]; + self.provider = + [[GACAppCheckDebugProvider alloc] initWithAPIService:self.fakeAPIService + serviceName:@"test-service" + resourceName:@"projects/test-project/apps/test-app"]; } - (void)tearDown { @@ -59,6 +71,8 @@ - (void)tearDown { [self.processInfoMock stopMocking]; self.processInfoMock = nil; [[GULUserDefaults standardUserDefaults] removeObjectForKey:kDebugTokenUserDefaultsKey]; + [[GULUserDefaults standardUserDefaults] removeObjectForKey:kDebugTokenRegisteredUserDefaultsKey]; + [super tearDown]; } #pragma mark - Debug token generating/storing @@ -69,7 +83,10 @@ - (void)testCurrentTokenWhenEnvironmentVariableSetAndTokenStored { NSString *envToken = @"env token"; OCMExpect([self.processInfoMock processInfo]).andReturn(self.processInfoMock); OCMExpect([self.processInfoMock environment]).andReturn(@{kDebugTokenEnvKey : envToken}); - self.provider = [[GACAppCheckDebugProvider alloc] initWithAPIService:self.fakeAPIService]; + self.provider = + [[GACAppCheckDebugProvider alloc] initWithAPIService:self.fakeAPIService + serviceName:@"test-service" + resourceName:@"projects/test-project/apps/test-app"]; XCTAssertEqualObjects([self.provider currentDebugToken], envToken); } @@ -82,7 +99,10 @@ - (void)testCurrentTokenWhenFirebaseAndCoreEnvironmentVariablesSetAndTokenStored OCMExpect([self.processInfoMock environment]) .andReturn( (@{kDebugTokenEnvKey : envToken, kFirebaseDebugTokenEnvKey : @"firebase env token"})); - self.provider = [[GACAppCheckDebugProvider alloc] initWithAPIService:self.fakeAPIService]; + self.provider = + [[GACAppCheckDebugProvider alloc] initWithAPIService:self.fakeAPIService + serviceName:@"test-service" + resourceName:@"projects/test-project/apps/test-app"]; XCTAssertEqualObjects([self.provider currentDebugToken], envToken); } @@ -95,7 +115,10 @@ - (void)testCurrentTokenWhenFirebaseEnvironmentVariableSetAndTokenStored { OCMExpect([self.processInfoMock environment]).andReturn((@{ kFirebaseDebugTokenEnvKey : envToken })); - self.provider = [[GACAppCheckDebugProvider alloc] initWithAPIService:self.fakeAPIService]; + self.provider = + [[GACAppCheckDebugProvider alloc] initWithAPIService:self.fakeAPIService + serviceName:@"test-service" + resourceName:@"projects/test-project/apps/test-app"]; XCTAssertEqualObjects([self.provider currentDebugToken], envToken); } @@ -106,7 +129,10 @@ - (void)testCurrentTokenWhenFirebaseAndCoreEnvironmentVariablesSet { OCMExpect([self.processInfoMock environment]) .andReturn( (@{kDebugTokenEnvKey : envToken, kFirebaseDebugTokenEnvKey : @"firebase env token"})); - self.provider = [[GACAppCheckDebugProvider alloc] initWithAPIService:self.fakeAPIService]; + self.provider = + [[GACAppCheckDebugProvider alloc] initWithAPIService:self.fakeAPIService + serviceName:@"test-service" + resourceName:@"projects/test-project/apps/test-app"]; XCTAssertEqualObjects([self.provider currentDebugToken], envToken); } @@ -225,6 +251,111 @@ - (void)testGetLimitedUseTokenAPIError { OCMVerifyAll(self.fakeAPIService); } +- (void)testGetTokenSuccessSetsRegisteredFlag { + // 1. Stub API service. + NSString *expectedDebugToken = [self.provider currentDebugToken]; + GACAppCheckToken *validToken = [[GACAppCheckToken alloc] initWithToken:@"valid_token" + expirationDate:[NSDate date] + receivedAtDate:[NSDate date]]; + FBLPromise *resolvedPromise = [FBLPromise pendingPromise]; + [resolvedPromise fulfill:validToken]; + OCMExpect([self.fakeAPIService appCheckTokenWithDebugToken:expectedDebugToken limitedUse:NO]) + .andReturn(resolvedPromise); + + [[GULUserDefaults standardUserDefaults] + removeObjectForKey:self.provider.registeredUserDefaultsKey]; + + // 2. Validate get token. + [self validateGetToken:^(GACAppCheckToken *_Nullable token, NSError *_Nullable error) { + XCTAssertNil(error); + XCTAssertNotNil(token); + }]; + + // 3. Verify flag is now YES. + XCTAssertTrue( + [[GULUserDefaults standardUserDefaults] boolForKey:self.provider.registeredUserDefaultsKey]); + + // 4. Verify fakes. + OCMVerifyAll(self.fakeAPIService); +} + +- (void)testGetTokenPermanentFailureClearsRegisteredFlag { + // 1. Stub API service. + NSString *expectedDebugToken = [self.provider currentDebugToken]; + NSError *APIError = [NSError errorWithDomain:@"testGetTokenPermanentFailureClearsRegisteredFlag" + code:-1 + userInfo:nil]; + FBLPromise *rejectedPromise = [FBLPromise pendingPromise]; + [rejectedPromise reject:APIError]; + OCMExpect([self.fakeAPIService appCheckTokenWithDebugToken:expectedDebugToken limitedUse:NO]) + .andReturn(rejectedPromise); + + // Pre-populate flag to YES. + [[GULUserDefaults standardUserDefaults] setBool:YES + forKey:self.provider.registeredUserDefaultsKey]; + + // 2. Validate get token. + [self validateGetToken:^(GACAppCheckToken *_Nullable token, NSError *_Nullable error) { + XCTAssertNotNil(error); + XCTAssertNil(token); + }]; + + // 3. Verify flag is cleared. + XCTAssertNil([[GULUserDefaults standardUserDefaults] + objectForKey:self.provider.registeredUserDefaultsKey]); + + // 4. Verify fakes. + OCMVerifyAll(self.fakeAPIService); +} + +- (void)testGetTokenNetworkFailureDoesNotClearRegisteredFlag { + // 1. Stub API service. + NSString *expectedDebugToken = [self.provider currentDebugToken]; + NSError *networkError = [NSError errorWithDomain:GACAppCheckErrorDomain + code:GACAppCheckErrorCodeServerUnreachable + userInfo:nil]; + FBLPromise *rejectedPromise = [FBLPromise pendingPromise]; + [rejectedPromise reject:networkError]; + OCMExpect([self.fakeAPIService appCheckTokenWithDebugToken:expectedDebugToken limitedUse:NO]) + .andReturn(rejectedPromise); + + // Pre-populate flag to YES. + [[GULUserDefaults standardUserDefaults] setBool:YES + forKey:self.provider.registeredUserDefaultsKey]; + + // 2. Validate get token. + [self validateGetToken:^(GACAppCheckToken *_Nullable token, NSError *_Nullable error) { + XCTAssertNotNil(error); + XCTAssertNil(token); + }]; + + // 3. Verify flag is still YES. + XCTAssertTrue( + [[GULUserDefaults standardUserDefaults] boolForKey:self.provider.registeredUserDefaultsKey]); + + // 4. Verify fakes. + OCMVerifyAll(self.fakeAPIService); +} + +#pragma mark - Keys + +- (void)testRegisteredUserDefaultsKeyForServiceName_resourceName { + XCTAssertEqualObjects( + [GACAppCheckDebugProvider registeredUserDefaultsKeyForServiceName:@"app1" + resourceName:@"projects/p1/apps/a1"], + @"GACAppCheckDebugTokenRegistered_app1_projects_p1_apps_a1"); + XCTAssertEqualObjects( + [GACAppCheckDebugProvider registeredUserDefaultsKeyForServiceName:@"app2" + resourceName:@"projects/p2/apps/a2"], + @"GACAppCheckDebugTokenRegistered_app2_projects_p2_apps_a2"); + XCTAssertEqualObjects([GACAppCheckDebugProvider registeredUserDefaultsKeyForServiceName:@"" + resourceName:@""], + @"GACAppCheckDebugTokenRegistered_default_default"); + XCTAssertEqualObjects([GACAppCheckDebugProvider registeredUserDefaultsKeyForServiceName:nil + resourceName:nil], + @"GACAppCheckDebugTokenRegistered_default_default"); +} + #pragma mark - Helpers - (void)validateGetToken:(GACAppCheckTokenValidationBlock)validationBlock { diff --git a/CHANGELOG.md b/CHANGELOG.md index 0fd105f..f455b58 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # 11.3.0 -- [changed] Added Recaptcha Enterprise attestation provider. +- [changed] Added Recaptcha Enterprise attestation provider. (#94) +- [changed] Only log local debug tokens when not yet registered. (#95) # 11.2.0 - [changed] To prevent reusing expired artifacts, skip local cache when making