From 4c528b7a881a6968189218bc1f71fee6809b13b1 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Thu, 31 Oct 2024 14:51:13 -0400 Subject: [PATCH 01/12] [Auth] Remove AuthBackendRPCImplementation type --- .../Sources/Swift/Backend/AuthBackend.swift | 229 +++++++----------- .../Swift/Backend/AuthBackendRPCIssuer.swift | 71 ++++++ FirebaseAuth/Sources/Swift/User/User.swift | 2 +- .../AuthBackendRPCImplementationTests.swift | 36 +-- .../Unit/Fakes/FakeBackendRPCIssuer.swift | 4 +- FirebaseAuth/Tests/Unit/RPCBaseTests.swift | 11 +- 6 files changed, 178 insertions(+), 175 deletions(-) create mode 100644 FirebaseAuth/Sources/Swift/Backend/AuthBackendRPCIssuer.swift diff --git a/FirebaseAuth/Sources/Swift/Backend/AuthBackend.swift b/FirebaseAuth/Sources/Swift/Backend/AuthBackend.swift index 24726262840..a6535f0ee2f 100644 --- a/FirebaseAuth/Sources/Swift/Backend/AuthBackend.swift +++ b/FirebaseAuth/Sources/Swift/Backend/AuthBackend.swift @@ -22,82 +22,53 @@ import Foundation #endif @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) -protocol AuthBackendRPCIssuer { - /// Asynchronously send a HTTP request. - /// - Parameter request: The request to be made. - /// - Parameter body: Request body. - /// - Parameter contentType: Content type of the body. - /// - Parameter completionHandler: Handles HTTP response. Invoked asynchronously - /// on the auth global work queue in the future. - func asyncCallToURL(with request: T, - body: Data?, - contentType: String) async -> (Data?, Error?) -} - -@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) -class AuthBackendRPCIssuerImplementation: AuthBackendRPCIssuer { - let fetcherService: GTMSessionFetcherService - - init() { - fetcherService = GTMSessionFetcherService() - fetcherService.userAgent = AuthBackend.authUserAgent() - fetcherService.callbackQueue = kAuthGlobalWorkQueue - - // Avoid reusing the session to prevent - // https://github.com/firebase/firebase-ios-sdk/issues/1261 - fetcherService.reuseSession = false - } - - func asyncCallToURL(with request: T, - body: Data?, - contentType: String) async -> (Data?, Error?) { - let requestConfiguration = request.requestConfiguration() - let request = await AuthBackend.request(withURL: request.requestURL(), - contentType: contentType, - requestConfiguration: requestConfiguration) - let fetcher = fetcherService.fetcher(with: request) - if let _ = requestConfiguration.emulatorHostAndPort { - fetcher.allowLocalhostRequest = true - fetcher.allowedInsecureSchemes = ["http"] - } - fetcher.bodyData = body - - return await withUnsafeContinuation { continuation in - fetcher.beginFetch { data, error in - continuation.resume(returning: (data, error)) - } - } - } +protocol AuthBackendProtocol { + func call(with request: T) async throws -> T.Response } @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) -class AuthBackend { +class AuthBackend: AuthBackendProtocol { static func authUserAgent() -> String { return "FirebaseAuth.iOS/\(FirebaseVersion()) \(GTMFetcherStandardUserAgentString(nil))" } - private static var realRPCBackend = AuthBackendRPCImplementation() - private static var gBackendImplementation = realRPCBackend - - class func setTestRPCIssuer(issuer: AuthBackendRPCIssuer) { - gBackendImplementation.rpcIssuer = issuer + static func call(with request: T) async throws -> T.Response { + return try await shared.call(with: request) } - class func resetRPCIssuer() { - gBackendImplementation.rpcIssuer = realRPCBackend.rpcIssuer - } + private static let shared: AuthBackend = .init(rpcIssuer: AuthBackendRPCIssuer()) - class func implementation() -> AuthBackendImplementation { - return gBackendImplementation + private let rpcIssuer: any AuthBackendRPCIssuerProtocol + + init(rpcIssuer: any AuthBackendRPCIssuerProtocol) { + self.rpcIssuer = rpcIssuer } - class func call(with request: T) async throws -> T.Response { - return try await implementation().call(with: request) + /// Calls the RPC using HTTP request. + /// Possible error responses: + /// * See FIRAuthInternalErrorCodeRPCRequestEncodingError + /// * See FIRAuthInternalErrorCodeJSONSerializationError + /// * See FIRAuthInternalErrorCodeNetworkError + /// * See FIRAuthInternalErrorCodeUnexpectedErrorResponse + /// * See FIRAuthInternalErrorCodeUnexpectedResponse + /// * See FIRAuthInternalErrorCodeRPCResponseDecodingError + /// - Parameter request: The request. + /// - Returns: The response. + func call(with request: T) async throws -> T.Response { + let response = try await callInternal(with: request) + if let auth = request.requestConfiguration().auth, + let mfaError = Self.generateMFAError(response: response, auth: auth) { + throw mfaError + } else if let error = Self.phoneCredentialInUse(response: response) { + throw error + } else { + return response + } } - class func request(withURL url: URL, - contentType: String, - requestConfiguration: AuthRequestConfiguration) async -> URLRequest { + static func request(withURL url: URL, + contentType: String, + requestConfiguration: AuthRequestConfiguration) async -> URLRequest { // Kick off tasks for the async header values. async let heartbeatsHeaderValue = requestConfiguration.heartbeatLogger?.asyncHeaderValue() async let appCheckTokenHeaderValue = requestConfiguration.appCheck? @@ -132,94 +103,56 @@ class AuthBackend { } return request } -} -@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) -protocol AuthBackendImplementation { - func call(with request: T) async throws -> T.Response -} - -@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) -private class AuthBackendRPCImplementation: AuthBackendImplementation { - var rpcIssuer: AuthBackendRPCIssuer = AuthBackendRPCIssuerImplementation() - - /// Calls the RPC using HTTP request. - /// Possible error responses: - /// * See FIRAuthInternalErrorCodeRPCRequestEncodingError - /// * See FIRAuthInternalErrorCodeJSONSerializationError - /// * See FIRAuthInternalErrorCodeNetworkError - /// * See FIRAuthInternalErrorCodeUnexpectedErrorResponse - /// * See FIRAuthInternalErrorCodeUnexpectedResponse - /// * See FIRAuthInternalErrorCodeRPCResponseDecodingError - /// - Parameter request: The request. - /// - Returns: The response. - fileprivate func call(with request: T) async throws -> T.Response { - let response = try await callInternal(with: request) - if let auth = request.requestConfiguration().auth, - let mfaError = Self.generateMFAError(response: response, auth: auth) { - throw mfaError - } else if let error = Self.phoneCredentialInUse(response: response) { - throw error - } else { - return response - } - } - - #if os(iOS) - private class func generateMFAError(response: AuthRPCResponse, auth: Auth) -> Error? { - if let mfaResponse = response as? AuthMFAResponse, - mfaResponse.idToken == nil, - let enrollments = mfaResponse.mfaInfo { - var info: [MultiFactorInfo] = [] - for enrollment in enrollments { - // check which MFA factors are enabled. - if let _ = enrollment.phoneInfo { - info.append(PhoneMultiFactorInfo(proto: enrollment)) - } else if let _ = enrollment.totpInfo { - info.append(TOTPMultiFactorInfo(proto: enrollment)) - } else { - AuthLog.logError(code: "I-AUT000021", message: "Multifactor type is not supported") - } + private static func generateMFAError(response: AuthRPCResponse, auth: Auth) -> Error? { + #if !os(iOS) + return nil + #endif // !os(iOS) + if let mfaResponse = response as? AuthMFAResponse, + mfaResponse.idToken == nil, + let enrollments = mfaResponse.mfaInfo { + var info: [MultiFactorInfo] = [] + for enrollment in enrollments { + // check which MFA factors are enabled. + if let _ = enrollment.phoneInfo { + info.append(PhoneMultiFactorInfo(proto: enrollment)) + } else if let _ = enrollment.totpInfo { + info.append(TOTPMultiFactorInfo(proto: enrollment)) + } else { + AuthLog.logError(code: "I-AUT000021", message: "Multifactor type is not supported") } - return AuthErrorUtils.secondFactorRequiredError( - pendingCredential: mfaResponse.mfaPendingCredential, - hints: info, - auth: auth - ) - } else { - return nil } - } - #else - private class func generateMFAError(response: AuthRPCResponse, auth: Auth?) -> Error? { + return AuthErrorUtils.secondFactorRequiredError( + pendingCredential: mfaResponse.mfaPendingCredential, + hints: info, + auth: auth + ) + } else { return nil } - #endif + } - #if os(iOS) - // Check whether or not the successful response is actually the special case phone - // auth flow that returns a temporary proof and phone number. - private class func phoneCredentialInUse(response: AuthRPCResponse) -> Error? { - if let phoneAuthResponse = response as? VerifyPhoneNumberResponse, - let phoneNumber = phoneAuthResponse.phoneNumber, - phoneNumber.count > 0, - let temporaryProof = phoneAuthResponse.temporaryProof, - temporaryProof.count > 0 { - let credential = PhoneAuthCredential(withTemporaryProof: temporaryProof, - phoneNumber: phoneNumber, - providerID: PhoneAuthProvider.id) - return AuthErrorUtils.credentialAlreadyInUseError(message: nil, - credential: credential, - email: nil) - } else { - return nil - } - } - #else - private class func phoneCredentialInUse(response: AuthRPCResponse) -> Error? { + // Check whether or not the successful response is actually the special case phone + // auth flow that returns a temporary proof and phone number. + private static func phoneCredentialInUse(response: AuthRPCResponse) -> Error? { + #if !os(iOS) + return nil + #endif // !os(iOS) + if let phoneAuthResponse = response as? VerifyPhoneNumberResponse, + let phoneNumber = phoneAuthResponse.phoneNumber, + phoneNumber.count > 0, + let temporaryProof = phoneAuthResponse.temporaryProof, + temporaryProof.count > 0 { + let credential = PhoneAuthCredential(withTemporaryProof: temporaryProof, + phoneNumber: phoneNumber, + providerID: PhoneAuthProvider.id) + return AuthErrorUtils.credentialAlreadyInUseError(message: nil, + credential: credential, + email: nil) + } else { return nil } - #endif + } /// Calls the RPC using HTTP request. /// @@ -318,7 +251,7 @@ private class AuthBackendRPCImplementation: AuthBackendImplementation { if error != nil { if let errorDictionary = dictionary["error"] as? [String: AnyHashable] { if let errorMessage = errorDictionary["message"] as? String { - if let clientError = AuthBackendRPCImplementation.clientError( + if let clientError = Self.clientError( withServerErrorMessage: errorMessage, errorDictionary: errorDictionary, response: response, @@ -351,7 +284,7 @@ private class AuthBackendRPCImplementation: AuthBackendImplementation { if let verifyAssertionRequest = request as? VerifyAssertionRequest { if verifyAssertionRequest.returnIDPCredential { if let errorMessage = dictionary["errorMessage"] as? String { - if let clientError = AuthBackendRPCImplementation.clientError( + if let clientError = Self.clientError( withServerErrorMessage: errorMessage, errorDictionary: dictionary, response: response, @@ -365,10 +298,10 @@ private class AuthBackendRPCImplementation: AuthBackendImplementation { return response } - private class func clientError(withServerErrorMessage serverErrorMessage: String, - errorDictionary: [String: Any], - response: AuthRPCResponse, - error: Error?) -> Error? { + private static func clientError(withServerErrorMessage serverErrorMessage: String, + errorDictionary: [String: Any], + response: AuthRPCResponse, + error: Error?) -> Error? { let split = serverErrorMessage.split(separator: ":") let shortErrorMessage = split.first?.trimmingCharacters(in: .whitespacesAndNewlines) let serverDetailErrorMessage = String(split.count > 1 ? split[1] : "") diff --git a/FirebaseAuth/Sources/Swift/Backend/AuthBackendRPCIssuer.swift b/FirebaseAuth/Sources/Swift/Backend/AuthBackendRPCIssuer.swift new file mode 100644 index 00000000000..01001c5d712 --- /dev/null +++ b/FirebaseAuth/Sources/Swift/Backend/AuthBackendRPCIssuer.swift @@ -0,0 +1,71 @@ +// Copyright 2024 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 FirebaseCore +import FirebaseCoreExtension +import Foundation +#if COCOAPODS + import GTMSessionFetcher +#else + import GTMSessionFetcherCore +#endif + +@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) +protocol AuthBackendRPCIssuerProtocol { + /// Asynchronously send a HTTP request. + /// - Parameter request: The request to be made. + /// - Parameter body: Request body. + /// - Parameter contentType: Content type of the body. + /// - Parameter completionHandler: Handles HTTP response. Invoked asynchronously + /// on the auth global work queue in the future. + func asyncCallToURL(with request: T, + body: Data?, + contentType: String) async -> (Data?, Error?) +} + +@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) +class AuthBackendRPCIssuer: AuthBackendRPCIssuerProtocol { + let fetcherService: GTMSessionFetcherService + + init() { + fetcherService = GTMSessionFetcherService() + fetcherService.userAgent = AuthBackend.authUserAgent() + fetcherService.callbackQueue = kAuthGlobalWorkQueue + + // Avoid reusing the session to prevent + // https://github.com/firebase/firebase-ios-sdk/issues/1261 + fetcherService.reuseSession = false + } + + func asyncCallToURL(with request: T, + body: Data?, + contentType: String) async -> (Data?, Error?) { + let requestConfiguration = request.requestConfiguration() + let request = await AuthBackend.request(withURL: request.requestURL(), + contentType: contentType, + requestConfiguration: requestConfiguration) + let fetcher = fetcherService.fetcher(with: request) + if let _ = requestConfiguration.emulatorHostAndPort { + fetcher.allowLocalhostRequest = true + fetcher.allowedInsecureSchemes = ["http"] + } + fetcher.bodyData = body + + return await withUnsafeContinuation { continuation in + fetcher.beginFetch { data, error in + continuation.resume(returning: (data, error)) + } + } + } +} diff --git a/FirebaseAuth/Sources/Swift/User/User.swift b/FirebaseAuth/Sources/Swift/User/User.swift index 02cdeb8f979..c9c2875e967 100644 --- a/FirebaseAuth/Sources/Swift/User/User.swift +++ b/FirebaseAuth/Sources/Swift/User/User.swift @@ -1396,7 +1396,7 @@ extension User: NSSecureCoding {} action: AuthRecaptchaAction .signUpPassword) #else - let response = try await AuthBackend.call(with: request) + let response = try await backend.call(with: request) #endif guard let refreshToken = response.refreshToken, let idToken = response.idToken else { diff --git a/FirebaseAuth/Tests/Unit/AuthBackendRPCImplementationTests.swift b/FirebaseAuth/Tests/Unit/AuthBackendRPCImplementationTests.swift index 7180313df34..91daaedf2d5 100644 --- a/FirebaseAuth/Tests/Unit/AuthBackendRPCImplementationTests.swift +++ b/FirebaseAuth/Tests/Unit/AuthBackendRPCImplementationTests.swift @@ -41,7 +41,7 @@ class AuthBackendRPCImplementationTests: RPCBaseTests { let request = FakeRequest(withEncodingError: encodingError) do { - let _ = try await rpcImplementation.call(with: request) + let _ = try await authBackend.call(with: request) XCTFail("Expected to throw") } catch { let rpcError = error as NSError @@ -71,7 +71,7 @@ class AuthBackendRPCImplementationTests: RPCBaseTests { func testBodyDataSerializationError() async throws { let request = FakeRequest(withRequestBody: ["unencodable": self]) do { - let _ = try await rpcImplementation.call(with: request) + let _ = try await authBackend.call(with: request) XCTFail("Expected to throw") } catch { let rpcError = error as NSError @@ -99,7 +99,7 @@ class AuthBackendRPCImplementationTests: RPCBaseTests { try self.rpcIssuer.respond(withData: nil, error: responseError) } do { - let _ = try await rpcImplementation.call(with: request) + let _ = try await authBackend.call(with: request) XCTFail("Expected to throw") } catch { let rpcError = error as NSError @@ -131,7 +131,7 @@ class AuthBackendRPCImplementationTests: RPCBaseTests { try self.rpcIssuer.respond(withData: data, error: responseError) } do { - let _ = try await rpcImplementation.call(with: request) + let _ = try await authBackend.call(with: request) XCTFail("Expected to throw") } catch { let rpcError = error as NSError @@ -168,7 +168,7 @@ class AuthBackendRPCImplementationTests: RPCBaseTests { try self.rpcIssuer.respond(withData: data, error: nil) } do { - let _ = try await rpcImplementation.call(with: request) + let _ = try await authBackend.call(with: request) XCTFail("Expected to throw") } catch { let rpcError = error as NSError @@ -210,7 +210,7 @@ class AuthBackendRPCImplementationTests: RPCBaseTests { try self.rpcIssuer.respond(withData: data, error: responseError) } do { - let _ = try await rpcImplementation.call(with: request) + let _ = try await authBackend.call(with: request) XCTFail("Expected to throw") } catch { let rpcError = error as NSError @@ -252,7 +252,7 @@ class AuthBackendRPCImplementationTests: RPCBaseTests { try self.rpcIssuer.respond(withData: data, error: nil) } do { - let _ = try await rpcImplementation.call(with: request) + let _ = try await authBackend.call(with: request) XCTFail("Expected to throw") } catch { let rpcError = error as NSError @@ -287,7 +287,7 @@ class AuthBackendRPCImplementationTests: RPCBaseTests { error: responseError) } do { - let _ = try await rpcImplementation.call(with: request) + let _ = try await authBackend.call(with: request) XCTFail("Expected to throw") } catch { let rpcError = error as NSError @@ -329,7 +329,7 @@ class AuthBackendRPCImplementationTests: RPCBaseTests { ) } do { - let _ = try await rpcImplementation.call(with: request) + let _ = try await authBackend.call(with: request) XCTFail("Expected to throw") } catch { let rpcError = error as NSError @@ -356,7 +356,7 @@ class AuthBackendRPCImplementationTests: RPCBaseTests { error: responseError) } do { - let _ = try await rpcImplementation.call(with: request) + let _ = try await authBackend.call(with: request) XCTFail("Expected to throw") } catch { let rpcError = error as NSError @@ -398,7 +398,7 @@ class AuthBackendRPCImplementationTests: RPCBaseTests { error: responseError) } do { - let _ = try await rpcImplementation.call(with: request) + let _ = try await authBackend.call(with: request) XCTFail("Expected to throw") } catch { let rpcError = error as NSError @@ -436,7 +436,7 @@ class AuthBackendRPCImplementationTests: RPCBaseTests { let _ = try self.rpcIssuer.respond(withJSON: [:], error: responseError) } do { - let _ = try await rpcImplementation.call(with: request) + let _ = try await authBackend.call(with: request) XCTFail("Expected to throw") } catch { let rpcError = error as NSError @@ -473,7 +473,7 @@ class AuthBackendRPCImplementationTests: RPCBaseTests { try self.rpcIssuer.respond(serverErrorMessage: customErrorMessage, error: responseError) } do { - let _ = try await rpcImplementation.call(with: FakeRequest(withRequestBody: [:])) + let _ = try await authBackend.call(with: FakeRequest(withRequestBody: [:])) XCTFail("Expected to throw") } catch { let rpcError = error as NSError @@ -497,7 +497,7 @@ class AuthBackendRPCImplementationTests: RPCBaseTests { } do { let request = FakeDecodingErrorRequest(withRequestBody: [:]) - let _ = try await rpcImplementation.call(with: request) + let _ = try await authBackend.call(with: request) XCTFail("Expected to throw") } catch { let rpcError = error as NSError @@ -528,7 +528,7 @@ class AuthBackendRPCImplementationTests: RPCBaseTests { // value it was given. try self.rpcIssuer.respond(withJSON: [kTestKey: kTestValue]) } - let rpcResponse = try await rpcImplementation.call(with: FakeRequest(withRequestBody: [:])) + let rpcResponse = try await authBackend.call(with: FakeRequest(withRequestBody: [:])) XCTAssertEqual(try XCTUnwrap(rpcResponse.receivedDictionary[kTestKey] as? String), kTestValue) } @@ -593,7 +593,7 @@ class AuthBackendRPCImplementationTests: RPCBaseTests { // Force return from async post try self.rpcIssuer.respond(withJSON: [:]) } - _ = try? await rpcImplementation.call(with: request) + _ = try? await authBackend.call(with: request) // Then let expectedHeader = HeartbeatLoggingTestUtils.nonEmptyHeartbeatsPayload.headerValue() @@ -620,7 +620,7 @@ class AuthBackendRPCImplementationTests: RPCBaseTests { // Just force return from async call. try self.rpcIssuer.respond(withJSON: [:]) } - _ = try? await rpcImplementation.call(with: request) + _ = try? await authBackend.call(with: request) let completeRequest = await rpcIssuer.completeRequest.value let headerValue = completeRequest.value(forHTTPHeaderField: "X-Firebase-AppCheck") @@ -650,7 +650,7 @@ class AuthBackendRPCImplementationTests: RPCBaseTests { // Force return from async post try self.rpcIssuer.respond(withJSON: [:]) } - _ = try? await rpcImplementation.call(with: request) + _ = try? await authBackend.call(with: request) // Then let completeRequest = await rpcIssuer.completeRequest.value diff --git a/FirebaseAuth/Tests/Unit/Fakes/FakeBackendRPCIssuer.swift b/FirebaseAuth/Tests/Unit/Fakes/FakeBackendRPCIssuer.swift index f1b4cee7a5d..e09e8d2106c 100644 --- a/FirebaseAuth/Tests/Unit/Fakes/FakeBackendRPCIssuer.swift +++ b/FirebaseAuth/Tests/Unit/Fakes/FakeBackendRPCIssuer.swift @@ -77,8 +77,8 @@ class FakeBackendRPCIssuer: AuthBackendRPCIssuer { var secureTokenErrorString: String? var recaptchaSiteKey = "unset recaptcha siteKey" - func asyncCallToURL(with request: T, body: Data?, - contentType: String) async -> (Data?, Error?) + override func asyncCallToURL(with request: T, body: Data?, + contentType: String) async -> (Data?, Error?) where T: FirebaseAuth.AuthRPCRequest { return await withCheckedContinuation { continuation in self.asyncCallToURL(with: request, body: body, contentType: contentType) { data, error in diff --git a/FirebaseAuth/Tests/Unit/RPCBaseTests.swift b/FirebaseAuth/Tests/Unit/RPCBaseTests.swift index 567b3aa4bbd..9a8f9026ea9 100644 --- a/FirebaseAuth/Tests/Unit/RPCBaseTests.swift +++ b/FirebaseAuth/Tests/Unit/RPCBaseTests.swift @@ -68,17 +68,16 @@ class RPCBaseTests: XCTestCase { let kTestIdentifier = "Identifier" var rpcIssuer: FakeBackendRPCIssuer! - var rpcImplementation: AuthBackendImplementation! + var authBackend: AuthBackend! override func setUp() { rpcIssuer = FakeBackendRPCIssuer() - AuthBackend.setTestRPCIssuer(issuer: rpcIssuer) - rpcImplementation = AuthBackend.implementation() + authBackend = AuthBackend(rpcIssuer: rpcIssuer) } override func tearDown() { rpcIssuer = nil - AuthBackend.resetRPCIssuer() + authBackend = nil } /** @fn checkRequest @@ -101,7 +100,7 @@ class RPCBaseTests: XCTestCase { // Dummy response to unblock await. let _ = try self.rpcIssuer?.respond(withJSON: [:]) } - let _ = try await AuthBackend.call(with: request) + let _ = try await authBackend.call(with: request) } /** @fn checkBackendError @@ -125,7 +124,7 @@ class RPCBaseTests: XCTestCase { } } do { - let _ = try await AuthBackend.call(with: request) + let _ = try await authBackend.call(with: request) XCTFail("Did not throw expected error") return } catch { From bb01fbea65df5ce61864c13178f2daf38fdf4f9e Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Thu, 31 Oct 2024 21:01:53 -0400 Subject: [PATCH 02/12] Dependency injection work --- FirebaseAuth/Sources/Swift/Auth/Auth.swift | 67 ++++++++++-------- .../Swift/AuthProvider/OAuthProvider.swift | 2 +- .../AuthProvider/PhoneAuthProvider.swift | 12 ++-- .../Sources/Swift/Backend/AuthBackend.swift | 10 +-- .../Swift/MultiFactor/MultiFactor.swift | 6 +- .../MultiFactor/MultiFactorResolver.swift | 2 +- .../TOTP/TOTPMultiFactorGenerator.swift | 7 +- .../SystemService/SecureTokenService.swift | 22 ++++-- FirebaseAuth/Sources/Swift/User/User.swift | 69 +++++++++++-------- .../Swift/User/UserProfileUpdate.swift | 22 +++--- .../Utilities/AuthRecaptchaVerifier.swift | 8 +-- .../Swift/Utilities/AuthWebUtils.swift | 5 +- FirebaseAuth/Tests/Unit/AuthTests.swift | 3 +- .../Tests/Unit/CreateAuthURITests.swift | 4 +- .../Tests/Unit/DeleteAccountTests.swift | 2 +- .../Tests/Unit/EmailLinkSignInTests.swift | 6 +- .../Tests/Unit/GetAccountInfoTests.swift | 2 +- .../Unit/GetOOBConfirmationCodeTests.swift | 4 +- .../Tests/Unit/GetProjectConfigTests.swift | 2 +- .../Tests/Unit/GetRecaptchaConfigTests.swift | 4 +- .../Tests/Unit/OAuthProviderTests.swift | 1 + .../Tests/Unit/ResetPasswordTests.swift | 2 +- .../Tests/Unit/RevokeTokenTests.swift | 2 +- .../Unit/SendVerificationCodeTests.swift | 2 +- .../Tests/Unit/SetAccountInfoTests.swift | 2 +- .../Unit/SignInWithGameCenterTests.swift | 2 +- .../Tests/Unit/SignUpNewUserTests.swift | 2 +- .../Tests/Unit/VerifyAssertionTests.swift | 4 +- .../Tests/Unit/VerifyClientTests.swift | 2 +- .../Tests/Unit/VerifyCustomTokenTests.swift | 2 +- .../Tests/Unit/VerifyPasswordTests.swift | 2 +- .../Tests/Unit/VerifyPhoneNumberTests.swift | 4 +- 32 files changed, 158 insertions(+), 128 deletions(-) diff --git a/FirebaseAuth/Sources/Swift/Auth/Auth.swift b/FirebaseAuth/Sources/Swift/Auth/Auth.swift index 8effd15c101..9dbd31e65a6 100644 --- a/FirebaseAuth/Sources/Swift/Auth/Auth.swift +++ b/FirebaseAuth/Sources/Swift/Auth/Auth.swift @@ -123,11 +123,12 @@ extension Auth: AuthInterop { return } // Call back with current user token. - currentUser.internalGetToken(forceRefresh: forceRefresh) { token, error in - DispatchQueue.main.async { - callback(token, error) + currentUser + .internalGetToken(forceRefresh: forceRefresh, backend: strongSelf.backend) { token, error in + DispatchQueue.main.async { + callback(token, error) + } } - } } } @@ -294,7 +295,7 @@ extension Auth: AuthInterop { requestConfiguration: self.requestConfiguration) Task { do { - let response = try await AuthBackend.call(with: request) + let response = try await self.backend.call(with: request) Auth.wrapMainAsync(callback: completion, withParam: response.signinMethods, error: nil) } catch { Auth.wrapMainAsync(callback: completion, withParam: nil, error: error) @@ -397,7 +398,7 @@ extension Auth: AuthInterop { let response = try await injectRecaptcha(request: request, action: AuthRecaptchaAction.signInWithPassword) #else - let response = try await AuthBackend.call(with: request) + let response = try await backend.call(with: request) #endif return try await completeSignIn( withAccessToken: response.idToken, @@ -711,7 +712,7 @@ extension Auth: AuthInterop { let request = SignUpNewUserRequest(requestConfiguration: self.requestConfiguration) Task { do { - let response = try await AuthBackend.call(with: request) + let response = try await self.backend.call(with: request) let user = try await self.completeSignIn( withAccessToken: response.idToken, accessTokenExpirationDate: response.approximateExpirationDate, @@ -773,7 +774,7 @@ extension Auth: AuthInterop { requestConfiguration: self.requestConfiguration) Task { do { - let response = try await AuthBackend.call(with: request) + let response = try await self.backend.call(with: request) let user = try await self.completeSignIn( withAccessToken: response.idToken, accessTokenExpirationDate: response.approximateExpirationDate, @@ -883,7 +884,7 @@ extension Auth: AuthInterop { if let inResponse { response = inResponse } else { - response = try await AuthBackend.call(with: request) + response = try await self.backend.call(with: request) } let user = try await self.completeSignIn( withAccessToken: response.idToken, @@ -995,7 +996,7 @@ extension Auth: AuthInterop { requestConfiguration: self.requestConfiguration) Task { do { - let response = try await AuthBackend.call(with: request) + let response = try await self.backend.call(with: request) let operation = ActionCodeInfo.actionCodeOperation(forRequestType: response.requestType) guard let email = response.email else { @@ -1434,7 +1435,7 @@ extension Auth: AuthInterop { /// complete, or fails. Invoked asynchronously on the main thread in the future. @objc open func revokeToken(withAuthorizationCode authorizationCode: String, completion: ((Error?) -> Void)? = nil) { - currentUser?.internalGetToken { idToken, error in + currentUser?.internalGetToken(backend: backend) { idToken, error in if let error { Auth.wrapMainAsync(completion, error) return @@ -1615,7 +1616,9 @@ extension Auth: AuthInterop { // MARK: Internal methods - init(app: FirebaseApp, keychainStorageProvider: AuthKeychainStorage = AuthKeychainStorageReal()) { + init(app: FirebaseApp, + keychainStorageProvider: AuthKeychainStorage = AuthKeychainStorageReal(), + backend: AuthBackend = AuthBackend(rpcIssuer: AuthBackendRPCIssuer())) { Auth.setKeychainServiceNameForApp(app) self.app = app mainBundleUrlTypes = Bundle.main @@ -1640,6 +1643,7 @@ extension Auth: AuthInterop { auth: nil, heartbeatLogger: app.heartbeatLogger, appCheck: appCheck) + self.backend = backend super.init() requestConfiguration.auth = self @@ -1913,17 +1917,18 @@ extension Auth: AuthInterop { return } let uid = strongSelf.currentUser?.uid - strongSelf.currentUser?.internalGetToken(forceRefresh: true) { token, error in - if strongSelf.currentUser?.uid != uid { - return - } - if error != nil { - // Kicks off exponential back off logic to retry failed attempt. Starts with one minute - // delay (60 seconds) if this is the first failed attempt. - let rescheduleDelay = retry ? min(delay * 2, 16 * 60) : 60 - strongSelf.scheduleAutoTokenRefresh(withDelay: rescheduleDelay, retry: true) + strongSelf.currentUser? + .internalGetToken(forceRefresh: true, backend: strongSelf.backend) { token, error in + if strongSelf.currentUser?.uid != uid { + return + } + if error != nil { + // Kicks off exponential back off logic to retry failed attempt. Starts with one minute + // delay (60 seconds) if this is the first failed attempt. + let rescheduleDelay = retry ? min(delay * 2, 16 * 60) : 60 + strongSelf.scheduleAutoTokenRefresh(withDelay: rescheduleDelay, retry: true) + } } - } } } @@ -2077,7 +2082,7 @@ extension Auth: AuthInterop { requestConfiguration: requestConfiguration) request.autoCreate = !isReauthentication credential.prepare(request) - let response = try await AuthBackend.call(with: request) + let response = try await backend.call(with: request) if response.needConfirmation { let email = response.email let credential = OAuthCredential(withVerifyAssertionResponse: response) @@ -2116,7 +2121,7 @@ extension Auth: AuthInterop { phoneNumber: phoneNumber, operation: operation, requestConfiguration: requestConfiguration) - return try await AuthBackend.call(with: request) + return try await backend.call(with: request) case let .verification(verificationID, code): guard verificationID.count > 0 else { throw AuthErrorUtils.missingVerificationIDError(message: nil) @@ -2128,7 +2133,7 @@ extension Auth: AuthInterop { verificationCode: code, operation: operation, requestConfiguration: requestConfiguration) - return try await AuthBackend.call(with: request) + return try await backend.call(with: request) } } #endif @@ -2154,7 +2159,7 @@ extension Auth: AuthInterop { timestamp: credential.timestamp, displayName: credential.displayName, requestConfiguration: requestConfiguration) - let response = try await AuthBackend.call(with: request) + let response = try await backend.call(with: request) let user = try await completeSignIn(withAccessToken: response.idToken, accessTokenExpirationDate: response .approximateExpirationDate, @@ -2186,7 +2191,7 @@ extension Auth: AuthInterop { let request = EmailLinkSignInRequest(email: email, oobCode: actionCode, requestConfiguration: requestConfiguration) - let response = try await AuthBackend.call(with: request) + let response = try await backend.call(with: request) let user = try await completeSignIn(withAccessToken: response.idToken, accessTokenExpirationDate: response .approximateExpirationDate, @@ -2244,7 +2249,7 @@ extension Auth: AuthInterop { private func wrapAsyncRPCTask(_ request: any AuthRPCRequest, _ callback: ((Error?) -> Void)?) { Task { do { - let _ = try await AuthBackend.call(with: request) + let _ = try await self.backend.call(with: request) Auth.wrapMainAsync(callback, nil) } catch { Auth.wrapMainAsync(callback, error) @@ -2296,7 +2301,7 @@ extension Auth: AuthInterop { action: action) } else { do { - return try await AuthBackend.call(with: request) + return try await backend.call(with: request) } catch { let nsError = error as NSError if let underlyingError = nsError.userInfo[NSUnderlyingErrorKey] as? NSError, @@ -2315,7 +2320,7 @@ extension Auth: AuthInterop { } } } - return try await AuthBackend.call(with: request) + return try await backend.call(with: request) } #endif @@ -2332,6 +2337,8 @@ extension Auth: AuthInterop { /// Auth's backend. var requestConfiguration: AuthRequestConfiguration + var backend: AuthBackend + #if os(iOS) /// The manager for APNs tokens used by phone number auth. diff --git a/FirebaseAuth/Sources/Swift/AuthProvider/OAuthProvider.swift b/FirebaseAuth/Sources/Swift/AuthProvider/OAuthProvider.swift index c14fed69031..860a5ebc455 100644 --- a/FirebaseAuth/Sources/Swift/AuthProvider/OAuthProvider.swift +++ b/FirebaseAuth/Sources/Swift/AuthProvider/OAuthProvider.swift @@ -412,7 +412,7 @@ import Foundation private func getHeadfulLiteUrl(eventID: String, sessionID: String) async throws -> URL? { let authDomain = try await AuthWebUtils - .fetchAuthDomain(withRequestConfiguration: auth.requestConfiguration) + .fetchAuthDomain(withRequestConfiguration: auth.requestConfiguration, backend: auth.backend) let bundleID = Bundle.main.bundleIdentifier let clientID = auth.app?.options.clientID let appID = auth.app?.options.googleAppID diff --git a/FirebaseAuth/Sources/Swift/AuthProvider/PhoneAuthProvider.swift b/FirebaseAuth/Sources/Swift/AuthProvider/PhoneAuthProvider.swift index 32d15499f74..2a1de385aa4 100644 --- a/FirebaseAuth/Sources/Swift/AuthProvider/PhoneAuthProvider.swift +++ b/FirebaseAuth/Sources/Swift/AuthProvider/PhoneAuthProvider.swift @@ -217,7 +217,7 @@ import Foundation .requestConfiguration) do { - let response = try await AuthBackend.call(with: request) + let response = try await auth.backend.call(with: request) return response.verificationID } catch { return try await handleVerifyErrorWithRetry(error: error, @@ -245,7 +245,7 @@ import Foundation requestConfiguration: auth.requestConfiguration ) - let response = try await AuthBackend.call(with: request) + let response = try await auth.backend.call(with: request) return response.verificationID } guard let session else { @@ -263,7 +263,7 @@ import Foundation let request = StartMFAEnrollmentRequest(idToken: idToken, enrollmentInfo: startMFARequestInfo, requestConfiguration: auth.requestConfiguration) - let response = try await AuthBackend.call(with: request) + let response = try await auth.backend.call(with: request) return response.phoneSessionInfo?.sessionInfo } else { let request = StartMFASignInRequest(MFAPendingCredential: session.mfaPendingCredential, @@ -271,7 +271,7 @@ import Foundation signInInfo: startMFARequestInfo, requestConfiguration: auth.requestConfiguration) - let response = try await AuthBackend.call(with: request) + let response = try await auth.backend.call(with: request) return response.responseInfo?.sessionInfo } } catch { @@ -328,7 +328,7 @@ import Foundation isSandbox: token.type == AuthAPNSTokenType.sandbox, requestConfiguration: auth.requestConfiguration) do { - let verifyResponse = try await AuthBackend.call(with: request) + let verifyResponse = try await auth.backend.call(with: request) guard let receipt = verifyResponse.receipt, let timeout = verifyResponse.suggestedTimeOutDate?.timeIntervalSinceNow else { fatalError("Internal Auth Error: invalid VerifyClientResponse") @@ -436,7 +436,7 @@ import Foundation /// - Parameter eventID: The event ID used for this purpose. private func reCAPTCHAURL(withEventID eventID: String) async throws -> URL? { let authDomain = try await AuthWebUtils - .fetchAuthDomain(withRequestConfiguration: auth.requestConfiguration) + .fetchAuthDomain(withRequestConfiguration: auth.requestConfiguration, backend: auth.backend) let bundleID = Bundle.main.bundleIdentifier let clientID = auth.app?.options.clientID let appID = auth.app?.options.googleAppID diff --git a/FirebaseAuth/Sources/Swift/Backend/AuthBackend.swift b/FirebaseAuth/Sources/Swift/Backend/AuthBackend.swift index a6535f0ee2f..669f585ae81 100644 --- a/FirebaseAuth/Sources/Swift/Backend/AuthBackend.swift +++ b/FirebaseAuth/Sources/Swift/Backend/AuthBackend.swift @@ -32,11 +32,11 @@ class AuthBackend: AuthBackendProtocol { return "FirebaseAuth.iOS/\(FirebaseVersion()) \(GTMFetcherStandardUserAgentString(nil))" } - static func call(with request: T) async throws -> T.Response { - return try await shared.call(with: request) - } - - private static let shared: AuthBackend = .init(rpcIssuer: AuthBackendRPCIssuer()) +// static func call(with request: T) async throws -> T.Response { +// return try await shared.call(with: request) +// } +// +// private static let shared: AuthBackend = .init(rpcIssuer: AuthBackendRPCIssuer()) private let rpcIssuer: any AuthBackendRPCIssuerProtocol diff --git a/FirebaseAuth/Sources/Swift/MultiFactor/MultiFactor.swift b/FirebaseAuth/Sources/Swift/MultiFactor/MultiFactor.swift index 8920da00ad1..e2f4bdcc747 100644 --- a/FirebaseAuth/Sources/Swift/MultiFactor/MultiFactor.swift +++ b/FirebaseAuth/Sources/Swift/MultiFactor/MultiFactor.swift @@ -88,7 +88,7 @@ import Foundation .requestConfiguration) Task { do { - let response = try await AuthBackend.call(with: request) + let response = try await auth.backend.call(with: request) do { let user = try await auth.completeSignIn(withAccessToken: response.idToken, accessTokenExpirationDate: nil, @@ -139,7 +139,7 @@ import Foundation Task { do { - let response = try await AuthBackend.call(with: request) + let response = try await auth.backend.call(with: request) do { let user = try await auth.completeSignIn(withAccessToken: response.idToken, accessTokenExpirationDate: nil, @@ -215,7 +215,7 @@ import Foundation requestConfiguration: user.requestConfiguration) Task { do { - let response = try await AuthBackend.call(with: request) + let response = try await auth.backend.call(with: request) do { let user = try await auth.completeSignIn(withAccessToken: response.idToken, accessTokenExpirationDate: nil, diff --git a/FirebaseAuth/Sources/Swift/MultiFactor/MultiFactorResolver.swift b/FirebaseAuth/Sources/Swift/MultiFactor/MultiFactorResolver.swift index a9936928fec..7175519e489 100644 --- a/FirebaseAuth/Sources/Swift/MultiFactor/MultiFactorResolver.swift +++ b/FirebaseAuth/Sources/Swift/MultiFactor/MultiFactorResolver.swift @@ -72,7 +72,7 @@ import Foundation ) Task { do { - let response = try await AuthBackend.call(with: request) + let response = try await self.auth.backend.call(with: request) let user = try await self.auth.completeSignIn(withAccessToken: response.idToken, accessTokenExpirationDate: nil, refreshToken: response.refreshToken, diff --git a/FirebaseAuth/Sources/Swift/MultiFactor/TOTP/TOTPMultiFactorGenerator.swift b/FirebaseAuth/Sources/Swift/MultiFactor/TOTP/TOTPMultiFactorGenerator.swift index 4fc574caedb..bf3b07634ca 100644 --- a/FirebaseAuth/Sources/Swift/MultiFactor/TOTP/TOTPMultiFactorGenerator.swift +++ b/FirebaseAuth/Sources/Swift/MultiFactor/TOTP/TOTPMultiFactorGenerator.swift @@ -31,8 +31,7 @@ import Foundation @objc(generateSecretWithMultiFactorSession:completion:) open class func generateSecret(with session: MultiFactorSession, completion: @escaping (TOTPSecret?, Error?) -> Void) { - guard let currentUser = session.currentUser, - let requestConfiguration = currentUser.auth?.requestConfiguration else { + guard let currentUser = session.currentUser, let auth = currentUser.auth else { let error = AuthErrorUtils.error(code: AuthErrorCode.internalError, userInfo: [NSLocalizedDescriptionKey: "Invalid ID token."]) @@ -42,10 +41,10 @@ import Foundation let totpEnrollmentInfo = AuthProtoStartMFATOTPEnrollmentRequestInfo() let request = StartMFAEnrollmentRequest(idToken: session.idToken, totpEnrollmentInfo: totpEnrollmentInfo, - requestConfiguration: requestConfiguration) + requestConfiguration: auth.requestConfiguration) Task { do { - let response = try await AuthBackend.call(with: request) + let response = try await auth.backend.call(with: request) if let totpSessionInfo = response.totpSessionInfo { let secret = TOTPSecret(secretKey: totpSessionInfo.sharedSecretKey, hashingAlgorithm: totpSessionInfo.hashingAlgorithm, diff --git a/FirebaseAuth/Sources/Swift/SystemService/SecureTokenService.swift b/FirebaseAuth/Sources/Swift/SystemService/SecureTokenService.swift index dd9e6dbde13..6bdee4c395a 100644 --- a/FirebaseAuth/Sources/Swift/SystemService/SecureTokenService.swift +++ b/FirebaseAuth/Sources/Swift/SystemService/SecureTokenService.swift @@ -25,12 +25,13 @@ actor SecureTokenServiceInternal { /// - Parameter forceRefresh: Forces the token to be refreshed. /// - Returns : A tuple with the token and flag of whether it was updated. func fetchAccessToken(forcingRefresh forceRefresh: Bool, - service: SecureTokenService) async throws -> (String?, Bool) { + service: SecureTokenService, + backend: AuthBackend) async throws -> (String?, Bool) { if !forceRefresh, hasValidAccessToken(service: service) { return (service.accessToken, false) } else { AuthLog.logDebug(code: "I-AUT000017", message: "Fetching new token from backend.") - return try await requestAccessToken(retryIfExpired: true, service: service) + return try await requestAccessToken(retryIfExpired: true, service: service, backend: backend) } } @@ -41,7 +42,8 @@ actor SecureTokenServiceInternal { /// /// - Returns: Token and Bool indicating if update occurred. private func requestAccessToken(retryIfExpired: Bool, - service: SecureTokenService) async throws -> (String?, Bool) { + service: SecureTokenService, + backend: AuthBackend) async throws -> (String?, Bool) { // TODO: This was a crash in ObjC SDK, should it callback with an error? guard let refreshToken = service.refreshToken, let requestConfiguration = service.requestConfiguration else { @@ -50,7 +52,7 @@ actor SecureTokenServiceInternal { let request = SecureTokenRequest.refreshRequest(refreshToken: refreshToken, requestConfiguration: requestConfiguration) - let response = try await AuthBackend.call(with: request) + let response = try await backend.call(with: request) var tokenUpdated = false if let newAccessToken = response.accessToken, newAccessToken.count > 0, @@ -67,7 +69,11 @@ actor SecureTokenServiceInternal { if expirationDate.timeIntervalSinceNow <= kFiveMinutes { // We only retry once, to avoid an infinite loop in the case that an end-user has // their local time skewed by over an hour. - return try await requestAccessToken(retryIfExpired: false, service: service) + return try await requestAccessToken( + retryIfExpired: false, + service: service, + backend: backend + ) } } } @@ -152,8 +158,10 @@ class SecureTokenService: NSObject, NSSecureCoding { /// Invoked asynchronously on the auth global work queue in the future. /// - Parameter forceRefresh: Forces the token to be refreshed. /// - Returns : A tuple with the token and flag of whether it was updated. - func fetchAccessToken(forcingRefresh forceRefresh: Bool) async throws -> (String?, Bool) { - return try await internalService.fetchAccessToken(forcingRefresh: forceRefresh, service: self) + func fetchAccessToken(forcingRefresh forceRefresh: Bool, + backend: AuthBackend) async throws -> (String?, Bool) { + return try await internalService + .fetchAccessToken(forcingRefresh: forceRefresh, service: self, backend: backend) } // MARK: NSSecureCoding diff --git a/FirebaseAuth/Sources/Swift/User/User.swift b/FirebaseAuth/Sources/Swift/User/User.swift index c9c2875e967..8b102113bb5 100644 --- a/FirebaseAuth/Sources/Swift/User/User.swift +++ b/FirebaseAuth/Sources/Swift/User/User.swift @@ -49,6 +49,9 @@ extension User: NSSecureCoding {} var providerDataRaw: [String: UserInfoImpl] + /// The backend service for the given instance. + private(set) var backend: AuthBackend + /// Metadata associated with the Firebase user in question. @objc public private(set) var metadata: UserMetadata @@ -583,7 +586,7 @@ extension User: NSSecureCoding {} open func getIDTokenResult(forcingRefresh: Bool, completion: ((AuthTokenResult?, Error?) -> Void)?) { kAuthGlobalWorkQueue.async { - self.internalGetToken(forceRefresh: forcingRefresh) { token, error in + self.internalGetToken(forceRefresh: forcingRefresh, backend: self.backend) { token, error in var tokenResult: AuthTokenResult? if let token { do { @@ -868,7 +871,7 @@ extension User: NSSecureCoding {} open func sendEmailVerification(with actionCodeSettings: ActionCodeSettings? = nil, completion: ((Error?) -> Void)? = nil) { kAuthGlobalWorkQueue.async { - self.internalGetToken { accessToken, error in + self.internalGetToken(backend: self.backend) { accessToken, error in if let error { User.callInMainThreadWithError(callback: completion, error: error) return @@ -886,7 +889,7 @@ extension User: NSSecureCoding {} ) Task { do { - let _ = try await AuthBackend.call(with: request) + let _ = try await self.backend.call(with: request) User.callInMainThreadWithError(callback: completion, error: nil) } catch { self.signOutIfTokenIsInvalid(withError: error) @@ -933,7 +936,7 @@ extension User: NSSecureCoding {} /// is complete, or fails. Invoked asynchronously on the main thread in the future. @objc open func delete(completion: ((Error?) -> Void)? = nil) { kAuthGlobalWorkQueue.async { - self.internalGetToken { accessToken, error in + self.internalGetToken(backend: self.backend) { accessToken, error in if let error { User.callInMainThreadWithError(callback: completion, error: error) return @@ -948,7 +951,7 @@ extension User: NSSecureCoding {} requestConfiguration: requestConfiguration) Task { do { - let _ = try await AuthBackend.call(with: request) + let _ = try await self.backend.call(with: request) try self.auth?.signOutByForce(withUserID: self.uid) User.callInMainThreadWithError(callback: completion, error: nil) } catch { @@ -998,7 +1001,7 @@ extension User: NSSecureCoding {} actionCodeSettings: ActionCodeSettings? = nil, completion: ((Error?) -> Void)? = nil) { kAuthGlobalWorkQueue.async { - self.internalGetToken { accessToken, error in + self.internalGetToken(backend: self.backend) { accessToken, error in if let error { User.callInMainThreadWithError(callback: completion, error: error) return @@ -1017,7 +1020,7 @@ extension User: NSSecureCoding {} ) Task { do { - let _ = try await AuthBackend.call(with: request) + let _ = try await self.backend.call(with: request) User.callInMainThreadWithError(callback: completion, error: nil) } catch { User.callInMainThreadWithError(callback: completion, error: error) @@ -1056,7 +1059,8 @@ extension User: NSSecureCoding {} return tokenService.accessTokenExpirationDate } - init(withTokenService tokenService: SecureTokenService) { + init(withTokenService tokenService: SecureTokenService, backend: AuthBackend) { + self.backend = backend providerDataRaw = [:] userProfileUpdate = UserProfileUpdate() self.tokenService = tokenService @@ -1085,16 +1089,16 @@ extension User: NSSecureCoding {} accessToken: accessToken, accessTokenExpirationDate: accessTokenExpirationDate, refreshToken: refreshToken) - let user = User(withTokenService: tokenService) + let user = User(withTokenService: tokenService, backend: auth.backend) user.auth = auth user.tenantID = auth.tenantID user.requestConfiguration = auth.requestConfiguration - let accessToken2 = try await user.internalGetTokenAsync() + let accessToken2 = try await user.internalGetTokenAsync(backend: user.backend) let getAccountInfoRequest = GetAccountInfoRequest( accessToken: accessToken2, requestConfiguration: user.requestConfiguration ) - let response = try await AuthBackend.call(with: getAccountInfoRequest) + let response = try await auth.backend.call(with: getAccountInfoRequest) user.isAnonymous = anonymous user.update(withGetAccountInfoResponse: response) return user @@ -1139,12 +1143,13 @@ extension User: NSSecureCoding {} /// A weak reference to an `Auth` instance associated with this instance. weak var auth: Auth? { set { - _auth = newValue - guard let requestConfiguration = auth?.requestConfiguration else { - fatalError("Firebase Auth Internal Error: nil requestConfiguration when initializing User") + guard let newValue else { + fatalError("Firebase Auth Internal Error: Set user's auth property with non-nil instance.") } + _auth = newValue + requestConfiguration = newValue.requestConfiguration tokenService.requestConfiguration = requestConfiguration - self.requestConfiguration = requestConfiguration + backend = newValue.backend } get { return _auth } } @@ -1175,12 +1180,12 @@ extension User: NSSecureCoding {} // The list of providers need to be updated for the newly added email-password provider. Task { do { - let accessToken = try await self.internalGetTokenAsync() + let accessToken = try await self.internalGetTokenAsync(backend: self.backend) if let requestConfiguration = self.auth?.requestConfiguration { let getAccountInfoRequest = GetAccountInfoRequest(accessToken: accessToken, requestConfiguration: requestConfiguration) do { - let accountInfoResponse = try await AuthBackend.call(with: getAccountInfoRequest) + let accountInfoResponse = try await self.backend.call(with: getAccountInfoRequest) if let users = accountInfoResponse.users { for userAccountInfo in users { // Set the account to non-anonymous if there are any providers, even if @@ -1315,7 +1320,7 @@ extension User: NSSecureCoding {} private func internalUpdateOrLinkPhoneNumber(credential: PhoneAuthCredential, isLinkOperation: Bool, completion: @escaping (Error?) -> Void) { - internalGetToken { accessToken, error in + internalGetToken(backend: backend) { accessToken, error in if let error { completion(error) return @@ -1337,7 +1342,7 @@ extension User: NSSecureCoding {} request.accessToken = accessToken Task { do { - let verifyResponse = try await AuthBackend.call(with: request) + let verifyResponse = try await self.backend.call(with: request) guard let idToken = verifyResponse.idToken, let refreshToken = verifyResponse.refreshToken else { fatalError("Internal Auth Error: missing token in internalUpdateOrLinkPhoneNumber") @@ -1377,7 +1382,7 @@ extension User: NSSecureCoding {} password: String, authResult: AuthDataResult, _ completion: ((AuthDataResult?, Error?) -> Void)?) { - internalGetToken { accessToken, error in + internalGetToken(backend: backend) { accessToken, error in guard let requestConfiguration = self.auth?.requestConfiguration else { fatalError("Internal auth error: missing auth on User") } @@ -1439,7 +1444,7 @@ extension User: NSSecureCoding {} let result = AuthDataResult(withUser: self, additionalUserInfo: nil) link(withEmail: emailCredential.email, password: password, authResult: result, completion) case let .link(link): - internalGetToken { accessToken, error in + internalGetToken(backend: backend) { accessToken, error in var queryItems = AuthWebUtils.parseURL(link) if link.count == 0 { if let urlComponents = URLComponents(string: link), @@ -1457,7 +1462,7 @@ extension User: NSSecureCoding {} request.idToken = accessToken Task { do { - let response = try await AuthBackend.call(with: request) + let response = try await self.backend.call(with: request) guard let idToken = response.idToken, let refreshToken = response.refreshToken else { fatalError("Internal Auth Error: missing token in EmailLinkSignInResponse") @@ -1486,7 +1491,7 @@ extension User: NSSecureCoding {} #if !os(watchOS) private func link(withGameCenterCredential gameCenterCredential: GameCenterAuthCredential, completion: ((AuthDataResult?, Error?) -> Void)?) { - internalGetToken { accessToken, error in + internalGetToken(backend: backend) { accessToken, error in guard let requestConfiguration = self.auth?.requestConfiguration, let publicKeyURL = gameCenterCredential.publicKeyURL, let signature = gameCenterCredential.signature, @@ -1505,7 +1510,7 @@ extension User: NSSecureCoding {} request.accessToken = accessToken Task { do { - let response = try await AuthBackend.call(with: request) + let response = try await self.backend.call(with: request) guard let idToken = response.idToken, let refreshToken = response.refreshToken else { fatalError("Internal Auth Error: missing token in link(withGameCredential") @@ -1586,10 +1591,11 @@ extension User: NSSecureCoding {} /// - Parameter callback: The block to invoke when the token is available. Invoked asynchronously /// on the global work thread in the future. func internalGetToken(forceRefresh: Bool = false, + backend: AuthBackend, callback: @escaping (String?, Error?) -> Void) { Task { do { - let token = try await internalGetTokenAsync(forceRefresh: forceRefresh) + let token = try await internalGetTokenAsync(forceRefresh: forceRefresh, backend: backend) callback(token, nil) } catch { callback(nil, error) @@ -1599,10 +1605,11 @@ extension User: NSSecureCoding {} /// Retrieves the Firebase authentication token, possibly refreshing it if it has expired. /// - Parameter forceRefresh - func internalGetTokenAsync(forceRefresh: Bool = false) async throws -> String { + func internalGetTokenAsync(forceRefresh: Bool = false, + backend: AuthBackend) async throws -> String { do { let (token, tokenUpdated) = try await tokenService.fetchAccessToken( - forcingRefresh: forceRefresh + forcingRefresh: forceRefresh, backend: backend ) if tokenUpdated { if let error = updateKeychain() { @@ -1748,8 +1755,14 @@ extension User: NSSecureCoding {} self.phoneNumber = phoneNumber self.metadata = metadata ?? UserMetadata(withCreationDate: nil, lastSignInDate: nil) self.tenantID = tenantID - // The `heartbeatLogger` and `appCheck` will be set later via a property update. + // This property will be overwritten later via the `user.auth` property update. This is to + // provide the `heartbeatLogger` and `appCheck` will be set later via the `user.auth` property + // update. For now, a placeholder is set as the property update should happen right after this + // intializer. requestConfiguration = AuthRequestConfiguration(apiKey: apiKey, appID: appID) + // This property will be overwritten later via the `user.auth` property update. For now, a + // placeholder is set as the property update should happen right after this intializer. + backend = AuthBackend(rpcIssuer: AuthBackendRPCIssuer()) userProfileUpdate = UserProfileUpdate() #if os(iOS) self.multiFactor = multiFactor ?? MultiFactor() diff --git a/FirebaseAuth/Sources/Swift/User/UserProfileUpdate.swift b/FirebaseAuth/Sources/Swift/User/UserProfileUpdate.swift index 2d561246557..cf354a6d7fc 100644 --- a/FirebaseAuth/Sources/Swift/User/UserProfileUpdate.swift +++ b/FirebaseAuth/Sources/Swift/User/UserProfileUpdate.swift @@ -18,13 +18,13 @@ import Foundation @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) actor UserProfileUpdate { func link(user: User, with credential: AuthCredential) async throws -> AuthDataResult { - let accessToken = try await user.internalGetTokenAsync() + let accessToken = try await user.internalGetTokenAsync(backend: user.backend) let request = VerifyAssertionRequest(providerID: credential.provider, requestConfiguration: user.requestConfiguration) credential.prepare(request) request.accessToken = accessToken do { - let response = try await AuthBackend.call(with: request) + let response = try await user.backend.call(with: request) guard let idToken = response.idToken, let refreshToken = response.refreshToken, let providerID = response.providerID else { @@ -48,7 +48,7 @@ actor UserProfileUpdate { } func unlink(user: User, fromProvider provider: String) async throws -> User { - let accessToken = try await user.internalGetTokenAsync() + let accessToken = try await user.internalGetTokenAsync(backend: user.backend) let request = SetAccountInfoRequest( accessToken: accessToken, requestConfiguration: user.requestConfiguration ) @@ -58,7 +58,7 @@ actor UserProfileUpdate { } request.deleteProviders = [provider] do { - let response = try await AuthBackend.call(with: request) + let response = try await user.backend.call(with: request) // We can't just use the provider info objects in SetAccountInfoResponse // because they don't have localID and email fields. Remove the specific @@ -105,7 +105,7 @@ actor UserProfileUpdate { SetAccountInfoRequest) -> Void) async throws { let userAccountInfo = try await getAccountInfoRefreshingCache(user) - let accessToken = try await user.internalGetTokenAsync() + let accessToken = try await user.internalGetTokenAsync(backend: user.backend) // Mutate setAccountInfoRequest in block let setAccountInfoRequest = @@ -115,7 +115,7 @@ actor UserProfileUpdate { ) changeBlock(userAccountInfo, setAccountInfoRequest) do { - let accountInfoResponse = try await AuthBackend.call(with: setAccountInfoRequest) + let accountInfoResponse = try await user.backend.call(with: setAccountInfoRequest) if let idToken = accountInfoResponse.idToken, let refreshToken = accountInfoResponse.refreshToken { let tokenService = SecureTokenService( @@ -143,13 +143,13 @@ actor UserProfileUpdate { accessTokenExpirationDate: expirationDate, refreshToken: refreshToken ) - let accessToken = try await user.internalGetTokenAsync() + let accessToken = try await user.internalGetTokenAsync(backend: user.backend) let getAccountInfoRequest = GetAccountInfoRequest( accessToken: accessToken, requestConfiguration: user.requestConfiguration ) do { - let response = try await AuthBackend.call(with: getAccountInfoRequest) + let response = try await user.backend.call(with: getAccountInfoRequest) user.isAnonymous = false user.update(withGetAccountInfoResponse: response) } catch { @@ -168,7 +168,7 @@ actor UserProfileUpdate { /// - Parameter tokenService: The new token service object. /// - Parameter callback: The block to be called in the global auth working queue once finished. func setTokenService(user: User, tokenService: SecureTokenService) async throws { - _ = try await tokenService.fetchAccessToken(forcingRefresh: false) + _ = try await tokenService.fetchAccessToken(forcingRefresh: false, backend: user.backend) user.tokenService = tokenService if let error = user.updateKeychain() { throw error @@ -180,11 +180,11 @@ actor UserProfileUpdate { /// error has been detected. Invoked asynchronously on the auth global work queue in the future. func getAccountInfoRefreshingCache(_ user: User) async throws -> GetAccountInfoResponseUser { - let token = try await user.internalGetTokenAsync() + let token = try await user.internalGetTokenAsync(backend: user.backend) let request = GetAccountInfoRequest(accessToken: token, requestConfiguration: user.requestConfiguration) do { - let accountInfoResponse = try await AuthBackend.call(with: request) + let accountInfoResponse = try await user.backend.call(with: request) user.update(withGetAccountInfoResponse: accountInfoResponse) if let error = user.updateKeychain() { throw error diff --git a/FirebaseAuth/Sources/Swift/Utilities/AuthRecaptchaVerifier.swift b/FirebaseAuth/Sources/Swift/Utilities/AuthRecaptchaVerifier.swift index 6919ac40807..d4247bd6c31 100644 --- a/FirebaseAuth/Sources/Swift/Utilities/AuthRecaptchaVerifier.swift +++ b/FirebaseAuth/Sources/Swift/Utilities/AuthRecaptchaVerifier.swift @@ -149,12 +149,12 @@ } } - guard let requestConfiguration = auth?.requestConfiguration else { + guard let auth = auth else { throw AuthErrorUtils.error(code: .recaptchaNotEnabled, message: "No requestConfiguration for Auth instance") } - let request = GetRecaptchaConfigRequest(requestConfiguration: requestConfiguration) - let response = try await AuthBackend.call(with: request) + let request = GetRecaptchaConfigRequest(requestConfiguration: auth.requestConfiguration) + let response = try await auth.backend.call(with: request) AuthLog.logInfo(code: "I-AUT000029", message: "reCAPTCHA config retrieval succeeded.") // Response's site key is of the format projects//keys/' guard let keys = response.recaptchaKey?.components(separatedBy: "/"), @@ -179,7 +179,7 @@ } let config = AuthRecaptchaConfig(siteKey: siteKey, enablementStatus: enablementStatus) - if let tenantID = auth?.tenantID { + if let tenantID = auth.tenantID { tenantConfigs[tenantID] = config } else { agentConfig = config diff --git a/FirebaseAuth/Sources/Swift/Utilities/AuthWebUtils.swift b/FirebaseAuth/Sources/Swift/Utilities/AuthWebUtils.swift index 2231c040023..69210b9326b 100644 --- a/FirebaseAuth/Sources/Swift/Utilities/AuthWebUtils.swift +++ b/FirebaseAuth/Sources/Swift/Utilities/AuthWebUtils.swift @@ -98,7 +98,8 @@ class AuthWebUtils { return urlComponents.first } - static func fetchAuthDomain(withRequestConfiguration requestConfiguration: AuthRequestConfiguration) + static func fetchAuthDomain(withRequestConfiguration requestConfiguration: AuthRequestConfiguration, + backend: AuthBackend) async throws -> String { if let emulatorHostAndPort = requestConfiguration.emulatorHostAndPort { // If we are using the auth emulator, we do not want to call the GetProjectConfig endpoint. @@ -107,7 +108,7 @@ class AuthWebUtils { } let request = GetProjectConfigRequest(requestConfiguration: requestConfiguration) - let response = try await AuthBackend.call(with: request) + let response = try await backend.call(with: request) // Look up an authorized domain ends with one of the supportedAuthDomains. // The sequence of supportedAuthDomains matters. ("firebaseapp.com", "web.app") diff --git a/FirebaseAuth/Tests/Unit/AuthTests.swift b/FirebaseAuth/Tests/Unit/AuthTests.swift index 1167bf22ca2..f84eb33d70f 100644 --- a/FirebaseAuth/Tests/Unit/AuthTests.swift +++ b/FirebaseAuth/Tests/Unit/AuthTests.swift @@ -47,7 +47,8 @@ class AuthTests: RPCBaseTests { #endif // (os(macOS) && !FIREBASE_AUTH_TESTING_USE_MACOS_KEYCHAIN) || SWIFT_PACKAGE auth = Auth( app: FirebaseApp.app(name: name)!, - keychainStorageProvider: keychainStorageProvider + keychainStorageProvider: keychainStorageProvider, + backend: authBackend ) // Set authDispatcherCallback implementation in order to save the token refresh task for later diff --git a/FirebaseAuth/Tests/Unit/CreateAuthURITests.swift b/FirebaseAuth/Tests/Unit/CreateAuthURITests.swift index af1f54bce28..334091d21fd 100644 --- a/FirebaseAuth/Tests/Unit/CreateAuthURITests.swift +++ b/FirebaseAuth/Tests/Unit/CreateAuthURITests.swift @@ -75,7 +75,7 @@ class CreateAuthURITests: RPCBaseTests { rpcIssuer?.respondBlock = { try self.rpcIssuer?.respond(withJSON: [kAuthUriKey: kTestAuthUri]) } - let rpcResponse = try await AuthBackend.call(with: makeAuthURIRequest()) + let rpcResponse = try await authBackend.call(with: makeAuthURIRequest()) XCTAssertEqual(rpcResponse.authURI, kTestAuthUri) } @@ -89,7 +89,7 @@ class CreateAuthURITests: RPCBaseTests { .respond(withJSON: ["kind": kTestExpectedKind, "allProviders": [kTestProviderID1, kTestProviderID2]]) } - let rpcResponse = try await AuthBackend.call(with: makeAuthURIRequest()) + let rpcResponse = try await authBackend.call(with: makeAuthURIRequest()) XCTAssertEqual(rpcIssuer?.requestURL?.absoluteString, kExpectedAPIURL) XCTAssertEqual(rpcIssuer?.decodedRequest?["identifier"] as? String, kTestIdentifier) diff --git a/FirebaseAuth/Tests/Unit/DeleteAccountTests.swift b/FirebaseAuth/Tests/Unit/DeleteAccountTests.swift index ae213aa8464..a968a02e3e8 100644 --- a/FirebaseAuth/Tests/Unit/DeleteAccountTests.swift +++ b/FirebaseAuth/Tests/Unit/DeleteAccountTests.swift @@ -64,7 +64,7 @@ class DeleteAccountTests: RPCBaseTests { rpcIssuer?.respondBlock = { try self.rpcIssuer?.respond(withJSON: [:]) } - let rpcResponse = try await AuthBackend.call(with: makeDeleteAccountRequest()) + let rpcResponse = try await authBackend.call(with: makeDeleteAccountRequest()) XCTAssertNotNil(rpcResponse) } diff --git a/FirebaseAuth/Tests/Unit/EmailLinkSignInTests.swift b/FirebaseAuth/Tests/Unit/EmailLinkSignInTests.swift index 2cdd4943653..ce4b2dd57d5 100644 --- a/FirebaseAuth/Tests/Unit/EmailLinkSignInTests.swift +++ b/FirebaseAuth/Tests/Unit/EmailLinkSignInTests.swift @@ -65,7 +65,7 @@ class EmailLinkSignInTests: RPCBaseTests { XCTAssertNil(requestDictionary[self.kIDTokenKey]) try self.rpcIssuer?.respond(withJSON: [:]) // unblock the await } - let _ = try await AuthBackend.call(with: makeEmailLinkSignInRequest()) + let _ = try await authBackend.call(with: makeEmailLinkSignInRequest()) } /** @fn testEmailLinkRequestCreationOptional @@ -87,7 +87,7 @@ class EmailLinkSignInTests: RPCBaseTests { XCTAssertEqual(requestDictionary[self.kIDTokenKey], kTestIDToken) try self.rpcIssuer?.respond(withJSON: [:]) // unblock the await } - let _ = try await AuthBackend.call(with: request) + let _ = try await authBackend.call(with: request) } func testEmailLinkSignInErrors() async throws { @@ -115,7 +115,7 @@ class EmailLinkSignInTests: RPCBaseTests { "expiresIn": "\(kTestTokenExpirationTimeInterval)", "refreshToken": kTestRefreshToken]) } - let response = try await AuthBackend.call(with: makeEmailLinkSignInRequest()) + let response = try await authBackend.call(with: makeEmailLinkSignInRequest()) XCTAssertEqual(response.idToken, kTestIDTokenResponse) XCTAssertEqual(response.email, kTestEmailResponse) diff --git a/FirebaseAuth/Tests/Unit/GetAccountInfoTests.swift b/FirebaseAuth/Tests/Unit/GetAccountInfoTests.swift index 87a294c0ef2..dff1e74936e 100644 --- a/FirebaseAuth/Tests/Unit/GetAccountInfoTests.swift +++ b/FirebaseAuth/Tests/Unit/GetAccountInfoTests.swift @@ -100,7 +100,7 @@ class GetAccountInfoTests: RPCBaseTests { rpcIssuer?.respondBlock = { try self.rpcIssuer?.respond(withJSON: ["users": usersIn]) } - let rpcResponse = try await AuthBackend.call(with: makeGetAccountInfoRequest()) + let rpcResponse = try await authBackend.call(with: makeGetAccountInfoRequest()) let users = try XCTUnwrap(rpcResponse.users) XCTAssertGreaterThan(users.count, 0) diff --git a/FirebaseAuth/Tests/Unit/GetOOBConfirmationCodeTests.swift b/FirebaseAuth/Tests/Unit/GetOOBConfirmationCodeTests.swift index b9fe798f57d..631bab3a723 100644 --- a/FirebaseAuth/Tests/Unit/GetOOBConfirmationCodeTests.swift +++ b/FirebaseAuth/Tests/Unit/GetOOBConfirmationCodeTests.swift @@ -199,7 +199,7 @@ class GetOOBConfirmationCodeTests: RPCBaseTests { rpcIssuer?.respondBlock = { try self.rpcIssuer?.respond(withJSON: [self.kOOBCodeKey: self.kTestOOBCode]) } - let response = try await AuthBackend.call(with: request()) + let response = try await authBackend.call(with: request()) XCTAssertEqual(response.OOBCode, kTestOOBCode) } } @@ -217,7 +217,7 @@ class GetOOBConfirmationCodeTests: RPCBaseTests { rpcIssuer?.respondBlock = { try self.rpcIssuer?.respond(withJSON: [:]) } - let response = try await AuthBackend.call(with: request()) + let response = try await authBackend.call(with: request()) XCTAssertNil(response.OOBCode) } } diff --git a/FirebaseAuth/Tests/Unit/GetProjectConfigTests.swift b/FirebaseAuth/Tests/Unit/GetProjectConfigTests.swift index 107a5c33b4f..6dbe4c644c6 100644 --- a/FirebaseAuth/Tests/Unit/GetProjectConfigTests.swift +++ b/FirebaseAuth/Tests/Unit/GetProjectConfigTests.swift @@ -60,7 +60,7 @@ class GetProjectConfigTests: RPCBaseTests { try self.rpcIssuer?.respond(withJSON: ["projectId": kTestProjectID, "authorizedDomains": [kTestDomain1, kTestDomain2]]) } - let rpcResponse = try await AuthBackend.call(with: makeGetProjectConfigRequest()) + let rpcResponse = try await authBackend.call(with: makeGetProjectConfigRequest()) XCTAssertEqual(rpcResponse.projectID, kTestProjectID) XCTAssertEqual(rpcResponse.authorizedDomains?.first, kTestDomain1) XCTAssertEqual(rpcResponse.authorizedDomains?[1], kTestDomain2) diff --git a/FirebaseAuth/Tests/Unit/GetRecaptchaConfigTests.swift b/FirebaseAuth/Tests/Unit/GetRecaptchaConfigTests.swift index df64689c7c0..75149c38d45 100644 --- a/FirebaseAuth/Tests/Unit/GetRecaptchaConfigTests.swift +++ b/FirebaseAuth/Tests/Unit/GetRecaptchaConfigTests.swift @@ -24,7 +24,7 @@ class GetRecaptchaConfigTests: RPCBaseTests { */ func testGetRecaptchaConfigRequest() async throws { let request = GetRecaptchaConfigRequest(requestConfiguration: makeRequestConfiguration()) - // let _ = try await AuthBackend.call(with: request) + // let _ = try await authBackend.call(with: request) XCTAssertFalse(request.containsPostBody) // Confirm that the request has no decoded body as it is get request. @@ -47,7 +47,7 @@ class GetRecaptchaConfigTests: RPCBaseTests { let request = GetRecaptchaConfigRequest(requestConfiguration: makeRequestConfiguration()) rpcIssuer.recaptchaSiteKey = kTestRecaptchaKey - let response = try await AuthBackend.call(with: request) + let response = try await authBackend.call(with: request) XCTAssertEqual(response.recaptchaKey, kTestRecaptchaKey) XCTAssertNil(response.enforcementState) } diff --git a/FirebaseAuth/Tests/Unit/OAuthProviderTests.swift b/FirebaseAuth/Tests/Unit/OAuthProviderTests.swift index 6f1ed895b0a..4c1ca1b7be6 100644 --- a/FirebaseAuth/Tests/Unit/OAuthProviderTests.swift +++ b/FirebaseAuth/Tests/Unit/OAuthProviderTests.swift @@ -282,6 +282,7 @@ import FirebaseCore .replacingOccurrences(of: ")", with: "") FirebaseApp.configure(name: strippedName, options: options) OAuthProviderTests.auth = Auth.auth(app: FirebaseApp.app(name: strippedName)!) + OAuthProviderTests.auth?.backend = authBackend OAuthProviderTests.auth?.mainBundleUrlTypes = [["CFBundleURLSchemes": [scheme]]] } diff --git a/FirebaseAuth/Tests/Unit/ResetPasswordTests.swift b/FirebaseAuth/Tests/Unit/ResetPasswordTests.swift index 7fd7f346d3d..a3220f3ee41 100644 --- a/FirebaseAuth/Tests/Unit/ResetPasswordTests.swift +++ b/FirebaseAuth/Tests/Unit/ResetPasswordTests.swift @@ -82,7 +82,7 @@ class ResetPasswordTests: RPCBaseTests { try self.rpcIssuer?.respond(withJSON: ["email": kTestEmail, "requestType": kExpectedResetPasswordRequestType]) } - let rpcResponse = try await AuthBackend.call(with: makeResetPasswordRequest()) + let rpcResponse = try await authBackend.call(with: makeResetPasswordRequest()) XCTAssertEqual(rpcResponse.email, kTestEmail) XCTAssertEqual(rpcResponse.requestType, kExpectedResetPasswordRequestType) diff --git a/FirebaseAuth/Tests/Unit/RevokeTokenTests.swift b/FirebaseAuth/Tests/Unit/RevokeTokenTests.swift index 945e1e1ee1b..f55f42514d7 100644 --- a/FirebaseAuth/Tests/Unit/RevokeTokenTests.swift +++ b/FirebaseAuth/Tests/Unit/RevokeTokenTests.swift @@ -53,7 +53,7 @@ class RevokeTokenTests: RPCBaseTests { rpcIssuer.respondBlock = { try self.rpcIssuer?.respond(withJSON: [:]) } - let rpcResponse = try await AuthBackend.call(with: makeRevokeTokenRequest()) + let rpcResponse = try await authBackend.call(with: makeRevokeTokenRequest()) XCTAssertNotNil(rpcResponse) } diff --git a/FirebaseAuth/Tests/Unit/SendVerificationCodeTests.swift b/FirebaseAuth/Tests/Unit/SendVerificationCodeTests.swift index aa5a30686b6..d6b4b42bc14 100644 --- a/FirebaseAuth/Tests/Unit/SendVerificationCodeTests.swift +++ b/FirebaseAuth/Tests/Unit/SendVerificationCodeTests.swift @@ -113,7 +113,7 @@ class SendVerificationCodeTests: RPCBaseTests { rpcIssuer.respondBlock = { try self.rpcIssuer?.respond(withJSON: [kVerificationIDKey: kFakeVerificationID]) } - let rpcResponse = try await AuthBackend.call(with: + let rpcResponse = try await authBackend.call(with: makeSendVerificationCodeRequest(CodeIdentity.recaptcha(kTestReCAPTCHAToken))) XCTAssertNotNil(rpcResponse) XCTAssertEqual(rpcResponse.verificationID, kFakeVerificationID) diff --git a/FirebaseAuth/Tests/Unit/SetAccountInfoTests.swift b/FirebaseAuth/Tests/Unit/SetAccountInfoTests.swift index 4efe951991a..037116982de 100644 --- a/FirebaseAuth/Tests/Unit/SetAccountInfoTests.swift +++ b/FirebaseAuth/Tests/Unit/SetAccountInfoTests.swift @@ -212,7 +212,7 @@ class SetAccountInfoTests: RPCBaseTests { kExpiresInKey: kTestExpiresIn, kRefreshTokenKey: kTestRefreshToken]) } - let response = try await AuthBackend.call(with: setAccountInfoRequest()) + let response = try await authBackend.call(with: setAccountInfoRequest()) XCTAssertEqual(response.providerUserInfo?.first?.photoURL?.absoluteString, kTestPhotoURL) XCTAssertEqual(response.idToken, kTestIDToken) XCTAssertEqual(response.refreshToken, kTestRefreshToken) diff --git a/FirebaseAuth/Tests/Unit/SignInWithGameCenterTests.swift b/FirebaseAuth/Tests/Unit/SignInWithGameCenterTests.swift index 3513e9212c8..515cbd13e87 100644 --- a/FirebaseAuth/Tests/Unit/SignInWithGameCenterTests.swift +++ b/FirebaseAuth/Tests/Unit/SignInWithGameCenterTests.swift @@ -99,7 +99,7 @@ class SignInWithGameCenterTests: RPCBaseTests { "displayName": kDisplayName, ]) } - let rpcResponse = try await AuthBackend.call(with: request) + let rpcResponse = try await authBackend.call(with: request) XCTAssertNotNil(rpcResponse) XCTAssertEqual(rpcResponse.idToken, kIDToken) diff --git a/FirebaseAuth/Tests/Unit/SignUpNewUserTests.swift b/FirebaseAuth/Tests/Unit/SignUpNewUserTests.swift index 74c271bff6e..a75f6263a79 100644 --- a/FirebaseAuth/Tests/Unit/SignUpNewUserTests.swift +++ b/FirebaseAuth/Tests/Unit/SignUpNewUserTests.swift @@ -82,7 +82,7 @@ class SignUpNewUserTests: RPCBaseTests { kRefreshTokenKey: kTestRefreshToken, ]) } - let rpcResponse = try await AuthBackend.call(with: makeSignUpNewUserRequest()) + let rpcResponse = try await authBackend.call(with: makeSignUpNewUserRequest()) XCTAssertEqual(rpcResponse.refreshToken, kTestRefreshToken) let expiresIn = try XCTUnwrap(rpcResponse.approximateExpirationDate?.timeIntervalSinceNow) XCTAssertEqual(expiresIn, 12345, accuracy: 0.1) diff --git a/FirebaseAuth/Tests/Unit/VerifyAssertionTests.swift b/FirebaseAuth/Tests/Unit/VerifyAssertionTests.swift index 95e131a8760..2fb630a62ce 100644 --- a/FirebaseAuth/Tests/Unit/VerifyAssertionTests.swift +++ b/FirebaseAuth/Tests/Unit/VerifyAssertionTests.swift @@ -180,7 +180,7 @@ class VerifyAssertionTests: RPCBaseTests { self.kRawUserInfoKey: self.profile, ]) } - let rpcResponse = try await AuthBackend.call(with: makeVerifyAssertionRequest()) + let rpcResponse = try await authBackend.call(with: makeVerifyAssertionRequest()) XCTAssertEqual(rpcResponse.idToken, kTestIDToken) XCTAssertEqual(rpcResponse.refreshToken, kTestRefreshToken) XCTAssertEqual(rpcResponse.verifiedProvider, [kTestProvider]) @@ -211,7 +211,7 @@ class VerifyAssertionTests: RPCBaseTests { self.kRawUserInfoKey: self.convertToJson(self.profile), ]) } - let rpcResponse = try await AuthBackend.call(with: makeVerifyAssertionRequest()) + let rpcResponse = try await authBackend.call(with: makeVerifyAssertionRequest()) XCTAssertEqual(rpcResponse.idToken, kTestIDToken) XCTAssertEqual(rpcResponse.refreshToken, kTestRefreshToken) XCTAssertEqual(rpcResponse.verifiedProvider, [kTestProvider]) diff --git a/FirebaseAuth/Tests/Unit/VerifyClientTests.swift b/FirebaseAuth/Tests/Unit/VerifyClientTests.swift index 0f7b0c792ca..528cb357c88 100644 --- a/FirebaseAuth/Tests/Unit/VerifyClientTests.swift +++ b/FirebaseAuth/Tests/Unit/VerifyClientTests.swift @@ -71,7 +71,7 @@ class VerifyClientTests: RPCBaseTests { kSuggestedTimeOutKey: kFakeSuggestedTimeout, ]) } - let rpcResponse = try await AuthBackend.call(with: makeVerifyClientRequest()) + let rpcResponse = try await authBackend.call(with: makeVerifyClientRequest()) XCTAssertEqual(rpcResponse.receipt, kFakeReceipt) let timeOut = try XCTUnwrap(rpcResponse.suggestedTimeOutDate?.timeIntervalSinceNow) XCTAssertEqual(timeOut, 1234, accuracy: 0.1) diff --git a/FirebaseAuth/Tests/Unit/VerifyCustomTokenTests.swift b/FirebaseAuth/Tests/Unit/VerifyCustomTokenTests.swift index 36fb06d13e6..8e3e052a4b2 100644 --- a/FirebaseAuth/Tests/Unit/VerifyCustomTokenTests.swift +++ b/FirebaseAuth/Tests/Unit/VerifyCustomTokenTests.swift @@ -107,7 +107,7 @@ class VerifyCustomTokenTests: RPCBaseTests { kIsNewUserKey: true, ]) } - let rpcResponse = try await AuthBackend.call(with: makeVerifyCustomTokenRequest()) + let rpcResponse = try await authBackend.call(with: makeVerifyCustomTokenRequest()) XCTAssertEqual(rpcResponse.idToken, kTestIDToken) XCTAssertEqual(rpcResponse.refreshToken, kTestRefreshToken) let expiresIn = try XCTUnwrap(rpcResponse.approximateExpirationDate?.timeIntervalSinceNow) diff --git a/FirebaseAuth/Tests/Unit/VerifyPasswordTests.swift b/FirebaseAuth/Tests/Unit/VerifyPasswordTests.swift index 6b831f0ce80..06821bf74c2 100644 --- a/FirebaseAuth/Tests/Unit/VerifyPasswordTests.swift +++ b/FirebaseAuth/Tests/Unit/VerifyPasswordTests.swift @@ -171,7 +171,7 @@ class VerifyPasswordTests: RPCBaseTests { kPhotoUrlKey: kTestPhotoUrl, ]) } - let rpcResponse = try await AuthBackend.call(with: makeVerifyPasswordRequest()) + let rpcResponse = try await authBackend.call(with: makeVerifyPasswordRequest()) XCTAssertEqual(rpcResponse.email, kTestEmail) XCTAssertEqual(rpcResponse.localID, kTestLocalID) XCTAssertEqual(rpcResponse.displayName, kTestDisplayName) diff --git a/FirebaseAuth/Tests/Unit/VerifyPhoneNumberTests.swift b/FirebaseAuth/Tests/Unit/VerifyPhoneNumberTests.swift index e6ff2a42434..50fb9cf67e5 100644 --- a/FirebaseAuth/Tests/Unit/VerifyPhoneNumberTests.swift +++ b/FirebaseAuth/Tests/Unit/VerifyPhoneNumberTests.swift @@ -116,7 +116,7 @@ import XCTest "isNewUser": true, ]) } - let rpcResponse = try await AuthBackend.call(with: makeVerifyPhoneNumberRequest()) + let rpcResponse = try await authBackend.call(with: makeVerifyPhoneNumberRequest()) XCTAssertEqual(rpcResponse.localID, kTestLocalID) XCTAssertEqual(rpcResponse.idToken, kTestIDToken) let expiresIn = try XCTUnwrap(rpcResponse.approximateExpirationDate?.timeIntervalSinceNow) @@ -135,7 +135,7 @@ import XCTest ]) } do { - let _ = try await AuthBackend.call(with: makeVerifyPhoneNumberRequestWithTemporaryProof()) + let _ = try await authBackend.call(with: makeVerifyPhoneNumberRequestWithTemporaryProof()) XCTFail("Expected to throw") } catch { let rpcError = error as NSError From 63f1a216f64c9718ea030a18ff6fe4292a5a76e5 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Thu, 31 Oct 2024 21:16:09 -0400 Subject: [PATCH 03/12] lint error --- FirebaseAuth/Sources/Swift/User/User.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FirebaseAuth/Sources/Swift/User/User.swift b/FirebaseAuth/Sources/Swift/User/User.swift index 8b102113bb5..c1784126232 100644 --- a/FirebaseAuth/Sources/Swift/User/User.swift +++ b/FirebaseAuth/Sources/Swift/User/User.swift @@ -1401,7 +1401,7 @@ extension User: NSSecureCoding {} action: AuthRecaptchaAction .signUpPassword) #else - let response = try await backend.call(with: request) + let response = try await self.backend.call(with: request) #endif guard let refreshToken = response.refreshToken, let idToken = response.idToken else { From 057affe8c77c6fe2a1ea61e5bd957635a065c5b7 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Fri, 1 Nov 2024 10:02:06 -0400 Subject: [PATCH 04/12] Fix non-iOS builds --- FirebaseAuth/Sources/Swift/Backend/AuthBackend.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/FirebaseAuth/Sources/Swift/Backend/AuthBackend.swift b/FirebaseAuth/Sources/Swift/Backend/AuthBackend.swift index 669f585ae81..3894272d5f9 100644 --- a/FirebaseAuth/Sources/Swift/Backend/AuthBackend.swift +++ b/FirebaseAuth/Sources/Swift/Backend/AuthBackend.swift @@ -107,7 +107,7 @@ class AuthBackend: AuthBackendProtocol { private static func generateMFAError(response: AuthRPCResponse, auth: Auth) -> Error? { #if !os(iOS) return nil - #endif // !os(iOS) + #else if let mfaResponse = response as? AuthMFAResponse, mfaResponse.idToken == nil, let enrollments = mfaResponse.mfaInfo { @@ -130,6 +130,7 @@ class AuthBackend: AuthBackendProtocol { } else { return nil } + #endif // !os(iOS) } // Check whether or not the successful response is actually the special case phone From 2c73bbb263edcc44dd5d360293e401234ab194bc Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Fri, 1 Nov 2024 10:13:08 -0400 Subject: [PATCH 05/12] style --- .../Sources/Swift/Backend/AuthBackend.swift | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/FirebaseAuth/Sources/Swift/Backend/AuthBackend.swift b/FirebaseAuth/Sources/Swift/Backend/AuthBackend.swift index 3894272d5f9..fb21b14cb5b 100644 --- a/FirebaseAuth/Sources/Swift/Backend/AuthBackend.swift +++ b/FirebaseAuth/Sources/Swift/Backend/AuthBackend.swift @@ -108,28 +108,28 @@ class AuthBackend: AuthBackendProtocol { #if !os(iOS) return nil #else - if let mfaResponse = response as? AuthMFAResponse, - mfaResponse.idToken == nil, - let enrollments = mfaResponse.mfaInfo { - var info: [MultiFactorInfo] = [] - for enrollment in enrollments { - // check which MFA factors are enabled. - if let _ = enrollment.phoneInfo { - info.append(PhoneMultiFactorInfo(proto: enrollment)) - } else if let _ = enrollment.totpInfo { - info.append(TOTPMultiFactorInfo(proto: enrollment)) - } else { - AuthLog.logError(code: "I-AUT000021", message: "Multifactor type is not supported") + if let mfaResponse = response as? AuthMFAResponse, + mfaResponse.idToken == nil, + let enrollments = mfaResponse.mfaInfo { + var info: [MultiFactorInfo] = [] + for enrollment in enrollments { + // check which MFA factors are enabled. + if let _ = enrollment.phoneInfo { + info.append(PhoneMultiFactorInfo(proto: enrollment)) + } else if let _ = enrollment.totpInfo { + info.append(TOTPMultiFactorInfo(proto: enrollment)) + } else { + AuthLog.logError(code: "I-AUT000021", message: "Multifactor type is not supported") + } } + return AuthErrorUtils.secondFactorRequiredError( + pendingCredential: mfaResponse.mfaPendingCredential, + hints: info, + auth: auth + ) + } else { + return nil } - return AuthErrorUtils.secondFactorRequiredError( - pendingCredential: mfaResponse.mfaPendingCredential, - hints: info, - auth: auth - ) - } else { - return nil - } #endif // !os(iOS) } From 88677ce1a4701b782e11f9c4a370828471c8cbd9 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Fri, 1 Nov 2024 10:14:45 -0400 Subject: [PATCH 06/12] Fix further linting issues --- .../Sources/Swift/Backend/AuthBackend.swift | 29 ++++++++++--------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/FirebaseAuth/Sources/Swift/Backend/AuthBackend.swift b/FirebaseAuth/Sources/Swift/Backend/AuthBackend.swift index fb21b14cb5b..55205ee8247 100644 --- a/FirebaseAuth/Sources/Swift/Backend/AuthBackend.swift +++ b/FirebaseAuth/Sources/Swift/Backend/AuthBackend.swift @@ -138,21 +138,22 @@ class AuthBackend: AuthBackendProtocol { private static func phoneCredentialInUse(response: AuthRPCResponse) -> Error? { #if !os(iOS) return nil + #else + if let phoneAuthResponse = response as? VerifyPhoneNumberResponse, + let phoneNumber = phoneAuthResponse.phoneNumber, + phoneNumber.count > 0, + let temporaryProof = phoneAuthResponse.temporaryProof, + temporaryProof.count > 0 { + let credential = PhoneAuthCredential(withTemporaryProof: temporaryProof, + phoneNumber: phoneNumber, + providerID: PhoneAuthProvider.id) + return AuthErrorUtils.credentialAlreadyInUseError(message: nil, + credential: credential, + email: nil) + } else { + return nil + } #endif // !os(iOS) - if let phoneAuthResponse = response as? VerifyPhoneNumberResponse, - let phoneNumber = phoneAuthResponse.phoneNumber, - phoneNumber.count > 0, - let temporaryProof = phoneAuthResponse.temporaryProof, - temporaryProof.count > 0 { - let credential = PhoneAuthCredential(withTemporaryProof: temporaryProof, - phoneNumber: phoneNumber, - providerID: PhoneAuthProvider.id) - return AuthErrorUtils.credentialAlreadyInUseError(message: nil, - credential: credential, - email: nil) - } else { - return nil - } } /// Calls the RPC using HTTP request. From f492714f5bea484025aea49aea9ad6a44e4f9a9c Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Fri, 1 Nov 2024 11:37:15 -0400 Subject: [PATCH 07/12] Fix user unit tests --- FirebaseAuth/Tests/Unit/UserTests.swift | 97 +++++++++++++------------ 1 file changed, 51 insertions(+), 46 deletions(-) diff --git a/FirebaseAuth/Tests/Unit/UserTests.swift b/FirebaseAuth/Tests/Unit/UserTests.swift index 77c1449792f..d1f305f7116 100644 --- a/FirebaseAuth/Tests/Unit/UserTests.swift +++ b/FirebaseAuth/Tests/Unit/UserTests.swift @@ -33,12 +33,13 @@ class UserTests: RPCBaseTests { let kVerificationID = "55432" let kPhoneNumber = "555-1234" - static var auth: Auth? + var auth: Auth? - override class func setUp() { + override func setUp() { + super.setUp() let options = FirebaseOptions(googleAppID: "0:0000000000000:ios:0000000000000000", gcmSenderID: "00000000000000000-00000000000-000000000") - options.apiKey = kFakeAPIKey + options.apiKey = Self.kFakeAPIKey options.projectID = "myUserProjectID" FirebaseApp.configure(name: "test-UserTests", options: options) #if (os(macOS) && !FIREBASE_AUTH_TESTING_USE_MACOS_KEYCHAIN) || SWIFT_PACKAGE @@ -48,13 +49,17 @@ class UserTests: RPCBaseTests { #endif // (os(macOS) && !FIREBASE_AUTH_TESTING_USE_MACOS_KEYCHAIN) || SWIFT_PACKAGE auth = Auth( app: FirebaseApp.app(name: "test-UserTests")!, - keychainStorageProvider: keychainStorageProvider + keychainStorageProvider: keychainStorageProvider, + backend: authBackend ) } override func tearDown() { // Verifies that no tasks are left suspended on the AuthSerialTaskQueue. - try? UserTests.auth?.signOut() + try? auth?.signOut() + auth = nil + FirebaseApp.resetApps() + super.tearDown() } /** @fn testUserPropertiesAndNSSecureCoding @@ -415,7 +420,7 @@ class UserTests: RPCBaseTests { // Email should not have changed on the client side. XCTAssertEqual(user.email, self.kEmail) // User is still signed in. - XCTAssertEqual(UserTests.auth?.currentUser, user) + XCTAssertEqual(self.auth?.currentUser, user) expectation.fulfill() } } @@ -441,7 +446,7 @@ class UserTests: RPCBaseTests { // Email should not have changed on the client side. XCTAssertEqual(user.email, self.kEmail) // User is no longer signed in.. - XCTAssertNil(UserTests.auth?.currentUser) + XCTAssertNil(self.auth?.currentUser) expectation.fulfill() } } @@ -457,7 +462,7 @@ class UserTests: RPCBaseTests { func testUpdatePhoneSuccess() throws { setFakeGetAccountProvider() let expectation = self.expectation(description: #function) - let auth = try XCTUnwrap(UserTests.auth) + let auth = try XCTUnwrap(self.auth) signInWithEmailPasswordReturnFakeUser { user in do { self.rpcIssuer.respondBlock = { @@ -491,7 +496,7 @@ class UserTests: RPCBaseTests { func testUpdatePhoneNumberFailure() throws { setFakeGetAccountProvider() let expectation = self.expectation(description: #function) - let auth = try XCTUnwrap(UserTests.auth) + let auth = try XCTUnwrap(self.auth) signInWithEmailPasswordReturnFakeUser { user in do { self.rpcIssuer.respondBlock = { @@ -523,7 +528,7 @@ class UserTests: RPCBaseTests { func testUpdatePhoneNumberFailureAutoSignOut() throws { setFakeGetAccountProvider() let expectation = self.expectation(description: #function) - let auth = try XCTUnwrap(UserTests.auth) + let auth = try XCTUnwrap(self.auth) signInWithEmailPasswordReturnFakeUser { user in do { self.rpcIssuer.respondBlock = { @@ -540,7 +545,7 @@ class UserTests: RPCBaseTests { let error = try! XCTUnwrap(rawError) XCTAssertEqual((error as NSError).code, AuthErrorCode.userTokenExpired.rawValue) // User is no longer signed in. - XCTAssertNil(UserTests.auth?.currentUser) + XCTAssertNil(self.auth?.currentUser) expectation.fulfill() } } @@ -579,7 +584,7 @@ class UserTests: RPCBaseTests { // Email should not have changed on the client side. XCTAssertEqual(user.email, self.kEmail) // User is still signed in. - XCTAssertEqual(UserTests.auth?.currentUser, user) + XCTAssertEqual(self.auth?.currentUser, user) expectation.fulfill() } } @@ -605,7 +610,7 @@ class UserTests: RPCBaseTests { // Email should not have changed on the client side. XCTAssertEqual(user.email, self.kEmail) // User is still signed in. - XCTAssertEqual(UserTests.auth?.currentUser, user) + XCTAssertEqual(self.auth?.currentUser, user) expectation.fulfill() } } @@ -632,7 +637,7 @@ class UserTests: RPCBaseTests { // Email should not have changed on the client side. XCTAssertEqual(user.email, self.kEmail) // User is signed out. - XCTAssertNil(UserTests.auth?.currentUser) + XCTAssertNil(self.auth?.currentUser) expectation.fulfill() } } @@ -688,7 +693,7 @@ class UserTests: RPCBaseTests { XCTAssertEqual(user.email, self.kEmail) XCTAssertEqual(user.displayName, self.kDisplayName) // User is still signed in. - XCTAssertEqual(UserTests.auth?.currentUser, user) + XCTAssertEqual(self.auth?.currentUser, user) expectation.fulfill() } } @@ -716,7 +721,7 @@ class UserTests: RPCBaseTests { // Email should not have changed on the client side. XCTAssertEqual(user.email, self.kEmail) // User is signed out. - XCTAssertNil(UserTests.auth?.currentUser) + XCTAssertNil(self.auth?.currentUser) expectation.fulfill() } } @@ -821,7 +826,7 @@ class UserTests: RPCBaseTests { let error = try! XCTUnwrap(rawError) XCTAssertEqual((error as NSError).code, AuthErrorCode.quotaExceeded.rawValue) // User is still signed in. - XCTAssertEqual(UserTests.auth?.currentUser, user) + XCTAssertEqual(self.auth?.currentUser, user) expectation.fulfill() } } @@ -848,7 +853,7 @@ class UserTests: RPCBaseTests { let error = try! XCTUnwrap(rawError) XCTAssertEqual((error as NSError).code, AuthErrorCode.userTokenExpired.rawValue) // User is no longer signed in. - XCTAssertNil(UserTests.auth?.currentUser) + XCTAssertNil(self.auth?.currentUser) expectation.fulfill() } } @@ -878,7 +883,7 @@ class UserTests: RPCBaseTests { XCTAssertEqual(result.user.email, user.email) XCTAssertEqual(result.additionalUserInfo?.isNewUser, false) // User is still signed in. - XCTAssertEqual(UserTests.auth?.currentUser, user) + XCTAssertEqual(self.auth?.currentUser, user) expectation.fulfill() } } @@ -914,7 +919,7 @@ class UserTests: RPCBaseTests { } XCTAssertNil(error) // Verify that the current user is unchanged. - XCTAssertEqual(UserTests.auth?.currentUser, user) + XCTAssertEqual(self.auth?.currentUser, user) // Verify that the current user and reauthenticated user are not same pointers. XCTAssertNotEqual(user, reauthenticatedAuthResult?.user) // Verify that anyway the current user and reauthenticated user have same IDs. @@ -933,7 +938,7 @@ class UserTests: RPCBaseTests { } } waitForExpectations(timeout: 5) - try assertUserGoogle(UserTests.auth?.currentUser) + try assertUserGoogle(auth?.currentUser) } /** @fn testReauthenticateFailure @@ -958,7 +963,7 @@ class UserTests: RPCBaseTests { // Email should not have changed on the client side. XCTAssertEqual(user.email, self.kEmail) // User is still signed in. - XCTAssertEqual(UserTests.auth?.currentUser, user) + XCTAssertEqual(self.auth?.currentUser, user) expectation.fulfill() } } @@ -987,7 +992,7 @@ class UserTests: RPCBaseTests { // Email should not have changed on the client side. XCTAssertEqual(user.email, self.kEmail) // User is still signed in. - XCTAssertEqual(UserTests.auth?.currentUser, user) + XCTAssertEqual(self.auth?.currentUser, user) expectation.fulfill() } } @@ -1001,7 +1006,7 @@ class UserTests: RPCBaseTests { func testLinkAndRetrieveDataSuccess() throws { setFakeGetAccountProvider() let expectation = self.expectation(description: #function) - let auth = try XCTUnwrap(UserTests.auth) + let auth = try XCTUnwrap(self.auth) signInWithFacebookCredential { user in XCTAssertNotNil(user) do { @@ -1069,7 +1074,7 @@ class UserTests: RPCBaseTests { // Email should not have changed on the client side. XCTAssertEqual(user.email, self.kFacebookEmail) // User is still signed in. - XCTAssertEqual(UserTests.auth?.currentUser, user) + XCTAssertEqual(self.auth?.currentUser, user) expectation.fulfill() } } @@ -1100,7 +1105,7 @@ class UserTests: RPCBaseTests { XCTFail("Expected to throw providerAlreadyLinked error.") } // User is still signed in. - XCTAssertEqual(UserTests.auth?.currentUser, user) + XCTAssertEqual(self.auth?.currentUser, user) expectation.fulfill() } } @@ -1130,7 +1135,7 @@ class UserTests: RPCBaseTests { let error = try! XCTUnwrap(rawError) XCTAssertEqual((error as NSError).code, AuthErrorCode.userDisabled.rawValue) // User is signed out. - XCTAssertNil(UserTests.auth?.currentUser) + XCTAssertNil(self.auth?.currentUser) expectation.fulfill() } } @@ -1145,7 +1150,7 @@ class UserTests: RPCBaseTests { func testLinkEmailAndRetrieveDataSuccess() throws { setFakeGetAccountProvider() let expectation = self.expectation(description: #function) - let auth = try XCTUnwrap(UserTests.auth) + let auth = try XCTUnwrap(self.auth) signInWithFacebookCredential { user in XCTAssertNotNil(user) do { @@ -1213,7 +1218,7 @@ class UserTests: RPCBaseTests { XCTFail("Expected to throw providerAlreadyLinked error.") } // User is still signed in. - XCTAssertEqual(UserTests.auth?.currentUser, user) + XCTAssertEqual(self.auth?.currentUser, user) expectation.fulfill() } } @@ -1247,7 +1252,7 @@ class UserTests: RPCBaseTests { let error = try! XCTUnwrap(rawError) XCTAssertEqual((error as NSError).code, AuthErrorCode.tooManyRequests.rawValue) // User is still signed in. - XCTAssertEqual(UserTests.auth?.currentUser, user) + XCTAssertEqual(self.auth?.currentUser, user) expectation.fulfill() } } @@ -1277,7 +1282,7 @@ class UserTests: RPCBaseTests { let error = try! XCTUnwrap(rawError) XCTAssertEqual((error as NSError).code, AuthErrorCode.userTokenExpired.rawValue) // User is signed out. - XCTAssertNil(UserTests.auth?.currentUser) + XCTAssertNil(self.auth?.currentUser) expectation.fulfill() } } @@ -1307,7 +1312,7 @@ class UserTests: RPCBaseTests { func testLinkProviderFailure() throws { setFakeGetAccountProvider() let expectation = self.expectation(description: #function) - let auth = try XCTUnwrap(UserTests.auth) + let auth = try XCTUnwrap(self.auth) signInWithFacebookCredential { user in XCTAssertNotNil(user) do { @@ -1322,7 +1327,7 @@ class UserTests: RPCBaseTests { let error = try! XCTUnwrap(rawError) XCTAssertEqual((error as NSError).code, AuthErrorCode.userTokenExpired.rawValue) // User is signed out. - XCTAssertNil(UserTests.auth?.currentUser) + XCTAssertNil(self.auth?.currentUser) expectation.fulfill() } } @@ -1336,7 +1341,7 @@ class UserTests: RPCBaseTests { func testReauthenticateWithProviderFailure() throws { setFakeGetAccountProvider() let expectation = self.expectation(description: #function) - let auth = try XCTUnwrap(UserTests.auth) + let auth = try XCTUnwrap(self.auth) signInWithFacebookCredential { user in XCTAssertNotNil(user) do { @@ -1351,7 +1356,7 @@ class UserTests: RPCBaseTests { let error = try! XCTUnwrap(rawError) XCTAssertEqual((error as NSError).code, AuthErrorCode.userTokenExpired.rawValue) // User is still signed in. - XCTAssertEqual(UserTests.auth?.currentUser, user) + XCTAssertEqual(self.auth?.currentUser, user) expectation.fulfill() } } @@ -1365,7 +1370,7 @@ class UserTests: RPCBaseTests { func testLinkPhoneAuthCredentialSuccess() throws { setFakeGetAccountProvider() let expectation = self.expectation(description: #function) - let auth = try XCTUnwrap(UserTests.auth) + let auth = try XCTUnwrap(self.auth) signInWithEmailPasswordReturnFakeUser { user in XCTAssertNotNil(user) self.expectVerifyPhoneNumberRequest(isLink: true) @@ -1410,7 +1415,7 @@ class UserTests: RPCBaseTests { func testUnlinkPhoneAuthCredentialSuccess() throws { setFakeGetAccountProvider() let expectation = self.expectation(description: #function) - let auth = try XCTUnwrap(UserTests.auth) + let auth = try XCTUnwrap(self.auth) signInWithEmailPasswordReturnFakeUser { user in XCTAssertNotNil(user) self.expectVerifyPhoneNumberRequest(isLink: true) @@ -1506,7 +1511,7 @@ class UserTests: RPCBaseTests { func testlinkPhoneCredentialAlreadyExistsError() throws { setFakeGetAccountProvider() let expectation = self.expectation(description: #function) - let auth = try XCTUnwrap(UserTests.auth) + let auth = try XCTUnwrap(self.auth) signInWithEmailPasswordReturnFakeUser { user in XCTAssertNotNil(user) self.expectVerifyPhoneNumberRequest(isLink: true) @@ -1668,12 +1673,12 @@ class UserTests: RPCBaseTests { } // 1. After setting up fakes, sign out and sign in. do { - try UserTests.auth?.signOut() + try auth?.signOut() } catch { XCTFail("Sign out failed: \(error)") return } - UserTests.auth?.signIn(withEmail: kEmail, password: kFakePassword) { authResult, error in + auth?.signIn(withEmail: kEmail, password: kFakePassword) { authResult, error in // 4. After the response triggers the callback, verify the returned result. XCTAssertTrue(Thread.isMainThread) guard let user = authResult?.user else { @@ -1716,10 +1721,10 @@ class UserTests: RPCBaseTests { } do { - try UserTests.auth?.signOut() + try auth?.signOut() let googleCredential = GoogleAuthProvider.credential(withIDToken: kGoogleIDToken, accessToken: kGoogleAccessToken) - UserTests.auth?.signIn(with: googleCredential) { authResult, error in + auth?.signIn(with: googleCredential) { authResult, error in // 4. After the response triggers the callback, verify the returned result. XCTAssertTrue(Thread.isMainThread) guard let user = authResult?.user else { @@ -1783,10 +1788,10 @@ class UserTests: RPCBaseTests { } do { - try UserTests.auth?.signOut() + try auth?.signOut() let facebookCredential = FacebookAuthProvider .credential(withAccessToken: kFacebookAccessToken) - UserTests.auth?.signIn(with: facebookCredential) { authResult, error in + auth?.signIn(with: facebookCredential) { authResult, error in // 4. After the response triggers the callback, verify the returned result. XCTAssertTrue(Thread.isMainThread) guard let user = authResult?.user else { @@ -1838,8 +1843,8 @@ class UserTests: RPCBaseTests { } do { - try UserTests.auth?.signOut() - UserTests.auth?.signIn( + try auth?.signOut() + auth?.signIn( withEmail: kEmail, link: "https://www.google.com?oobCode=aCode&mode=signIn" ) { authResult, error in From f25c7d5d226257a4bf8eca29145f806e2780e216 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Fri, 1 Nov 2024 12:01:35 -0400 Subject: [PATCH 08/12] Change Auth.backend property to constant --- FirebaseAuth/Sources/Swift/Auth/Auth.swift | 2 +- FirebaseAuth/Tests/Unit/OAuthProviderTests.swift | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/FirebaseAuth/Sources/Swift/Auth/Auth.swift b/FirebaseAuth/Sources/Swift/Auth/Auth.swift index 9dbd31e65a6..adb14de9b30 100644 --- a/FirebaseAuth/Sources/Swift/Auth/Auth.swift +++ b/FirebaseAuth/Sources/Swift/Auth/Auth.swift @@ -2337,7 +2337,7 @@ extension Auth: AuthInterop { /// Auth's backend. var requestConfiguration: AuthRequestConfiguration - var backend: AuthBackend + let backend: AuthBackend #if os(iOS) diff --git a/FirebaseAuth/Tests/Unit/OAuthProviderTests.swift b/FirebaseAuth/Tests/Unit/OAuthProviderTests.swift index 4c1ca1b7be6..a5a330babf7 100644 --- a/FirebaseAuth/Tests/Unit/OAuthProviderTests.swift +++ b/FirebaseAuth/Tests/Unit/OAuthProviderTests.swift @@ -282,7 +282,10 @@ import FirebaseCore .replacingOccurrences(of: ")", with: "") FirebaseApp.configure(name: strippedName, options: options) OAuthProviderTests.auth = Auth.auth(app: FirebaseApp.app(name: strippedName)!) - OAuthProviderTests.auth?.backend = authBackend + OAuthProviderTests.auth = Auth( + app: FirebaseApp.app(name: strippedName)!, + backend: authBackend + ) OAuthProviderTests.auth?.mainBundleUrlTypes = [["CFBundleURLSchemes": [scheme]]] } From 5fcd4d2cd691cc500b4f7df9332694eebd9471ba Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Fri, 1 Nov 2024 12:02:03 -0400 Subject: [PATCH 09/12] Update int. test code --- .../ViewControllers/AuthViewController.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/ViewControllers/AuthViewController.swift b/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/ViewControllers/AuthViewController.swift index fdde2e0757d..e9567a69e9d 100644 --- a/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/ViewControllers/AuthViewController.swift +++ b/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/ViewControllers/AuthViewController.swift @@ -481,7 +481,7 @@ class AuthViewController: UIViewController, DataSourceProviderDelegate { Task { do { - let verifyResponse = try await AuthBackend.call(with: request) + let verifyResponse = try await AppManager.shared.auth().backend.call(with: request) guard let receipt = verifyResponse.receipt, let timeoutDate = verifyResponse.suggestedTimeOutDate else { @@ -510,7 +510,7 @@ class AuthViewController: UIViewController, DataSourceProviderDelegate { ) do { - _ = try await AuthBackend.call(with: request) + _ = try await AppManager.shared.auth().backend.call(with: request) print("Verify iOS client succeeded") } catch { print("Verify iOS Client failed: \(error.localizedDescription)") From c5ada6ee1cd2a3149b582adafee0c1a7434440e8 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Fri, 1 Nov 2024 12:05:19 -0400 Subject: [PATCH 10/12] Fix more tests --- FirebaseAuth/Tests/Unit/PhoneAuthProviderTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FirebaseAuth/Tests/Unit/PhoneAuthProviderTests.swift b/FirebaseAuth/Tests/Unit/PhoneAuthProviderTests.swift index 0030c52776d..9a319ea9755 100644 --- a/FirebaseAuth/Tests/Unit/PhoneAuthProviderTests.swift +++ b/FirebaseAuth/Tests/Unit/PhoneAuthProviderTests.swift @@ -662,7 +662,7 @@ let strippedName = functionName.replacingOccurrences(of: "(", with: "") .replacingOccurrences(of: ")", with: "") FirebaseApp.configure(name: strippedName, options: options) - let auth = Auth.auth(app: FirebaseApp.app(name: strippedName)!) + let auth = Auth(app: FirebaseApp.app(name: strippedName)!, backend: authBackend) kAuthGlobalWorkQueue.sync { // Wait for Auth protectedDataInitialization to finish. From 6f95736120032133b05d9916d8b0872b0bba0c05 Mon Sep 17 00:00:00 2001 From: Nick Cooke <36927374+ncooke3@users.noreply.github.com> Date: Fri, 1 Nov 2024 12:53:09 -0400 Subject: [PATCH 11/12] [skip ci] Update FirebaseAuth/Sources/Swift/Backend/AuthBackend.swift --- FirebaseAuth/Sources/Swift/Backend/AuthBackend.swift | 5 ----- 1 file changed, 5 deletions(-) diff --git a/FirebaseAuth/Sources/Swift/Backend/AuthBackend.swift b/FirebaseAuth/Sources/Swift/Backend/AuthBackend.swift index 55205ee8247..f53a926ae74 100644 --- a/FirebaseAuth/Sources/Swift/Backend/AuthBackend.swift +++ b/FirebaseAuth/Sources/Swift/Backend/AuthBackend.swift @@ -32,11 +32,6 @@ class AuthBackend: AuthBackendProtocol { return "FirebaseAuth.iOS/\(FirebaseVersion()) \(GTMFetcherStandardUserAgentString(nil))" } -// static func call(with request: T) async throws -> T.Response { -// return try await shared.call(with: request) -// } -// -// private static let shared: AuthBackend = .init(rpcIssuer: AuthBackendRPCIssuer()) private let rpcIssuer: any AuthBackendRPCIssuerProtocol From e17fc968580f5b7d885df32a3df11f4cee806bca Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Fri, 1 Nov 2024 14:10:35 -0400 Subject: [PATCH 12/12] [skip ci] rename test class --- ...ckendRPCImplementationTests.swift => AuthBackendTests.swift} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename FirebaseAuth/Tests/Unit/{AuthBackendRPCImplementationTests.swift => AuthBackendTests.swift} (99%) diff --git a/FirebaseAuth/Tests/Unit/AuthBackendRPCImplementationTests.swift b/FirebaseAuth/Tests/Unit/AuthBackendTests.swift similarity index 99% rename from FirebaseAuth/Tests/Unit/AuthBackendRPCImplementationTests.swift rename to FirebaseAuth/Tests/Unit/AuthBackendTests.swift index 91daaedf2d5..2e62c8a8cd1 100644 --- a/FirebaseAuth/Tests/Unit/AuthBackendRPCImplementationTests.swift +++ b/FirebaseAuth/Tests/Unit/AuthBackendTests.swift @@ -27,7 +27,7 @@ private let kFakeAPIKey = "kTestAPIKey" private let kFakeAppID = "kTestFirebaseAppID" @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) -class AuthBackendRPCImplementationTests: RPCBaseTests { +class AuthBackendTests: RPCBaseTests { let kFakeErrorDomain = "fakeDomain" let kFakeErrorCode = -1