Skip to content

Commit e09c321

Browse files
authored
Check if reverse client ID is a registered URL scheme before setting callback scheme (#7211)
* Check if reverse client ID is registered as a custom URL scheme before setting it as the callback scheme. * Fix existing tests. * Add tests. * Update changelog.
1 parent ac270f9 commit e09c321

File tree

5 files changed

+351
-83
lines changed

5 files changed

+351
-83
lines changed

FirebaseAuth/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
# Unreleased
2+
- [fixed] Check if the reverse client ID is configured as a custom URL scheme before setting it as the callback scheme. (#7211).
3+
14
# 7.3.0
25
- [fixed] Catalyst browser issue with `verifyPhoneNumber` API. (#7049)
36

FirebaseAuth/Sources/AuthProvider/OAuth/FIROAuthProvider.m

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,12 @@ @implementation FIROAuthProvider {
7272
@brief The callback URL scheme used for headful-lite sign-in.
7373
*/
7474
NSString *_callbackScheme;
75+
76+
/** @var _usingClientIDScheme
77+
@brief True if the reverse client ID is registered as a custom URL scheme, and false
78+
otherwise.
79+
*/
80+
BOOL _usingClientIDScheme;
7581
}
7682

7783
+ (FIROAuthCredential *)credentialWithProviderID:(NSString *)providerID
@@ -216,9 +222,16 @@ - (nullable instancetype)initWithProviderID:(NSString *)providerID auth:(FIRAuth
216222
_auth = auth;
217223
_providerID = providerID;
218224
if (_auth.app.options.clientID) {
219-
_callbackScheme = [[[_auth.app.options.clientID componentsSeparatedByString:@"."]
220-
reverseObjectEnumerator].allObjects componentsJoinedByString:@"."];
221-
} else {
225+
NSString *reverseClientIDScheme =
226+
[[[_auth.app.options.clientID componentsSeparatedByString:@"."]
227+
reverseObjectEnumerator].allObjects componentsJoinedByString:@"."];
228+
if ([FIRAuthWebUtils isCallbackSchemeRegisteredForCustomURLScheme:reverseClientIDScheme]) {
229+
_callbackScheme = reverseClientIDScheme;
230+
_usingClientIDScheme = YES;
231+
}
232+
}
233+
234+
if (!_usingClientIDScheme) {
222235
_callbackScheme = [kCustomUrlSchemePrefix
223236
stringByAppendingString:[_auth.app.options.googleAppID
224237
stringByReplacingOccurrencesOfString:@":"
@@ -304,7 +317,7 @@ - (void)getHeadFulLiteURLWithEventID:(NSString *)eventID
304317
@"eventId" : eventID,
305318
@"providerId" : strongSelf->_providerID,
306319
} mutableCopy];
307-
if (clientID) {
320+
if (strongSelf->_usingClientIDScheme) {
308321
urlArguments[@"clientId"] = clientID;
309322
} else {
310323
urlArguments[@"appId"] = appID;

FirebaseAuth/Sources/AuthProvider/Phone/FIRPhoneAuthProvider.m

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,12 @@ @implementation FIRPhoneAuthProvider {
104104
@brief The callback URL scheme used for reCAPTCHA fallback.
105105
*/
106106
NSString *_callbackScheme;
107+
108+
/** @var _usingClientIDScheme
109+
@brief True if the reverse client ID is registered as a custom URL scheme, and false
110+
otherwise.
111+
*/
112+
BOOL _usingClientIDScheme;
107113
}
108114

109115
/** @fn initWithAuth:
@@ -116,9 +122,15 @@ - (nullable instancetype)initWithAuth:(FIRAuth *)auth {
116122
if (self) {
117123
_auth = auth;
118124
if (_auth.app.options.clientID) {
119-
_callbackScheme = [[[_auth.app.options.clientID componentsSeparatedByString:@"."]
120-
reverseObjectEnumerator].allObjects componentsJoinedByString:@"."];
121-
} else {
125+
NSString *reverseClientIDScheme =
126+
[[[_auth.app.options.clientID componentsSeparatedByString:@"."]
127+
reverseObjectEnumerator].allObjects componentsJoinedByString:@"."];
128+
if ([FIRAuthWebUtils isCallbackSchemeRegisteredForCustomURLScheme:reverseClientIDScheme]) {
129+
_callbackScheme = reverseClientIDScheme;
130+
_usingClientIDScheme = YES;
131+
}
132+
}
133+
if (!_usingClientIDScheme) {
122134
_callbackScheme = [kCustomUrlSchemePrefix
123135
stringByAppendingString:[_auth.app.options.googleAppID
124136
stringByReplacingOccurrencesOfString:@":"
@@ -699,7 +711,7 @@ - (void)reCAPTCHAURLWithEventID:(NSString *)eventID completion:(FIRReCAPTCHAURLC
699711
value:[FIRAuthBackend authUserAgent]],
700712
[NSURLQueryItem queryItemWithName:@"eventId" value:eventID]
701713
] mutableCopy];
702-
if (clientID) {
714+
if (self->_usingClientIDScheme) {
703715
[queryItems
704716
addObject:[NSURLQueryItem queryItemWithName:@"clientId"
705717
value:clientID]];

FirebaseAuth/Tests/Unit/FIROAuthProviderTests.m

Lines changed: 128 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -268,16 +268,16 @@ - (void)testObtainingOAuthProvider {
268268
@brief Tests a successful invocation of @c getCredentialWithUIDelegte:completion:
269269
*/
270270
- (void)testGetCredentialWithUIDelegateWithClientID {
271-
OCMStub([_mockOptions clientID]).andReturn(kFakeClientID);
272-
_provider = [FIROAuthProvider providerWithProviderID:kFakeProviderID auth:_mockAuth];
273-
274271
id mockBundle = OCMClassMock([NSBundle class]);
275272
OCMStub(ClassMethod([mockBundle mainBundle])).andReturn(mockBundle);
276273
OCMStub([mockBundle objectForInfoDictionaryKey:@"CFBundleURLTypes"]).andReturn(@[
277274
@{@"CFBundleURLSchemes" : @[ kFakeReverseClientID ]}
278275
]);
279276
OCMStub([mockBundle bundleIdentifier]).andReturn(kFakeBundleID);
280277

278+
OCMStub([_mockOptions clientID]).andReturn(kFakeClientID);
279+
_provider = [FIROAuthProvider providerWithProviderID:kFakeProviderID auth:_mockAuth];
280+
281281
OCMExpect([_mockBackend getProjectConfig:[OCMArg any] callback:[OCMArg any]])
282282
.andCallBlock2(
283283
^(FIRGetProjectConfigRequest *request, FIRGetProjectConfigResponseCallback callback) {
@@ -371,16 +371,16 @@ - (void)testGetCredentialWithUIDelegateWithClientID {
371371
cancelation.
372372
*/
373373
- (void)testGetCredentialWithUIDelegateUserCancellationWithClientID {
374-
OCMStub([_mockOptions clientID]).andReturn(kFakeClientID);
375-
_provider = [FIROAuthProvider providerWithProviderID:kFakeProviderID auth:_mockAuth];
376-
377374
id mockBundle = OCMClassMock([NSBundle class]);
378375
OCMStub(ClassMethod([mockBundle mainBundle])).andReturn(mockBundle);
379376
OCMStub([mockBundle objectForInfoDictionaryKey:@"CFBundleURLTypes"]).andReturn(@[
380377
@{@"CFBundleURLSchemes" : @[ kFakeReverseClientID ]}
381378
]);
382379
OCMStub([mockBundle bundleIdentifier]).andReturn(kFakeBundleID);
383380

381+
OCMStub([_mockOptions clientID]).andReturn(kFakeClientID);
382+
_provider = [FIROAuthProvider providerWithProviderID:kFakeProviderID auth:_mockAuth];
383+
384384
OCMExpect([_mockBackend getProjectConfig:[OCMArg any] callback:[OCMArg any]])
385385
.andCallBlock2(
386386
^(FIRGetProjectConfigRequest *request, FIRGetProjectConfigResponseCallback callback) {
@@ -471,16 +471,16 @@ - (void)testGetCredentialWithUIDelegateUserCancellationWithClientID {
471471
failed network request within the web context.
472472
*/
473473
- (void)testGetCredentialWithUIDelegateNetworkRequestFailedWithClientID {
474-
OCMStub([_mockOptions clientID]).andReturn(kFakeClientID);
475-
_provider = [FIROAuthProvider providerWithProviderID:kFakeProviderID auth:_mockAuth];
476-
477474
id mockBundle = OCMClassMock([NSBundle class]);
478475
OCMStub(ClassMethod([mockBundle mainBundle])).andReturn(mockBundle);
479476
OCMStub([mockBundle objectForInfoDictionaryKey:@"CFBundleURLTypes"]).andReturn(@[
480477
@{@"CFBundleURLSchemes" : @[ kFakeReverseClientID ]}
481478
]);
482479
OCMStub([mockBundle bundleIdentifier]).andReturn(kFakeBundleID);
483480

481+
OCMStub([_mockOptions clientID]).andReturn(kFakeClientID);
482+
_provider = [FIROAuthProvider providerWithProviderID:kFakeProviderID auth:_mockAuth];
483+
484484
OCMExpect([_mockBackend getProjectConfig:[OCMArg any] callback:[OCMArg any]])
485485
.andCallBlock2(
486486
^(FIRGetProjectConfigRequest *request, FIRGetProjectConfigResponseCallback callback) {
@@ -569,16 +569,16 @@ - (void)testGetCredentialWithUIDelegateNetworkRequestFailedWithClientID {
569569
internal error within the web context.
570570
*/
571571
- (void)testGetCredentialWithUIDelegateInternalErrorWithClientID {
572-
OCMStub([_mockOptions clientID]).andReturn(kFakeClientID);
573-
_provider = [FIROAuthProvider providerWithProviderID:kFakeProviderID auth:_mockAuth];
574-
575572
id mockBundle = OCMClassMock([NSBundle class]);
576573
OCMStub(ClassMethod([mockBundle mainBundle])).andReturn(mockBundle);
577574
OCMStub([mockBundle objectForInfoDictionaryKey:@"CFBundleURLTypes"]).andReturn(@[
578575
@{@"CFBundleURLSchemes" : @[ kFakeReverseClientID ]}
579576
]);
580577
OCMStub([mockBundle bundleIdentifier]).andReturn(kFakeBundleID);
581578

579+
OCMStub([_mockOptions clientID]).andReturn(kFakeClientID);
580+
_provider = [FIROAuthProvider providerWithProviderID:kFakeProviderID auth:_mockAuth];
581+
582582
OCMExpect([_mockBackend getProjectConfig:[OCMArg any] callback:[OCMArg any]])
583583
.andCallBlock2(
584584
^(FIRGetProjectConfigRequest *request, FIRGetProjectConfigResponseCallback callback) {
@@ -668,16 +668,16 @@ - (void)testGetCredentialWithUIDelegateInternalErrorWithClientID {
668668
use of an invalid client ID.
669669
*/
670670
- (void)testGetCredentialWithUIDelegateInvalidClientID {
671-
OCMStub([_mockOptions clientID]).andReturn(kFakeClientID);
672-
_provider = [FIROAuthProvider providerWithProviderID:kFakeProviderID auth:_mockAuth];
673-
674671
id mockBundle = OCMClassMock([NSBundle class]);
675672
OCMStub(ClassMethod([mockBundle mainBundle])).andReturn(mockBundle);
676673
OCMStub([mockBundle objectForInfoDictionaryKey:@"CFBundleURLTypes"]).andReturn(@[
677674
@{@"CFBundleURLSchemes" : @[ kFakeReverseClientID ]}
678675
]);
679676
OCMStub([mockBundle bundleIdentifier]).andReturn(kFakeBundleID);
680677

678+
OCMStub([_mockOptions clientID]).andReturn(kFakeClientID);
679+
_provider = [FIROAuthProvider providerWithProviderID:kFakeProviderID auth:_mockAuth];
680+
681681
OCMExpect([_mockBackend getProjectConfig:[OCMArg any] callback:[OCMArg any]])
682682
.andCallBlock2(
683683
^(FIRGetProjectConfigRequest *request, FIRGetProjectConfigResponseCallback callback) {
@@ -767,16 +767,16 @@ - (void)testGetCredentialWithUIDelegateInvalidClientID {
767767
unknown error.
768768
*/
769769
- (void)testGetCredentialWithUIDelegateUnknownErrorWithClientID {
770-
OCMStub([_mockOptions clientID]).andReturn(kFakeClientID);
771-
_provider = [FIROAuthProvider providerWithProviderID:kFakeProviderID auth:_mockAuth];
772-
773770
id mockBundle = OCMClassMock([NSBundle class]);
774771
OCMStub(ClassMethod([mockBundle mainBundle])).andReturn(mockBundle);
775772
OCMStub([mockBundle objectForInfoDictionaryKey:@"CFBundleURLTypes"]).andReturn(@[
776773
@{@"CFBundleURLSchemes" : @[ kFakeReverseClientID ]}
777774
]);
778775
OCMStub([mockBundle bundleIdentifier]).andReturn(kFakeBundleID);
779776

777+
OCMStub([_mockOptions clientID]).andReturn(kFakeClientID);
778+
_provider = [FIROAuthProvider providerWithProviderID:kFakeProviderID auth:_mockAuth];
779+
780780
OCMExpect([_mockBackend getProjectConfig:[OCMArg any] callback:[OCMArg any]])
781781
.andCallBlock2(
782782
^(FIRGetProjectConfigRequest *request, FIRGetProjectConfigResponseCallback callback) {
@@ -866,15 +866,119 @@ - (void)testGetCredentialWithUIDelegateUnknownErrorWithClientID {
866866
@brief Tests a successful invocation of @c getCredentialWithUIDelegte:completion:
867867
*/
868868
- (void)testGetCredentialWithUIDelegateWithFirebaseAppID {
869+
id mockBundle = OCMClassMock([NSBundle class]);
870+
OCMStub(ClassMethod([mockBundle mainBundle])).andReturn(mockBundle);
871+
OCMStub([mockBundle objectForInfoDictionaryKey:@"CFBundleURLTypes"]).andReturn(@[
872+
@{@"CFBundleURLSchemes" : @[ kFakeEncodedFirebaseAppID ]}
873+
]);
874+
OCMStub([mockBundle bundleIdentifier]).andReturn(kFakeBundleID);
875+
869876
_provider = [FIROAuthProvider providerWithProviderID:kFakeProviderID auth:_mockAuth];
870877

878+
OCMExpect([_mockBackend getProjectConfig:[OCMArg any] callback:[OCMArg any]])
879+
.andCallBlock2(
880+
^(FIRGetProjectConfigRequest *request, FIRGetProjectConfigResponseCallback callback) {
881+
XCTAssertNotNil(request);
882+
dispatch_async(FIRAuthGlobalWorkQueue(), ^() {
883+
id mockGetProjectConfigResponse = OCMClassMock([FIRGetProjectConfigResponse class]);
884+
OCMStub([mockGetProjectConfigResponse authorizedDomains]).andReturn(@[
885+
kFakeAuthorizedDomain
886+
]);
887+
callback(mockGetProjectConfigResponse, nil);
888+
});
889+
});
890+
891+
id mockUIDelegate = OCMProtocolMock(@protocol(FIRAuthUIDelegate));
892+
893+
// Expect view controller presentation by UIDelegate.
894+
OCMExpect([_mockURLPresenter presentURL:OCMOCK_ANY
895+
UIDelegate:mockUIDelegate
896+
callbackMatcher:OCMOCK_ANY
897+
completion:OCMOCK_ANY])
898+
.andDo(^(NSInvocation *invocation) {
899+
__unsafe_unretained id unretainedArgument;
900+
// Indices 0 and 1 indicate the hidden arguments self and _cmd.
901+
// `presentURL` is at index 2.
902+
[invocation getArgument:&unretainedArgument atIndex:2];
903+
NSURL *presentURL = unretainedArgument;
904+
XCTAssertEqualObjects(presentURL.scheme, @"https");
905+
XCTAssertEqualObjects(presentURL.host, kFakeAuthorizedDomain);
906+
XCTAssertEqualObjects(presentURL.path, @"/__/auth/handler");
907+
NSDictionary *params = [FIRAuthWebUtils dictionaryWithHttpArgumentsString:presentURL.query];
908+
XCTAssertEqualObjects(params[@"ibi"], kFakeBundleID);
909+
XCTAssertEqualObjects(params[@"appId"], kFakeFirebaseAppID);
910+
XCTAssertEqualObjects(params[@"apiKey"], kFakeAPIKey);
911+
XCTAssertEqualObjects(params[@"authType"], @"signInWithRedirect");
912+
XCTAssertNotNil(params[@"v"]);
913+
// `callbackMatcher` is at index 4
914+
[invocation getArgument:&unretainedArgument atIndex:4];
915+
FIRAuthURLCallbackMatcher callbackMatcher = unretainedArgument;
916+
NSMutableString *redirectURL = [NSMutableString
917+
stringWithString:[kFakeEncodedFirebaseAppID
918+
stringByAppendingString:kFakeRedirectURLResponseURL]];
919+
// Add fake OAuthResponse to callback.
920+
[redirectURL appendString:kFakeOAuthResponseURL];
921+
// Verify that the URL is rejected by the callback matcher without the event ID.
922+
XCTAssertFalse(callbackMatcher([NSURL URLWithString:redirectURL]));
923+
[redirectURL appendString:@"%26eventId%3D"];
924+
[redirectURL appendString:params[@"eventId"]];
925+
NSURLComponents *originalComponents = [[NSURLComponents alloc] initWithString:redirectURL];
926+
// Verify that the URL is accepted by the callback matcher with the matching event ID.
927+
XCTAssertTrue(callbackMatcher([originalComponents URL]));
928+
NSURLComponents *components = [originalComponents copy];
929+
components.query = @"https";
930+
XCTAssertFalse(callbackMatcher([components URL]));
931+
components = [originalComponents copy];
932+
components.host = @"badhost";
933+
XCTAssertFalse(callbackMatcher([components URL]));
934+
components = [originalComponents copy];
935+
components.path = @"badpath";
936+
XCTAssertFalse(callbackMatcher([components URL]));
937+
components = [originalComponents copy];
938+
components.query = @"badquery";
939+
XCTAssertFalse(callbackMatcher([components URL]));
940+
941+
// `completion` is at index 5
942+
[invocation getArgument:&unretainedArgument atIndex:5];
943+
FIRAuthURLPresentationCompletion completion = unretainedArgument;
944+
dispatch_async(FIRAuthGlobalWorkQueue(), ^() {
945+
completion(originalComponents.URL, nil);
946+
});
947+
});
948+
949+
XCTestExpectation *expectation = [self expectationWithDescription:@"callback"];
950+
[_provider
951+
getCredentialWithUIDelegate:mockUIDelegate
952+
completion:^(FIRAuthCredential *_Nullable credential,
953+
NSError *_Nullable error) {
954+
XCTAssertTrue([NSThread isMainThread]);
955+
XCTAssertNil(error);
956+
XCTAssertTrue([credential isKindOfClass:[FIROAuthCredential class]]);
957+
FIROAuthCredential *OAuthCredential = (FIROAuthCredential *)credential;
958+
XCTAssertEqualObjects(kFakeOAuthResponseURL,
959+
OAuthCredential.OAuthResponseURLString);
960+
[expectation fulfill];
961+
}];
962+
[self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];
963+
OCMVerifyAll(_mockBackend);
964+
}
965+
966+
/** @fn testGetCredentialWithUIDelegateWithFirebaseAppIDWhileClientIdPresent
967+
@brief Tests a successful invocation of @c getCredentialWithUIDelegte:completion: when the
968+
client ID is present in the plist file, but the encoded app ID is the registered custom URL
969+
scheme.
970+
*/
971+
- (void)testGetCredentialWithUIDelegateWithFirebaseAppIDWhileClientIdPresent {
871972
id mockBundle = OCMClassMock([NSBundle class]);
872973
OCMStub(ClassMethod([mockBundle mainBundle])).andReturn(mockBundle);
873974
OCMStub([mockBundle objectForInfoDictionaryKey:@"CFBundleURLTypes"]).andReturn(@[
874975
@{@"CFBundleURLSchemes" : @[ kFakeEncodedFirebaseAppID ]}
875976
]);
876977
OCMStub([mockBundle bundleIdentifier]).andReturn(kFakeBundleID);
877978

979+
OCMStub([_mockOptions clientID]).andReturn(kFakeClientID);
980+
_provider = [FIROAuthProvider providerWithProviderID:kFakeProviderID auth:_mockAuth];
981+
878982
OCMExpect([_mockBackend getProjectConfig:[OCMArg any] callback:[OCMArg any]])
879983
.andCallBlock2(
880984
^(FIRGetProjectConfigRequest *request, FIRGetProjectConfigResponseCallback callback) {
@@ -968,19 +1072,19 @@ - (void)testGetCredentialWithUIDelegateWithFirebaseAppID {
9681072
emulator.
9691073
*/
9701074
- (void)testGetCredentialWithUIDelegateUseEmulator {
971-
OCMStub([_mockOptions clientID]).andReturn(kFakeClientID);
972-
NSString *emulatorHostAndPort =
973-
[NSString stringWithFormat:@"%@:%@", kFakeEmulatorHost, kFakeEmulatorPort];
974-
OCMStub([_mockRequestConfiguration emulatorHostAndPort]).andReturn(emulatorHostAndPort);
975-
_provider = [FIROAuthProvider providerWithProviderID:kFakeProviderID auth:_mockAuth];
976-
9771075
id mockBundle = OCMClassMock([NSBundle class]);
9781076
OCMStub(ClassMethod([mockBundle mainBundle])).andReturn(mockBundle);
9791077
OCMStub([mockBundle objectForInfoDictionaryKey:@"CFBundleURLTypes"]).andReturn(@[
9801078
@{@"CFBundleURLSchemes" : @[ kFakeReverseClientID ]}
9811079
]);
9821080
OCMStub([mockBundle bundleIdentifier]).andReturn(kFakeBundleID);
9831081

1082+
OCMStub([_mockOptions clientID]).andReturn(kFakeClientID);
1083+
NSString *emulatorHostAndPort =
1084+
[NSString stringWithFormat:@"%@:%@", kFakeEmulatorHost, kFakeEmulatorPort];
1085+
OCMStub([_mockRequestConfiguration emulatorHostAndPort]).andReturn(emulatorHostAndPort);
1086+
_provider = [FIROAuthProvider providerWithProviderID:kFakeProviderID auth:_mockAuth];
1087+
9841088
id mockUIDelegate = OCMProtocolMock(@protocol(FIRAuthUIDelegate));
9851089

9861090
// Expect view controller presentation by UIDelegate.

0 commit comments

Comments
 (0)