Files
firebase-ios-sdk/FirebaseAuth/Tests/Unit/OAuthProviderTests.swift
T

476 lines
21 KiB
Swift

// 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