Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/app_check_core.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
54 changes: 47 additions & 7 deletions AppCheckCore/Sources/DebugProvider/GACAppCheckDebugProvider.m
Original file line number Diff line number Diff line change
Expand Up @@ -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<GACAppCheckDebugProviderAPIServiceProtocol> 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<GACAppCheckDebugProviderAPIServiceProtocol>)APIService {
- (instancetype)initWithAPIService:(id<GACAppCheckDebugProviderAPIServiceProtocol>)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;
}
Expand All @@ -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 {
Expand Down Expand Up @@ -108,13 +126,18 @@ - (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;
})
.catch(^void(NSError *error) {
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);
});
}
Expand All @@ -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<NSString *, NSString *> *environment = [[NSProcessInfo processInfo] environment];
NSString *envVariableValue = environment[kDebugTokenEnvKey];
NSString *firebaseEnvVariableValue = environment[kFirebaseDebugTokenEnvKey];
Expand Down Expand Up @@ -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;
}
Expand Down
143 changes: 137 additions & 6 deletions AppCheckCore/Tests/Unit/DebugProvider/GACAppCheckDebugProviderTests.m
Original file line number Diff line number Diff line change
Expand Up @@ -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<GACAppCheckDebugProviderAPIServiceProtocol>)APIService;
- (instancetype)initWithAPIService:(id<GACAppCheckDebugProviderAPIServiceProtocol>)APIService
serviceName:(NSString *)serviceName
resourceName:(NSString *)resourceName;

+ (NSString *)registeredUserDefaultsKeyForServiceName:(NSString *)serviceName
resourceName:(NSString *)resourceName;

@property(nonatomic, readonly, copy) NSString *registeredUserDefaultsKey;

@end

Expand All @@ -51,14 +60,19 @@ - (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 {
self.provider = nil;
[self.processInfoMock stopMocking];
self.processInfoMock = nil;
[[GULUserDefaults standardUserDefaults] removeObjectForKey:kDebugTokenUserDefaultsKey];
[[GULUserDefaults standardUserDefaults] removeObjectForKey:kDebugTokenRegisteredUserDefaultsKey];
[super tearDown];
}

#pragma mark - Debug token generating/storing
Expand All @@ -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);
}
Expand All @@ -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);
}
Expand All @@ -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);
}
Expand All @@ -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);
}
Expand Down Expand Up @@ -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 {
Expand Down
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Loading