Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
3 changes: 3 additions & 0 deletions FirebaseAuth/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# Unreleased
- [fixed] Fall back to reCAPTCHA for phone auth app verification if the push notification is not received before the timeout. (#8653)

# 8.6.0
- [fixed] Annotated platform-level availability using `API_UNAVAILABLE` instead of conditionally compiling certain methods with `#if` directives (#8451).

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -629,8 +629,12 @@ - (void)verifyClientWithUIDelegate:(nullable id<FIRAuthUIDelegate>)UIDelegate
FIRLogWarning(kFIRLoggerAuth, @"I-AUT000014",
@"Failed to receive remote notification "
@"to verify app identity within "
@"%.0f second(s)",
@"%.0f second(s), falling back to "
@"reCAPTCHA verification.",
timeout);
[self reCAPTCHAFlowWithUIDelegate:UIDelegate
completion:completion];
return;
}
completion(credential, nil, nil);
}];
Expand Down
161 changes: 161 additions & 0 deletions FirebaseAuth/Tests/Unit/FIRPhoneAuthProviderTests.m
Original file line number Diff line number Diff line change
Expand Up @@ -1434,6 +1434,167 @@ - (void)testMissingAPNSToken {
OCMVerifyAll(_mockNotificationManager);
}

/** @fn testVerifyPhoneNumberUIDelegateiOSSecretMissingFlow
@brief Tests a successful invocation of @c verifyPhoneNumber:UIDelegate:completion: that falls
back to the reCAPTCHA flow when the push notification is not received before the timeout.
*/
- (void)testVerifyPhoneNumberUIDelegateiOSSecretMissingFlow {
id mockBundle = OCMClassMock([NSBundle class]);
OCMStub(ClassMethod([mockBundle mainBundle])).andReturn(mockBundle);
OCMStub([mockBundle objectForInfoDictionaryKey:@"CFBundleURLTypes"]).andReturn(@[
@{@"CFBundleURLSchemes" : @[ kFakeEncodedFirebaseAppID ]}
]);
OCMStub([mockBundle bundleIdentifier]).andReturn(kFakeBundleID);

_provider = [FIRPhoneAuthProvider providerWithAuth:_mockAuth];

OCMExpect([_mockNotificationManager checkNotificationForwardingWithCallback:OCMOCK_ANY])
.andCallBlock1(^(FIRAuthNotificationForwardingCallback callback) {
callback(YES);
});
OCMExpect([_mockAppCredentialManager credential]).andReturn(nil);
NSData *data = [@"!@#$%^" dataUsingEncoding:NSUTF8StringEncoding];
FIRAuthAPNSToken *token = [[FIRAuthAPNSToken alloc] initWithData:data
type:FIRAuthAPNSTokenTypeProd];
OCMExpect([_mockAPNSTokenManager getTokenWithCallback:OCMOCK_ANY])
.andCallBlock1(^(FIRAuthAPNSTokenCallback callback) {
callback(token, nil);
});
// Expect verify client request to the backend.
OCMExpect([_mockBackend verifyClient:[OCMArg any] callback:[OCMArg any]])
.andCallBlock2(^(FIRVerifyClientRequest *request, FIRVerifyClientResponseCallback callback) {
XCTAssertEqualObjects(request.appToken, @"21402324255E");
XCTAssertFalse(request.isSandbox);
dispatch_async(FIRAuthGlobalWorkQueue(), ^() {
id mockVerifyClientResponse = OCMClassMock([FIRVerifyClientResponse class]);
OCMStub([mockVerifyClientResponse receipt]).andReturn(kTestReceipt);
OCMStub([mockVerifyClientResponse suggestedTimeOutDate])
.andReturn([NSDate dateWithTimeIntervalSinceNow:kTestTimeout]);
callback(mockVerifyClientResponse, nil);
});
});
// Mock failing to receive the push notification before the timeout.
OCMExpect([_mockAppCredentialManager didStartVerificationWithReceipt:OCMOCK_ANY
timeout:0
callback:OCMOCK_ANY])
.ignoringNonObjectArgs()
.andCallIdDoubleIdBlock(
^(NSString *receipt, NSTimeInterval timeout, FIRAuthAppCredentialCallback callback) {
XCTAssertEqualObjects(receipt, kTestReceipt);
// Unfortunately 'ignoringNonObjectArgs' means the real value for 'timeout' doesn't get
// passed into the block either, so we can't verify it here.
dispatch_async(FIRAuthGlobalWorkQueue(), ^() {
callback([[FIRAuthAppCredential alloc] initWithReceipt:kTestReceipt secret:nil]);
});
});

// Falls back to reCAPTCHA flow.
OCMExpect([_mockBackend getProjectConfig:[OCMArg any] callback:[OCMArg any]])
.andCallBlock2(
^(FIRGetProjectConfigRequest *request, FIRGetProjectConfigResponseCallback callback) {
XCTAssertNotNil(request);
dispatch_async(FIRAuthGlobalWorkQueue(), ^() {
id mockGetProjectConfigResponse = OCMClassMock([FIRGetProjectConfigResponse class]);
OCMStub([mockGetProjectConfigResponse authorizedDomains]).andReturn(@[
kFakeAuthorizedDomain
]);
callback(mockGetProjectConfigResponse, nil);
});
});
id mockUIDelegate = OCMProtocolMock(@protocol(FIRAuthUIDelegate));

// Expect view controller presentation by UIDelegate.
OCMExpect([_mockURLPresenter presentURL:OCMOCK_ANY
UIDelegate:mockUIDelegate
callbackMatcher:OCMOCK_ANY
completion:OCMOCK_ANY])
.andDo(^(NSInvocation *invocation) {
__unsafe_unretained id unretainedArgument;
// Indices 0 and 1 indicate the hidden arguments self and _cmd.
// `presentURL` is at index 2.
[invocation getArgument:&unretainedArgument atIndex:2];
NSURL *presentURL = unretainedArgument;
XCTAssertEqualObjects(presentURL.scheme, @"https");
XCTAssertEqualObjects(presentURL.host, kFakeAuthorizedDomain);
XCTAssertEqualObjects(presentURL.path, @"/__/auth/handler");

NSURLComponents *actualURLComponents = [NSURLComponents componentsWithURL:presentURL
resolvingAgainstBaseURL:NO];
NSArray<NSURLQueryItem *> *queryItems = [actualURLComponents queryItems];
XCTAssertEqualObjects([FIRAuthWebUtils queryItemValue:@"ibi" from:queryItems],
kFakeBundleID);
XCTAssertEqualObjects([FIRAuthWebUtils queryItemValue:@"appId" from:queryItems],
kFakeFirebaseAppID);
XCTAssertEqualObjects([FIRAuthWebUtils queryItemValue:@"apiKey" from:queryItems],
kFakeAPIKey);
XCTAssertEqualObjects([FIRAuthWebUtils queryItemValue:@"authType" from:queryItems],
@"verifyApp");
XCTAssertNotNil([FIRAuthWebUtils queryItemValue:@"v" from:queryItems]);
// `callbackMatcher` is at index 4
[invocation getArgument:&unretainedArgument atIndex:4];
FIRAuthURLCallbackMatcher callbackMatcher = unretainedArgument;
NSMutableString *redirectURL = [NSMutableString
stringWithString:[kFakeEncodedFirebaseAppID
stringByAppendingString:kFakeRedirectURLStringWithReCAPTCHAToken]];
// Verify that the URL is rejected by the callback matcher without the event ID.
XCTAssertFalse(callbackMatcher([NSURL URLWithString:redirectURL]));
[redirectURL appendString:@"%26eventId%3D"];
[redirectURL appendString:[FIRAuthWebUtils queryItemValue:@"eventId" from:queryItems]];
NSURLComponents *originalComponents = [[NSURLComponents alloc] initWithString:redirectURL];
// Verify that the URL is accepted by the callback matcher with the matching event ID.
XCTAssertTrue(callbackMatcher([originalComponents URL]));
NSURLComponents *components = [originalComponents copy];
components.query = @"https";
XCTAssertFalse(callbackMatcher([components URL]));
components = [originalComponents copy];
components.host = @"badhost";
XCTAssertFalse(callbackMatcher([components URL]));
components = [originalComponents copy];
components.path = @"badpath";
XCTAssertFalse(callbackMatcher([components URL]));
components = [originalComponents copy];
components.query = @"badquery";
XCTAssertFalse(callbackMatcher([components URL]));

// `completion` is at index 5
[invocation getArgument:&unretainedArgument atIndex:5];
FIRAuthURLPresentationCompletion completion = unretainedArgument;
dispatch_async(FIRAuthGlobalWorkQueue(), ^() {
completion([NSURL URLWithString:[kFakeEncodedFirebaseAppID
stringByAppendingString:
kFakeRedirectURLStringWithReCAPTCHAToken]],
nil);
});
});

OCMExpect([_mockBackend sendVerificationCode:[OCMArg any] callback:[OCMArg any]])
.andCallBlock2(^(FIRSendVerificationCodeRequest *request,
FIRSendVerificationCodeResponseCallback callback) {
XCTAssertEqualObjects(request.phoneNumber, kTestPhoneNumber);
XCTAssertNil(request.appCredential);
XCTAssertEqualObjects(request.reCAPTCHAToken, kFakeReCAPTCHAToken);
dispatch_async(FIRAuthGlobalWorkQueue(), ^() {
id mockSendVerificationCodeResponse =
OCMClassMock([FIRSendVerificationCodeResponse class]);
OCMStub([mockSendVerificationCodeResponse verificationID]).andReturn(kTestVerificationID);
callback(mockSendVerificationCodeResponse, nil);
});
});

XCTestExpectation *expectation = [self expectationWithDescription:@"callback"];
[_provider verifyPhoneNumber:kTestPhoneNumber
UIDelegate:mockUIDelegate
completion:^(NSString *_Nullable verificationID, NSError *_Nullable error) {
XCTAssertTrue([NSThread isMainThread]);
XCTAssertNil(error);
XCTAssertEqualObjects(verificationID, kTestVerificationID);
[expectation fulfill];
}];
[self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];
OCMVerifyAll(_mockBackend);
OCMVerifyAll(_mockNotificationManager);
}

/** @fn testVerifyClient
@brief Tests verifying client before sending verification code.
*/
Expand Down