| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475 |
- // Copyright 2023 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 Foundation
- import XCTest
- @testable import FirebaseAuth
- import FirebaseCore
- #if os(iOS)
- @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
- class OAuthProviderTests: RPCBaseTests {
- static let kFakeAuthorizedDomain = "test.firebaseapp.com"
- static let kFakeAuthorizedWebDomain = "test.web.app"
- private let kFakeAccessToken = "fakeAccessToken"
- private let kFakeIDToken = "fakeIDToken"
- private let kFakeProviderID = "fakeProviderID"
- static let kFakeAPIKey = "asdfghjkl"
- static let kFakeEmulatorHost = "emulatorhost"
- static let kFakeEmulatorPort = 12345
- static let kFakeClientID = "123456.apps.googleusercontent.com"
- static let kFakeOAuthResponseURL = "fakeOAuthResponseURL"
- static let kFakeFirebaseAppID = "1:123456789:ios:123abc456def"
- static let kFakeEncodedFirebaseAppID = "app-1-123456789-ios-123abc456def"
- static let kFakeTenantID = "tenantID"
- static let kFakeReverseClientID = "com.googleusercontent.apps.123456"
- // Switches for testing different OAuth test flows
- // TODO: Consider using an enum for these instead.
- static var testTenantID = false
- static var testCancel = false
- static var testErrorString = false
- static var testInternalError = false
- static var testInvalidClientID = false
- static var testUnknownError = false
- static var testAppID = false
- static var testEmulator = false
- static var testAppCheck = false
- static var auth: Auth?
- /** @fn testObtainingOAuthCredentialNoIDToken
- @brief Tests the correct creation of an OAuthCredential without an IDToken.
- */
- func testObtainingOAuthCredentialNoIDToken() throws {
- initApp(#function)
- let credential = OAuthProvider.credential(providerID: .apple,
- accessToken: kFakeAccessToken)
- XCTAssertEqual(credential.accessToken, kFakeAccessToken)
- XCTAssertEqual(credential.provider, AuthProviderID.apple.rawValue)
- XCTAssertNil(credential.idToken)
- }
- /** @fn testObtainingOAuthCredentialWithFullName
- @brief Tests the correct creation of an OAuthCredential with a fullName.
- */
- func testObtainingOAuthCredentialWithFullName() throws {
- let kFakeGivenName = "Paul"
- let kFakeFamilyName = "B"
- var fullName = PersonNameComponents()
- fullName.givenName = kFakeGivenName
- fullName.familyName = kFakeFamilyName
- initApp(#function)
- let credential = OAuthProvider.appleCredential(withIDToken: kFakeIDToken, rawNonce: nil,
- fullName: fullName)
- XCTAssertEqual(credential.fullName, fullName)
- XCTAssertEqual(credential.provider, "apple.com")
- XCTAssertEqual(credential.idToken, kFakeIDToken)
- XCTAssertNil(credential.accessToken)
- }
- /** @fn testObtainingOAuthCredentialWithIDToken
- @brief Tests the correct creation of an OAuthCredential with an IDToken
- */
- func testObtainingOAuthCredentialWithIDToken() throws {
- initApp(#function)
- let credential = OAuthProvider.credential(providerID: .email,
- idToken: kFakeIDToken,
- accessToken: kFakeAccessToken)
- XCTAssertEqual(credential.accessToken, kFakeAccessToken)
- XCTAssertEqual(credential.provider, AuthProviderID.email.rawValue)
- XCTAssertEqual(credential.idToken, kFakeIDToken)
- }
- /** @fn testObtainingOAuthProvider
- @brief Tests the correct creation of an FIROAuthProvider instance.
- */
- func testObtainingOAuthProvider() throws {
- initApp(#function)
- let provider = OAuthProvider(providerID: kFakeProviderID, auth: OAuthProviderTests.auth!)
- XCTAssertEqual(provider.providerID, kFakeProviderID)
- }
- /** @fn testGetCredentialWithUIDelegateWithClientID
- @brief Tests a successful invocation of @c getCredentialWithUIDelegate
- */
- func testGetCredentialWithUIDelegateWithClientID() throws {
- initApp(#function)
- try testOAuthFlow(description: #function)
- }
- /** @fn testGetCredentialWithUIDelegateWithTenantID
- @brief Tests a successful invocation of @c getCredentialWithUIDelegate:completion:
- */
- func testGetCredentialWithUIDelegateWithTenantID() throws {
- initApp(#function)
- // Update tenantID on workqueue to enable _protectedDataDidBecomeAvailableObserver to finish
- // init.
- kAuthGlobalWorkQueue.sync {
- OAuthProviderTests.auth?.tenantID = OAuthProviderTests.kFakeTenantID
- }
- OAuthProviderTests.testTenantID = true
- try testOAuthFlow(description: #function)
- OAuthProviderTests.auth?.tenantID = nil
- OAuthProviderTests.testTenantID = false
- }
- /** @fn testGetCredentialWithUIDelegateUserCancellationWithClientID
- @brief Tests an unsuccessful invocation of @c testGetCredentialWithUIDelegateUserCancellationWithClientID due to user
- cancelation.
- */
- func testGetCredentialWithUIDelegateUserCancellationWithClientID() throws {
- initApp(#function)
- OAuthProviderTests.testCancel = true
- try testOAuthFlow(description: #function)
- OAuthProviderTests.testCancel = false
- }
- /** @fn testGetCredentialWithUIDelegateNetworkRequestFailedWithClientID
- @brief Tests an unsuccessful invocation of @c getCredentialWithUIDelegate due to a
- failed network request within the web context.
- */
- func testGetCredentialWithUIDelegateNetworkRequestFailedWithClientID() throws {
- initApp(#function)
- OAuthProviderTests.testErrorString = true
- try testOAuthFlow(description: #function)
- OAuthProviderTests.testErrorString = false
- }
- /** @fn testGetCredentialWithUIDelegateInternalErrorWithClientID
- @brief Tests an unsuccessful invocation of @c getCredentialWithUIDelegate due to an
- internal error within the web context.
- */
- func testGetCredentialWithUIDelegateInternalErrorWithClientID() throws {
- initApp(#function)
- OAuthProviderTests.testInternalError = true
- try testOAuthFlow(description: #function)
- OAuthProviderTests.testInternalError = false
- }
- /** @fn testGetCredentialWithUIDelegateInvalidClientID
- @brief Tests an unsuccessful invocation of @c getCredentialWithUIDelegate due to an
- use of an invalid client ID.
- */
- func testGetCredentialWithUIDelegateInvalidClientID() throws {
- initApp(#function)
- OAuthProviderTests.testInvalidClientID = true
- try testOAuthFlow(description: #function)
- OAuthProviderTests.testInvalidClientID = false
- }
- /** @fn testGetCredentialWithUIDelegateUnknownErrorWithClientID
- @brief Tests an unsuccessful invocation of @c getCredentialWithUIDelegate due to an
- unknown error.
- */
- func testGetCredentialWithUIDelegateUnknownErrorWithClientID() throws {
- initApp(#function)
- OAuthProviderTests.testUnknownError = true
- try testOAuthFlow(description: #function)
- OAuthProviderTests.testUnknownError = false
- }
- /** @fn testGetCredentialWithUIDelegateWithFirebaseAppID
- @brief Tests a successful invocation of @c getCredentialWithUIDelegate
- */
- func testGetCredentialWithUIDelegateWithFirebaseAppID() throws {
- initApp(#function, useAppID: true, omitClientID: true,
- scheme: OAuthProviderTests.kFakeEncodedFirebaseAppID)
- OAuthProviderTests.testAppID = true
- try testOAuthFlow(description: #function)
- OAuthProviderTests.testAppID = false
- }
- /** @fn testGetCredentialWithUIDelegateWithFirebaseAppIDWhileClientIdPresent
- @brief Tests a successful invocation of @c getCredentialWithUIDelegate when the
- client ID is present in the plist file, but the encoded app ID is the registered custom URL
- scheme.
- */
- func testGetCredentialWithUIDelegateWithFirebaseAppIDWhileClientIdPresent() throws {
- initApp(#function, useAppID: true, scheme: OAuthProviderTests.kFakeEncodedFirebaseAppID)
- OAuthProviderTests.testAppID = true
- try testOAuthFlow(description: #function)
- OAuthProviderTests.testAppID = false
- }
- /** @fn testGetCredentialWithUIDelegateUseEmulator
- @brief Tests a successful invocation of @c getCredentialWithUIDelegate when using the emulator.
- */
- func testGetCredentialWithUIDelegateUseEmulator() throws {
- initApp(#function, useAppID: true)
- OAuthProviderTests.auth?.requestConfiguration.emulatorHostAndPort =
- "\(OAuthProviderTests.kFakeEmulatorHost):\(OAuthProviderTests.kFakeEmulatorPort)"
- OAuthProviderTests.testEmulator = true
- try testOAuthFlow(description: #function)
- OAuthProviderTests.testEmulator = false
- }
- /** @fn testGetCredentialWithUIDelegateWithAppCheckToken
- @brief Tests a successful invocation of @c getCredentialWithUIDelegate
- */
- func testGetCredentialWithUIDelegateWithAppCheckToken() throws {
- let fakeAppCheck = FakeAppCheck()
- initApp(#function, useAppID: true)
- OAuthProviderTests.auth?.requestConfiguration.appCheck = fakeAppCheck
- OAuthProviderTests.testAppCheck = true
- try testOAuthFlow(description: #function)
- OAuthProviderTests.testAppCheck = false
- }
- /** @fn testOAuthCredentialCoding
- @brief Tests successful archiving and unarchiving of @c GoogleAuthCredential.
- */
- func testOAuthCredentialCoding() throws {
- let kAccessToken = "accessToken"
- let kIDToken = "idToken"
- let kRawNonce = "nonce"
- let kSecret = "sEcret"
- let kFullName = PersonNameComponents()
- let kPendingToken = "pendingToken"
- let credential = OAuthCredential(withProviderID: "dummyProvider",
- idToken: kIDToken,
- rawNonce: kRawNonce,
- accessToken: kAccessToken,
- secret: kSecret,
- fullName: kFullName,
- pendingToken: kPendingToken)
- XCTAssertTrue(OAuthCredential.supportsSecureCoding)
- let data = try NSKeyedArchiver.archivedData(
- withRootObject: credential,
- requiringSecureCoding: true
- )
- let unarchivedCredential = try XCTUnwrap(NSKeyedUnarchiver.unarchivedObject(
- ofClasses: [OAuthCredential.self, NSPersonNameComponents.self], from: data
- ) as? OAuthCredential)
- XCTAssertEqual(unarchivedCredential.idToken, kIDToken)
- XCTAssertEqual(unarchivedCredential.rawNonce, kRawNonce)
- XCTAssertEqual(unarchivedCredential.accessToken, kAccessToken)
- XCTAssertEqual(unarchivedCredential.secret, kSecret)
- XCTAssertEqual(unarchivedCredential.fullName, kFullName)
- XCTAssertEqual(unarchivedCredential.pendingToken, kPendingToken)
- XCTAssertEqual(unarchivedCredential.provider, OAuthProvider.id)
- }
- private func initApp(_ functionName: String, useAppID: Bool = false, omitClientID: Bool = false,
- scheme: String = OAuthProviderTests.kFakeReverseClientID) {
- let options = FirebaseOptions(googleAppID: "0:0000000000000:ios:0000000000000000",
- gcmSenderID: "00000000000000000-00000000000-000000000")
- options.apiKey = OAuthProviderTests.kFakeAPIKey
- options.projectID = "myProjectID"
- if useAppID {
- options.googleAppID = OAuthProviderTests.kFakeFirebaseAppID
- }
- if !omitClientID {
- options.clientID = OAuthProviderTests.kFakeClientID
- }
- let strippedName = functionName.replacingOccurrences(of: "(", with: "")
- .replacingOccurrences(of: ")", with: "")
- FirebaseApp.configure(name: strippedName, options: options)
- OAuthProviderTests.auth = Auth.auth(app: FirebaseApp.app(name: strippedName)!)
- OAuthProviderTests.auth?.mainBundleUrlTypes =
- [["CFBundleURLSchemes": [scheme]]]
- }
- private func testOAuthFlow(description: String,
- with fakeAppCheck: FakeAppCheck? = nil) throws {
- let expectation = self.expectation(description: description)
- let provider = OAuthProvider(providerID: kFakeProviderID, auth: OAuthProviderTests.auth!)
- // Use fake authURLPresenter so we can test the parameters that get sent to it.
- OAuthProviderTests.auth?.authURLPresenter = FakePresenter()
- // 1. Setup fakes and parameters for getCredential.
- if !OAuthProviderTests.testEmulator {
- let projectConfigExpectation = self.expectation(description: "projectConfiguration")
- rpcIssuer?.projectConfigRequester = { request in
- // 3. Validate the created Request instance.
- XCTAssertEqual(request.apiKey, OAuthProviderTests.kFakeAPIKey)
- XCTAssertEqual(request.endpoint, "getProjectConfig")
- // 4. Fulfill the expectation.
- projectConfigExpectation.fulfill()
- kAuthGlobalWorkQueue.async {
- do {
- // 5. Send the response from the fake backend.
- try self.rpcIssuer?
- .respond(withJSON: ["authorizedDomains": [
- OAuthProviderTests.kFakeAuthorizedWebDomain,
- OAuthProviderTests.kFakeAuthorizedDomain]])
- } catch {
- XCTFail("Failure sending response: \(error)")
- }
- }
- }
- }
- class FakePresenter: NSObject, AuthWebViewControllerDelegate {
- func webViewController(_ webViewController: AuthWebViewController,
- canHandle URL: URL) -> Bool {
- XCTFail("Do not call")
- return false
- }
- func webViewControllerDidCancel(_ webViewController: AuthWebViewController) {
- XCTFail("Do not call")
- }
- func webViewController(_ webViewController: AuthWebViewController,
- didFailWithError error: Error) {
- XCTFail("Do not call")
- }
- func present(_ presentURL: URL, uiDelegate UIDelegate: AuthUIDelegate?,
- callbackMatcher: @escaping (URL?) -> Bool,
- completion: @escaping (URL?, Error?) -> Void) {
- // 6. Verify flow triggers present in the FakePresenter class with the right parameters.
- if OAuthProviderTests.testEmulator {
- XCTAssertEqual(presentURL.scheme, "http")
- XCTAssertEqual(presentURL.host, OAuthProviderTests.kFakeEmulatorHost)
- XCTAssertEqual(presentURL.port, OAuthProviderTests.kFakeEmulatorPort)
- XCTAssertEqual(presentURL.path, "/emulator/auth/handler")
- } else {
- XCTAssertEqual(presentURL.scheme, "https")
- XCTAssertEqual(presentURL.host, OAuthProviderTests.kFakeAuthorizedDomain)
- XCTAssertEqual(presentURL.path, "/__/auth/handler")
- }
- let params = AuthWebUtils.dictionary(withHttpArgumentsString: presentURL.query)
- XCTAssertEqual(params["ibi"], Bundle.main.bundleIdentifier)
- if OAuthProviderTests.testAppID {
- XCTAssertEqual(params["appId"], OAuthProviderTests.kFakeFirebaseAppID)
- } else {
- XCTAssertEqual(params["clientId"], OAuthProviderTests.kFakeClientID)
- }
- XCTAssertEqual(params["apiKey"], OAuthProviderTests.kFakeAPIKey)
- XCTAssertEqual(params["authType"], "signInWithRedirect")
- XCTAssertNotNil(params["v"])
- if OAuthProviderTests.testTenantID {
- XCTAssertEqual(params["tid"], OAuthProviderTests.kFakeTenantID)
- } else {
- XCTAssertNil(params["tid"])
- }
- let appCheckToken = presentURL.fragment
- let verifyAppCheckToken = OAuthProviderTests.testAppCheck ? "fac=fakeAppCheckToken" : nil
- XCTAssertEqual(appCheckToken, verifyAppCheckToken)
- // 7. Test callbackMatcher
- let kFakeRedirectStart = OAuthProviderTests
- .testAppID ? "app-1-123456789-ios-123abc456def" :
- OAuthProviderTests.kFakeReverseClientID
- let kFakeRedirectURLBase = kFakeRedirectStart + "://firebaseauth/" +
- "link?deep_link_id=https%3A%2F%2Fexample.firebaseapp.com%2F__%2Fauth%2Fcallback%3F"
- var kFakeRedirectURLRest = "authType%3DsignInWithRedirect%26link%3D"
- if OAuthProviderTests.testInternalError {
- kFakeRedirectURLRest = "firebaseError%3D%257B%2522code%2522%253" +
- "A%2522auth%252Finternal-error%2522%252C%2522message%2522%253A%2522Internal%2520" +
- "error%2520.%2522%257D%26authType%3DsignInWithRedirect"
- } else if OAuthProviderTests.testInvalidClientID {
- kFakeRedirectURLRest = "firebaseError%3D%257B%2522code%2522%253A%2522auth" +
- "%252Finvalid-oauth-client-id%2522%252C%2522message%2522%253A%2522The%2520OAuth%2520client%" +
- "2520ID%2520provided%2520is%2520either%2520invalid%2520or%2520does%2520not%2520match%2520the%" +
- "2520specified%2520API%2520key.%2522%257D%26authType%3DsignInWithRedirect"
- } else if OAuthProviderTests.testErrorString {
- kFakeRedirectURLRest = "firebaseError%3D%257B%2522code%2" +
- "522%253A%2522auth%252Fnetwork-request-failed%2522%252C%2522message%2522%253A%2522The%" +
- "2520network%2520request%2520failed%2520.%2522%257D%26authType%3DsignInWithRedirect"
- } else if OAuthProviderTests.testUnknownError {
- kFakeRedirectURLRest = "firebaseError%3D%257B%2522code%2522%253A%2522auth%2" +
- "52Funknown-error-id%2522%252C%2522message%2522%253A%2522The%2520OAuth%2520client%2520ID%" +
- "2520provided%2520is%2520either%2520invalid%2520or%2520does%2520not%2520match%2520the%2520" +
- "specified%2520API%2520key.%2522%257D%26authType%3DsignInWithRedirect"
- }
- var redirectURL = "\(kFakeRedirectURLBase)\(kFakeRedirectURLRest)"
- // Add fake OAuthResponse to callback.
- if !OAuthProviderTests.testErrorString, !OAuthProviderTests.testInternalError,
- !OAuthProviderTests.testInvalidClientID, !OAuthProviderTests.testUnknownError {
- redirectURL += OAuthProviderTests.kFakeOAuthResponseURL
- }
- // Verify that the URL is rejected by the callback matcher without the event ID.
- XCTAssertFalse(callbackMatcher(URL(string: "\(redirectURL)")))
- // Verify that the URL is accepted by the callback matcher with the matching event ID.
- let redirectWithEventID =
- "\(redirectURL)%26eventId%3D\(params["eventId"] ?? "missingEventID")"
- let originalComponents = URLComponents(string: redirectWithEventID)!
- XCTAssertTrue(callbackMatcher(originalComponents.url))
- var components = originalComponents
- components.query = "https"
- XCTAssertFalse(callbackMatcher(components.url))
- components = originalComponents
- components.host = "badhost"
- XCTAssertFalse(callbackMatcher(components.url))
- components = originalComponents
- components.path = "badpath"
- XCTAssertFalse(callbackMatcher(components.url))
- components = originalComponents
- components.query = "badquery"
- XCTAssertFalse(callbackMatcher(components.url))
- // 8. Do the callback to the original call.
- kAuthGlobalWorkQueue.async {
- if OAuthProviderTests.testCancel {
- completion(nil, AuthErrorUtils.webContextCancelledError(message: nil))
- } else {
- completion(originalComponents.url, nil)
- }
- }
- }
- }
- // 2. Request the credential.
- provider.getCredentialWith(nil) { credential, error in
- // 9. After the response triggers the callback, verify the values in the callback credential
- XCTAssertTrue(Thread.isMainThread)
- if OAuthProviderTests.testCancel {
- XCTAssertNil(credential)
- XCTAssertEqual((error as? NSError)?.code, AuthErrorCode.webContextCancelled.rawValue)
- } else if OAuthProviderTests.testErrorString {
- XCTAssertNil(credential)
- XCTAssertEqual((error as? NSError)?.code, AuthErrorCode.webNetworkRequestFailed.rawValue)
- } else if OAuthProviderTests.testInternalError {
- XCTAssertNil(credential)
- XCTAssertEqual((error as? NSError)?.code, AuthErrorCode.webInternalError.rawValue)
- } else if OAuthProviderTests.testInvalidClientID {
- XCTAssertNil(credential)
- XCTAssertEqual((error as? NSError)?.code, AuthErrorCode.invalidClientID.rawValue)
- } else if OAuthProviderTests.testUnknownError {
- XCTAssertNil(credential)
- XCTAssertEqual(
- (error as? NSError)?.code,
- AuthErrorCode.webSignInUserInteractionFailure.rawValue
- )
- } else {
- XCTAssertNil(error)
- let oAuthCredential = credential as? OAuthCredential
- XCTAssertEqual(
- oAuthCredential?.OAuthResponseURLString,
- OAuthProviderTests.kFakeOAuthResponseURL
- )
- }
- expectation.fulfill()
- }
- waitForExpectations(timeout: 5)
- }
- }
- #endif
|