diff --git a/FirebaseAuth/CHANGELOG.md b/FirebaseAuth/CHANGELOG.md index 6b0f356bdf99..819927da2b22 100644 --- a/FirebaseAuth/CHANGELOG.md +++ b/FirebaseAuth/CHANGELOG.md @@ -4,7 +4,7 @@ - [added] Introduced the Swift enum `AuthProviderID` for the Auth Provider IDs. (#9236) - [deprecated] Swift APIs using `String`-typed `productID`s have been deprecated in favor of newly added API that leverages the `AuthProviderID` enum. -- [fixed] Breaking API: The `email` property in `ActionCodeInfo` is not non-optional. +- [fixed] Breaking API: The `email` property in `ActionCodeInfo` is now non-optional. # 10.21.0 - [fixed] Fixed multifactor resolver to use the correct Auth instance instead of diff --git a/FirebaseAuth/Sources/Swift/Auth/Auth.swift b/FirebaseAuth/Sources/Swift/Auth/Auth.swift index 46d39520201f..15757bca11f0 100644 --- a/FirebaseAuth/Sources/Swift/Auth/Auth.swift +++ b/FirebaseAuth/Sources/Swift/Auth/Auth.swift @@ -53,7 +53,7 @@ import FirebaseCoreExtension open func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { - self.tokenManagerGet().cancel(withError: error) + tokenManagerGet().cancel(withError: error) } open func application(_ application: UIApplication, @@ -76,30 +76,30 @@ import FirebaseCoreExtension extension Auth: AuthInterop { func getTokenInternal(forcingRefresh forceRefresh: Bool) { // Enable token auto-refresh if not already enabled. - if !self.autoRefreshTokens { + if !autoRefreshTokens { AuthLog.logInfo(code: "I-AUT000002", message: "Token auto-refresh enabled.") - self.autoRefreshTokens = true - self.scheduleAutoTokenRefresh() - -#if os(iOS) || os(tvOS) // TODO(ObjC): Is a similar mechanism needed on macOS? - self.applicationDidBecomeActiveObserver = - NotificationCenter.default.addObserver( - forName: UIApplication.didBecomeActiveNotification, - object: nil, queue: nil - ) { notification in - self.isAppInBackground = false - if !self.autoRefreshScheduled { - self.scheduleAutoTokenRefresh() - } - } - self.applicationDidEnterBackgroundObserver = - NotificationCenter.default.addObserver( - forName: UIApplication.didEnterBackgroundNotification, - object: nil, queue: nil - ) { notification in - self.isAppInBackground = true - } -#endif + autoRefreshTokens = true + scheduleAutoTokenRefresh() + + #if os(iOS) || os(tvOS) // TODO(ObjC): Is a similar mechanism needed on macOS? + applicationDidBecomeActiveObserver = + NotificationCenter.default.addObserver( + forName: UIApplication.didBecomeActiveNotification, + object: nil, queue: nil + ) { notification in + self.isAppInBackground = false + if !self.autoRefreshScheduled { + self.scheduleAutoTokenRefresh() + } + } + applicationDidEnterBackgroundObserver = + NotificationCenter.default.addObserver( + forName: UIApplication.didEnterBackgroundNotification, + object: nil, queue: nil + ) { notification in + self.isAppInBackground = true + } + #endif } } @@ -172,10 +172,10 @@ extension Auth: AuthInterop { /// The string used to set this property must be a language code that follows BCP 47. @objc open var languageCode: String? { get { - self.getLanguageCode() + getLanguageCode() } set(val) { - self.setLanguageCode(val) + setLanguageCode(val) } } @@ -1160,8 +1160,8 @@ extension Auth: AuthInterop { if let currentUser { let idToken = try await currentUser.internalGetTokenAsync() let request = RevokeTokenRequest(withToken: authorizationCode, - idToken: idToken, - requestConfiguration: self.requestConfiguration) + idToken: idToken, + requestConfiguration: requestConfiguration) let _ = try await AuthBackend.call(with: request) } } @@ -1294,33 +1294,33 @@ extension Auth: AuthInterop { return data } - func tokenManagerInit(_ manager: AuthAPNSTokenManager) { - let semaphore = DispatchSemaphore(value: 0) - Task { - await authWorker.tokenManagerInit(manager) - semaphore.signal() + func tokenManagerInit(_ manager: AuthAPNSTokenManager) { + let semaphore = DispatchSemaphore(value: 0) + Task { + await authWorker.tokenManagerInit(manager) + semaphore.signal() + } + semaphore.wait() } - semaphore.wait() - } func tokenManagerInit(_ manager: AuthAPNSTokenManager) async { await authWorker.tokenManagerInit(manager) } - func tokenManagerGet() -> AuthAPNSTokenManager { - var manager: AuthAPNSTokenManager! - let semaphore = DispatchSemaphore(value: 0) - Task { - manager = await tokenManagerGet() - semaphore.signal() + func tokenManagerGet() -> AuthAPNSTokenManager { + var manager: AuthAPNSTokenManager! + let semaphore = DispatchSemaphore(value: 0) + Task { + manager = await tokenManagerGet() + semaphore.signal() + } + semaphore.wait() + return manager } - semaphore.wait() - return manager - } - func tokenManagerGet() async -> AuthAPNSTokenManager { - return await authWorker.tokenManagerGet() - } + func tokenManagerGet() async -> AuthAPNSTokenManager { + return await authWorker.tokenManagerGet() + } /// Sets the APNs token along with its type. /// @@ -1337,38 +1337,38 @@ extension Auth: AuthInterop { semaphore.wait() } - /// Sets the APNs token along with its type. - /// - /// This method is available on iOS only. - /// - /// If swizzling is disabled, the APNs Token must be set for phone number auth to work, - /// by either setting calling this method or by setting the `APNSToken` property. - open func setAPNSToken(_ token: Data, type: AuthAPNSTokenType) async { + /// Sets the APNs token along with its type. + /// + /// This method is available on iOS only. + /// + /// If swizzling is disabled, the APNs Token must be set for phone number auth to work, + /// by either setting calling this method or by setting the `APNSToken` property. + open func setAPNSToken(_ token: Data, type: AuthAPNSTokenType) async { await authWorker.tokenManagerSet(token, type: type) - } + } - /// Whether the specific remote notification is handled by `Auth` . - /// - /// This method is available on iOS only. - /// - /// If swizzling is disabled, related remote notifications must be forwarded to this method - /// for phone number auth to work. - /// - Parameter userInfo: A dictionary that contains information related to the - /// notification in question. - /// - Returns: Whether or the notification is handled. A return value of `true` means the - /// notification is for Firebase Auth so the caller should ignore the notification from further - /// processing, and `false` means the notification is for the app (or another library) so - /// the caller should continue handling this notification as usual. - @objc open func canHandleNotification(_ userInfo: [AnyHashable: Any]) -> Bool { - var result = false - let semaphore = DispatchSemaphore(value: 0) - Task { - result = await authWorker.canHandleNotification(userInfo) - semaphore.signal() + /// Whether the specific remote notification is handled by `Auth` . + /// + /// This method is available on iOS only. + /// + /// If swizzling is disabled, related remote notifications must be forwarded to this method + /// for phone number auth to work. + /// - Parameter userInfo: A dictionary that contains information related to the + /// notification in question. + /// - Returns: Whether or the notification is handled. A return value of `true` means the + /// notification is for Firebase Auth so the caller should ignore the notification from further + /// processing, and `false` means the notification is for the app (or another library) so + /// the caller should continue handling this notification as usual. + @objc open func canHandleNotification(_ userInfo: [AnyHashable: Any]) -> Bool { + var result = false + let semaphore = DispatchSemaphore(value: 0) + Task { + result = await authWorker.canHandleNotification(userInfo) + semaphore.signal() + } + semaphore.wait() + return result } - semaphore.wait() - return result - } /// Whether the specific remote notification is handled by `Auth` . /// @@ -1382,7 +1382,7 @@ extension Auth: AuthInterop { /// notification is for Firebase Auth so the caller should ignore the notification from further /// processing, and `false` means the notification is for the app (or another library) so /// the caller should continue handling this notification as usual. - @objc open func canHandleNotification(_ userInfo: [AnyHashable: Any]) async -> Bool { + @objc open func canHandleNotification(_ userInfo: [AnyHashable: Any]) async -> Bool { return await authWorker.canHandleNotification(userInfo) } @@ -1409,21 +1409,21 @@ extension Auth: AuthInterop { return result } - /// Whether the specific URL is handled by `Auth` . - /// - /// This method is available on iOS only. - /// - /// If swizzling is disabled, URLs received by the application delegate must be forwarded - /// to this method for phone number auth to work. - /// - Parameter url: The URL received by the application delegate from any of the openURL - /// method. - /// - Returns: Whether or the URL is handled. `true` means the URL is for Firebase Auth - /// so the caller should ignore the URL from further processing, and `false` means the - /// the URL is for the app (or another library) so the caller should continue handling - /// this URL as usual. - open func canHandle(_ url: URL) async -> Bool { + /// Whether the specific URL is handled by `Auth` . + /// + /// This method is available on iOS only. + /// + /// If swizzling is disabled, URLs received by the application delegate must be forwarded + /// to this method for phone number auth to work. + /// - Parameter url: The URL received by the application delegate from any of the openURL + /// method. + /// - Returns: Whether or the URL is handled. `true` means the URL is for Firebase Auth + /// so the caller should ignore the URL from further processing, and `false` means the + /// the URL is for the app (or another library) so the caller should continue handling + /// this URL as usual. + open func canHandle(_ url: URL) async -> Bool { return await authWorker.canHandle(url) - } + } #endif /// The name of the `NSNotificationCenter` notification which is posted when the auth state @@ -1469,7 +1469,7 @@ extension Auth: AuthInterop { } } - // TODO delete me + // TODO: delete me func signInFlowAuthDataResultCallback(byDecorating callback: ((AuthDataResult?, Error?) -> Void)?) -> (AuthDataResult?, Error?) -> Void { @@ -1704,7 +1704,7 @@ extension Auth: AuthInterop { } autoRefreshScheduled = true Task { - await authWorker.autoTokenRefresh(accessToken: accessToken, + await authWorker.autoTokenRefresh(accessToken: accessToken, retry: retry, delay: fastTokenRefreshForTest ? 0.1 : delay) } @@ -1759,8 +1759,6 @@ extension Auth: AuthInterop { anonymous: anonymous) } - - private func getQueryItems(_ link: String) -> [String: String] { var queryItems = AuthWebUtils.parseURL(link) if queryItems.count == 0 { diff --git a/FirebaseAuth/Sources/Swift/Auth/AuthWorker.swift b/FirebaseAuth/Sources/Swift/Auth/AuthWorker.swift index fcb886ebd8cd..d6666b47d1d7 100644 --- a/FirebaseAuth/Sources/Swift/Auth/AuthWorker.swift +++ b/FirebaseAuth/Sources/Swift/Auth/AuthWorker.swift @@ -18,7 +18,7 @@ import Foundation @_implementationOnly import GoogleUtilities #else @_implementationOnly import GoogleUtilities_AppDelegateSwizzler -@_implementationOnly import GoogleUtilities_Environment + @_implementationOnly import GoogleUtilities_Environment #endif #if os(iOS) || os(tvOS) || targetEnvironment(macCatalyst) @@ -45,7 +45,7 @@ actor AuthWorker { } func tokenManagerSet(_ token: Data, type: AuthAPNSTokenType) { - self.tokenManager.token = AuthAPNSToken(withData: token, type: type) + tokenManager.token = AuthAPNSToken(withData: token, type: type) } func tokenManagerGet() -> AuthAPNSTokenManager { @@ -68,13 +68,13 @@ actor AuthWorker { /// Only for testing func tokenManagerInit(_ manager: AuthAPNSTokenManager) { - self.tokenManager = manager + tokenManager = manager } func fetchSignInMethods(forEmail email: String) async throws -> [String] { let request = CreateAuthURIRequest(identifier: email, continueURI: "http:www.google.com", - requestConfiguration: self.requestConfiguration) + requestConfiguration: requestConfiguration) let response = try await AuthBackend.call(with: request) return response.signinMethods ?? [] } @@ -96,26 +96,25 @@ actor AuthWorker { isReauthentication: false) } - -#if os(iOS) - func signIn(with provider: FederatedAuthProvider, - uiDelegate: AuthUIDelegate?) async throws -> AuthDataResult { - let credential = try await provider.credential(with: uiDelegate) - return try await self.internalSignInAndRetrieveData( - withCredential: credential, - isReauthentication: false - ) - } -#endif + #if os(iOS) + func signIn(with provider: FederatedAuthProvider, + uiDelegate: AuthUIDelegate?) async throws -> AuthDataResult { + let credential = try await provider.credential(with: uiDelegate) + return try await internalSignInAndRetrieveData( + withCredential: credential, + isReauthentication: false + ) + } + #endif func signInAnonymously() async throws -> AuthDataResult { if let currentUser = requestConfiguration.auth?.currentUser, currentUser.isAnonymous { return AuthDataResult(withUser: currentUser, additionalUserInfo: nil) } - let request = SignUpNewUserRequest(requestConfiguration: self.requestConfiguration) + let request = SignUpNewUserRequest(requestConfiguration: requestConfiguration) let response = try await AuthBackend.call(with: request) - let user = try await self.completeSignIn( + let user = try await completeSignIn( withAccessToken: response.idToken, accessTokenExpirationDate: response.approximateExpirationDate, refreshToken: response.refreshToken, @@ -131,9 +130,9 @@ actor AuthWorker { func signIn(withCustomToken token: String) async throws -> AuthDataResult { let request = VerifyCustomTokenRequest(token: token, - requestConfiguration: self.requestConfiguration) + requestConfiguration: requestConfiguration) let response = try await AuthBackend.call(with: request) - let user = try await self.completeSignIn( + let user = try await completeSignIn( withAccessToken: response.idToken, accessTokenExpirationDate: response.approximateExpirationDate, refreshToken: response.refreshToken, @@ -152,14 +151,14 @@ actor AuthWorker { password: password, displayName: nil, idToken: nil, - requestConfiguration: self.requestConfiguration) -#if os(iOS) - let response = try await injectRecaptcha(request: request, - action: AuthRecaptchaAction.signUpPassword) -#else - let response = try await AuthBackend.call(with: request) -#endif - let user = try await self.completeSignIn( + requestConfiguration: requestConfiguration) + #if os(iOS) + let response = try await injectRecaptcha(request: request, + action: AuthRecaptchaAction.signUpPassword) + #else + let response = try await AuthBackend.call(with: request) + #endif + let user = try await completeSignIn( withAccessToken: response.idToken, accessTokenExpirationDate: response.approximateExpirationDate, refreshToken: response.refreshToken, @@ -175,14 +174,14 @@ actor AuthWorker { func confirmPasswordReset(withCode code: String, newPassword: String) async throws { let request = ResetPasswordRequest(oobCode: code, newPassword: newPassword, - requestConfiguration: self.requestConfiguration) - let _ = try await AuthBackend.call(with: request) + requestConfiguration: requestConfiguration) + _ = try await AuthBackend.call(with: request) } func checkActionCode(_ code: String) async throws -> ActionCodeInfo { let request = ResetPasswordRequest(oobCode: code, newPassword: nil, - requestConfiguration: self.requestConfiguration) + requestConfiguration: requestConfiguration) let response = try await AuthBackend.call(with: request) let operation = ActionCodeInfo.actionCodeOperation(forRequestType: response.requestType) @@ -200,9 +199,9 @@ actor AuthWorker { } func applyActionCode(_ code: String) async throws { - let request = SetAccountInfoRequest(requestConfiguration: self.requestConfiguration) + let request = SetAccountInfoRequest(requestConfiguration: requestConfiguration) request.oobCode = code - let _ = try await AuthBackend.call(with: request) + _ = try await AuthBackend.call(with: request) } func sendPasswordReset(withEmail email: String, @@ -210,14 +209,14 @@ actor AuthWorker { let request = GetOOBConfirmationCodeRequest.passwordResetRequest( email: email, actionCodeSettings: actionCodeSettings, - requestConfiguration: self.requestConfiguration + requestConfiguration: requestConfiguration ) -#if os(iOS) - let _ = try await injectRecaptcha(request: request, - action: AuthRecaptchaAction.getOobCode) -#else - let _ = try await AuthBackend.call(with: request) -#endif + #if os(iOS) + _ = try await injectRecaptcha(request: request, + action: AuthRecaptchaAction.getOobCode) + #else + _ = try await AuthBackend.call(with: request) + #endif } func sendSignInLink(toEmail email: String, @@ -225,14 +224,14 @@ actor AuthWorker { let request = GetOOBConfirmationCodeRequest.signInWithEmailLinkRequest( email, actionCodeSettings: actionCodeSettings, - requestConfiguration: self.requestConfiguration + requestConfiguration: requestConfiguration ) -#if os(iOS) - let _ = try await injectRecaptcha(request: request, - action: AuthRecaptchaAction.getOobCode) -#else - let _ = try await AuthBackend.call(with: request) -#endif + #if os(iOS) + _ = try await injectRecaptcha(request: request, + action: AuthRecaptchaAction.getOobCode) + #else + _ = try await AuthBackend.call(with: request) + #endif } func signOut() throws { @@ -243,13 +242,13 @@ actor AuthWorker { } func updateCurrentUser(_ user: User) async throws { - if user.requestConfiguration.apiKey != self.requestConfiguration.apiKey { + if user.requestConfiguration.apiKey != requestConfiguration.apiKey { // If the API keys are different, then we need to confirm that the user belongs to the same // project before proceeding. - user.requestConfiguration = self.requestConfiguration + user.requestConfiguration = requestConfiguration try await user.reload() } - try self.updateCurrentUser(user, byForce: true, savingToDisk: true) + try updateCurrentUser(user, byForce: true, savingToDisk: true) } /// Continue with the rest of the Auth object initialization in the worker actor. @@ -271,51 +270,51 @@ actor AuthWorker { try auth.internalUseUserAccessGroup(storedUserAccessGroup) } else { let user = try auth.getUser() - try self.updateCurrentUser(user, byForce: false, savingToDisk: false) + try updateCurrentUser(user, byForce: false, savingToDisk: false) if let user { auth.tenantID = user.tenantID auth.lastNotifiedUserToken = user.rawAccessToken() } } } catch { -#if os(iOS) || os(tvOS) || targetEnvironment(macCatalyst) - if (error as NSError).code == AuthErrorCode.keychainError.rawValue { - // If there's a keychain error, assume it is due to the keychain being accessed - // before the device is unlocked as a result of prewarming, and listen for the - // UIApplicationProtectedDataDidBecomeAvailable notification. - auth.addProtectedDataDidBecomeAvailableObserver() - } -#endif + #if os(iOS) || os(tvOS) || targetEnvironment(macCatalyst) + if (error as NSError).code == AuthErrorCode.keychainError.rawValue { + // If there's a keychain error, assume it is due to the keychain being accessed + // before the device is unlocked as a result of prewarming, and listen for the + // UIApplicationProtectedDataDidBecomeAvailable notification. + auth.addProtectedDataDidBecomeAvailableObserver() + } + #endif AuthLog.logError(code: "I-AUT000001", message: "Error loading saved user when starting up: \(error)") } -#if os(iOS) - if GULAppEnvironmentUtil.isAppExtension() { - // iOS App extensions should not call [UIApplication sharedApplication], even if - // UIApplication responds to it. - return - } + #if os(iOS) + if GULAppEnvironmentUtil.isAppExtension() { + // iOS App extensions should not call [UIApplication sharedApplication], even if + // UIApplication responds to it. + return + } - // Using reflection here to avoid build errors in extensions. - let sel = NSSelectorFromString("sharedApplication") - guard UIApplication.responds(to: sel), - let rawApplication = UIApplication.perform(sel), - let application = rawApplication.takeUnretainedValue() as? UIApplication else { - return - } + // Using reflection here to avoid build errors in extensions. + let sel = NSSelectorFromString("sharedApplication") + guard UIApplication.responds(to: sel), + let rawApplication = UIApplication.perform(sel), + let application = rawApplication.takeUnretainedValue() as? UIApplication else { + return + } - // Initialize for phone number auth. - tokenManager = AuthAPNSTokenManager(withApplication: application) - auth.appCredentialManager = AuthAppCredentialManager(withKeychain: auth.keychainServices) - auth.notificationManager = AuthNotificationManager( - withApplication: application, - appCredentialManager: auth.appCredentialManager - ) + // Initialize for phone number auth. + tokenManager = AuthAPNSTokenManager(withApplication: application) + auth.appCredentialManager = AuthAppCredentialManager(withKeychain: auth.keychainServices) + auth.notificationManager = AuthNotificationManager( + withApplication: application, + appCredentialManager: auth.appCredentialManager + ) - GULAppDelegateSwizzler.registerAppDelegateInterceptor(auth) - GULSceneDelegateSwizzler.registerSceneDelegateInterceptor(auth) -#endif + GULAppDelegateSwizzler.registerAppDelegateInterceptor(auth) + GULSceneDelegateSwizzler.registerSceneDelegateInterceptor(auth) + #endif } func updateEmail(user: User, @@ -379,8 +378,10 @@ actor AuthWorker { /// - Parameter changeBlock: A block responsible for mutating a template `SetAccountInfoRequest` /// - Parameter callback: A block to invoke when the change is complete. Invoked asynchronously on /// the auth global work queue in the future. - private func executeUserUpdateWithChanges(user: User, changeBlock: @escaping (GetAccountInfoResponseUser, - SetAccountInfoRequest) -> Void) async throws { + private func executeUserUpdateWithChanges(user: User, + changeBlock: @escaping (GetAccountInfoResponseUser, + SetAccountInfoRequest) + -> Void) async throws { let userAccountInfo = try await getAccountInfoRefreshingCache(user) let accessToken = try await user.internalGetTokenAsync() @@ -409,7 +410,8 @@ actor AuthWorker { /// Gets the users' account data from the server, updating our local values. /// - Parameter callback: Invoked when the request to getAccountInfo has completed, or when an /// error has been detected. Invoked asynchronously on the auth global work queue in the future. - private func getAccountInfoRefreshingCache(_ user: User) async throws -> GetAccountInfoResponseUser { + private func getAccountInfoRefreshingCache(_ user: User) async throws + -> GetAccountInfoResponseUser { let token = try await user.internalGetTokenAsync() let request = GetAccountInfoRequest(accessToken: token, requestConfiguration: requestConfiguration) @@ -459,7 +461,7 @@ actor AuthWorker { func updateCurrentUser(_ user: User?, byForce force: Bool, savingToDisk saveToDisk: Bool) throws { if user == requestConfiguration.auth?.currentUser { - // TODO local + // TODO: local requestConfiguration.auth?.possiblyPostAuthStateChangeNotification() } if let user { @@ -471,7 +473,7 @@ actor AuthWorker { var throwError: Error? if saveToDisk { do { - // TODO call local saveSuer + // TODO: call local saveSuer try requestConfiguration.auth?.saveUser(user) } catch { throwError = error @@ -479,7 +481,7 @@ actor AuthWorker { } if throwError == nil || force { requestConfiguration.auth?.currentUser = user - // TODO + // TODO: requestConfiguration.auth?.possiblyPostAuthStateChangeNotification() } if let throwError { @@ -491,33 +493,33 @@ actor AuthWorker { // If host is an IPv6 address, it should be formatted with surrounding brackets. let formattedHost = host.contains(":") ? "[\(host)]" : host requestConfiguration.emulatorHostAndPort = "\(formattedHost):\(port)" -#if os(iOS) - requestConfiguration.auth?.settings?.appVerificationDisabledForTesting = true -#endif + #if os(iOS) + requestConfiguration.auth?.settings?.appVerificationDisabledForTesting = true + #endif } -#if os(iOS) - func canHandleNotification(_ userInfo: [AnyHashable: Any]) async -> Bool { - guard let auth = requestConfiguration.auth else { - return false + #if os(iOS) + func canHandleNotification(_ userInfo: [AnyHashable: Any]) async -> Bool { + guard let auth = requestConfiguration.auth else { + return false + } + return auth.notificationManager.canHandle(notification: userInfo) } - return auth.notificationManager.canHandle(notification: userInfo) - } - func canHandle(_ url: URL) -> Bool { - guard let auth = requestConfiguration.auth, - let authURLPresenter = auth.authURLPresenter as? AuthURLPresenter else { - return false + func canHandle(_ url: URL) -> Bool { + guard let auth = requestConfiguration.auth, + let authURLPresenter = auth.authURLPresenter as? AuthURLPresenter else { + return false + } + return authURLPresenter.canHandle(url: url) } - return authURLPresenter.canHandle(url: url) - } -#endif + #endif func autoTokenRefresh(accessToken: String, retry: Bool, delay: TimeInterval) async { try? await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000)) guard let auth = requestConfiguration.auth, - let currentUser = auth.currentUser else { + let currentUser = auth.currentUser else { return } let accessToken = currentUser.rawAccessToken() @@ -531,7 +533,7 @@ actor AuthWorker { } let uid = currentUser.uid do { - let _ = try await currentUser.internalGetTokenAsync(forceRefresh: true) + _ = try await currentUser.internalGetTokenAsync(forceRefresh: true) if auth.currentUser?.uid != uid { return } @@ -543,7 +545,8 @@ actor AuthWorker { } } - func fetchAccessToken(user: User, forcingRefresh forceRefresh: Bool) async throws -> (String?, Bool) { + func fetchAccessToken(user: User, + forcingRefresh forceRefresh: Bool) async throws -> (String?, Bool) { if !forceRefresh, user.tokenService.hasValidAccessToken() { return (user.tokenService.accessToken, false) } else { @@ -553,7 +556,7 @@ actor AuthWorker { } private func internalSignInAndRetrieveData(withCredential credential: AuthCredential, - isReauthentication: Bool) async throws + isReauthentication: Bool) async throws -> AuthDataResult { if let emailCredential = credential as? EmailAuthCredential { // Special case for email/password credentials @@ -631,70 +634,70 @@ actor AuthWorker { credential: updatedOAuthCredential) } -#if os(iOS) - /// Signs in using a phone credential. - /// - Parameter credential: The Phone Auth credential used to sign in. - /// - Parameter operation: The type of operation for which this sign-in attempt is initiated. - private func signIn(withPhoneCredential credential: PhoneAuthCredential, - operation: AuthOperationType) async throws -> VerifyPhoneNumberResponse { - switch credential.credentialKind { - case let .phoneNumber(phoneNumber, temporaryProof): - let request = VerifyPhoneNumberRequest(temporaryProof: temporaryProof, - phoneNumber: phoneNumber, - operation: operation, - requestConfiguration: requestConfiguration) - return try await AuthBackend.call(with: request) - case let .verification(verificationID, code): - guard verificationID.count > 0 else { - throw AuthErrorUtils.missingVerificationIDError(message: nil) + #if os(iOS) + /// Signs in using a phone credential. + /// - Parameter credential: The Phone Auth credential used to sign in. + /// - Parameter operation: The type of operation for which this sign-in attempt is initiated. + private func signIn(withPhoneCredential credential: PhoneAuthCredential, + operation: AuthOperationType) async throws -> VerifyPhoneNumberResponse { + switch credential.credentialKind { + case let .phoneNumber(phoneNumber, temporaryProof): + let request = VerifyPhoneNumberRequest(temporaryProof: temporaryProof, + phoneNumber: phoneNumber, + operation: operation, + requestConfiguration: requestConfiguration) + return try await AuthBackend.call(with: request) + case let .verification(verificationID, code): + guard verificationID.count > 0 else { + throw AuthErrorUtils.missingVerificationIDError(message: nil) + } + guard code.count > 0 else { + throw AuthErrorUtils.missingVerificationCodeError(message: nil) + } + let request = VerifyPhoneNumberRequest(verificationID: verificationID, + verificationCode: code, + operation: operation, + requestConfiguration: requestConfiguration) + return try await AuthBackend.call(with: request) } - guard code.count > 0 else { - throw AuthErrorUtils.missingVerificationCodeError(message: nil) + } + #endif + + #if !os(watchOS) + /// Signs in using a game center credential. + /// - Parameter credential: The Game Center Auth Credential used to sign in. + private func signInAndRetrieveData(withGameCenterCredential credential: GameCenterAuthCredential) async throws + -> AuthDataResult { + guard let publicKeyURL = credential.publicKeyURL, + let signature = credential.signature, + let salt = credential.salt else { + fatalError( + "Internal Auth Error: Game Center credential missing publicKeyURL, signature, or salt" + ) } - let request = VerifyPhoneNumberRequest(verificationID: verificationID, - verificationCode: code, - operation: operation, - requestConfiguration: requestConfiguration) - return try await AuthBackend.call(with: request) + let request = SignInWithGameCenterRequest(playerID: credential.playerID, + teamPlayerID: credential.teamPlayerID, + gamePlayerID: credential.gamePlayerID, + publicKeyURL: publicKeyURL, + signature: signature, + salt: salt, + timestamp: credential.timestamp, + displayName: credential.displayName, + requestConfiguration: requestConfiguration) + let response = try await AuthBackend.call(with: request) + let user = try await completeSignIn(withAccessToken: response.idToken, + accessTokenExpirationDate: response + .approximateExpirationDate, + refreshToken: response.refreshToken, + anonymous: false) + let additionalUserInfo = AdditionalUserInfo(providerID: GameCenterAuthProvider.id, + profile: nil, + username: nil, + isNewUser: response.isNewUser) + return AuthDataResult(withUser: user, additionalUserInfo: additionalUserInfo) } - } -#endif -#if !os(watchOS) - /// Signs in using a game center credential. - /// - Parameter credential: The Game Center Auth Credential used to sign in. - private func signInAndRetrieveData(withGameCenterCredential credential: GameCenterAuthCredential) async throws - -> AuthDataResult { - guard let publicKeyURL = credential.publicKeyURL, - let signature = credential.signature, - let salt = credential.salt else { - fatalError( - "Internal Auth Error: Game Center credential missing publicKeyURL, signature, or salt" - ) - } - let request = SignInWithGameCenterRequest(playerID: credential.playerID, - teamPlayerID: credential.teamPlayerID, - gamePlayerID: credential.gamePlayerID, - publicKeyURL: publicKeyURL, - signature: signature, - salt: salt, - timestamp: credential.timestamp, - displayName: credential.displayName, - requestConfiguration: requestConfiguration) - let response = try await AuthBackend.call(with: request) - let user = try await completeSignIn(withAccessToken: response.idToken, - accessTokenExpirationDate: response - .approximateExpirationDate, - refreshToken: response.refreshToken, - anonymous: false) - let additionalUserInfo = AdditionalUserInfo(providerID: GameCenterAuthProvider.id, - profile: nil, - username: nil, - isNewUser: response.isNewUser) - return AuthDataResult(withUser: user, additionalUserInfo: additionalUserInfo) - } - -#endif + #endif /// Signs in using an email and email sign-in link. /// - Parameter email: The user's email address. @@ -726,6 +729,7 @@ actor AuthWorker { isNewUser: response.isNewUser) return AuthDataResult(withUser: user, additionalUserInfo: additionalUserInfo) } + private func getQueryItems(_ link: String) -> [String: String] { var queryItems = AuthWebUtils.parseURL(link) if queryItems.count == 0 { @@ -737,8 +741,6 @@ actor AuthWorker { return queryItems } - - private func internalSignInUser(withEmail email: String, password: String) async throws -> User { let request = VerifyPasswordRequest(email: email, password: password, @@ -761,43 +763,43 @@ actor AuthWorker { } #if os(iOS) - func injectRecaptcha(request: T, - action: AuthRecaptchaAction) async throws -> T - .Response { - let recaptchaVerifier = AuthRecaptchaVerifier.shared(auth: requestConfiguration.auth) - if recaptchaVerifier.enablementStatus(forProvider: AuthRecaptchaProvider.password) { - try await recaptchaVerifier.injectRecaptchaFields(request: request, - provider: AuthRecaptchaProvider.password, - action: action) - } else { - do { - return try await AuthBackend.call(with: request) - } catch { - let nsError = error as NSError - if let underlyingError = nsError.userInfo[NSUnderlyingErrorKey] as? NSError, - nsError.code == AuthErrorCode.internalError.rawValue, - let messages = underlyingError - .userInfo[AuthErrorUtils.userInfoDeserializedResponseKey] as? [String: AnyHashable], - let message = messages["message"] as? String, - message.hasPrefix("MISSING_RECAPTCHA_TOKEN") { - try await recaptchaVerifier.injectRecaptchaFields( - request: request, - provider: AuthRecaptchaProvider.password, - action: action - ) - } else { - throw error + func injectRecaptcha(request: T, + action: AuthRecaptchaAction) async throws -> T + .Response { + let recaptchaVerifier = AuthRecaptchaVerifier.shared(auth: requestConfiguration.auth) + if recaptchaVerifier.enablementStatus(forProvider: AuthRecaptchaProvider.password) { + try await recaptchaVerifier.injectRecaptchaFields(request: request, + provider: AuthRecaptchaProvider.password, + action: action) + } else { + do { + return try await AuthBackend.call(with: request) + } catch { + let nsError = error as NSError + if let underlyingError = nsError.userInfo[NSUnderlyingErrorKey] as? NSError, + nsError.code == AuthErrorCode.internalError.rawValue, + let messages = underlyingError + .userInfo[AuthErrorUtils.userInfoDeserializedResponseKey] as? [String: AnyHashable], + let message = messages["message"] as? String, + message.hasPrefix("MISSING_RECAPTCHA_TOKEN") { + try await recaptchaVerifier.injectRecaptchaFields( + request: request, + provider: AuthRecaptchaProvider.password, + action: action + ) + } else { + throw error + } } } + return try await AuthBackend.call(with: request) } - return try await AuthBackend.call(with: request) - } -#endif + #endif private func completeSignIn(withAccessToken accessToken: String?, - accessTokenExpirationDate: Date?, - refreshToken: String?, - anonymous: Bool) async throws -> User { + accessTokenExpirationDate: Date?, + refreshToken: String?, + anonymous: Bool) async throws -> User { return try await User.retrieveUser(withAuth: requestConfiguration.auth!, accessToken: accessToken, accessTokenExpirationDate: accessTokenExpirationDate, @@ -805,11 +807,7 @@ actor AuthWorker { anonymous: anonymous) } - init(requestConfiguration: AuthRequestConfiguration) { self.requestConfiguration = requestConfiguration } - } - - diff --git a/FirebaseAuth/Sources/Swift/SystemService/SecureTokenService.swift b/FirebaseAuth/Sources/Swift/SystemService/SecureTokenService.swift index 704de8c1c822..a7f569b1dfc6 100644 --- a/FirebaseAuth/Sources/Swift/SystemService/SecureTokenService.swift +++ b/FirebaseAuth/Sources/Swift/SystemService/SecureTokenService.swift @@ -90,7 +90,8 @@ class SecureTokenService: NSObject, NSSecureCoding { /// - Parameter forceRefresh: Forces the token to be refreshed. /// - Parameter callback: Callback block that will be called to return either the token or an /// error. - func fetchAccessToken(user: User, forcingRefresh forceRefresh: Bool) async throws -> (String?, Bool) { + func fetchAccessToken(user: User, + forcingRefresh forceRefresh: Bool) async throws -> (String?, Bool) { return try await user.auth.authWorker.fetchAccessToken(user: user, forcingRefresh: forceRefresh) } diff --git a/FirebaseAuth/Sources/Swift/User/User.swift b/FirebaseAuth/Sources/Swift/User/User.swift index 7f4bd0f7e08e..943057da3e27 100644 --- a/FirebaseAuth/Sources/Swift/User/User.swift +++ b/FirebaseAuth/Sources/Swift/User/User.swift @@ -198,7 +198,7 @@ extension User: NSSecureCoding {} @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) open func updatePassword(to password: String) async throws { guard password.count > 0 else { - throw AuthErrorUtils.weakPasswordError(serverResponseReason: "Missing Password") + throw AuthErrorUtils.weakPasswordError(serverResponseReason: "Missing Password") } return try await auth.authWorker.updateEmail(user: self, email: nil, password: password) } @@ -396,7 +396,7 @@ extension User: NSSecureCoding {} @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) @discardableResult open func reauthenticate(with credential: AuthCredential) async throws -> AuthDataResult { - return try await self.auth.authWorker.reauthenticate(with: credential) + return try await auth.authWorker.reauthenticate(with: credential) } #if os(iOS) @@ -1180,8 +1180,6 @@ extension User: NSSecureCoding {} // MARK: Private functions - - /// Sets a new token service for the `User` instance. /// /// The method makes sure the token service has access and refresh token and the new tokens @@ -1189,7 +1187,7 @@ extension User: NSSecureCoding {} /// - Parameter tokenService: The new token service object. /// - Parameter callback: The block to be called in the global auth working queue once finished. func setTokenService(tokenService: SecureTokenService, - callback: @escaping (Error?) -> Void) { + callback: @escaping (Error?) -> Void) { tokenService.fetchAccessToken(forcingRefresh: false) { token, error, tokenUpdated in if let error { callback(error) @@ -1211,7 +1209,7 @@ extension User: NSSecureCoding {} /// - Parameter tokenService: The new token service object. func setTokenService(tokenService: SecureTokenService) async throws { self.tokenService = tokenService - if let error = self.updateKeychain() { + if let error = updateKeychain() { throw error } } @@ -1376,9 +1374,9 @@ extension User: NSSecureCoding {} guard let auth = self.auth else { fatalError("Internal Auth error: missing auth instance on user") } - let response = try await auth.authWorker.injectRecaptcha(request: request, - action: AuthRecaptchaAction - .signUpPassword) + let response = try await auth.authWorker.injectRecaptcha(request: request, + action: AuthRecaptchaAction + .signUpPassword) #else let response = try await AuthBackend.call(with: request) #endif @@ -1500,7 +1498,6 @@ extension User: NSSecureCoding {} } } - private func link(withEmailCredential emailCredential: EmailAuthCredential, completion: ((AuthDataResult?, Error?) -> Void)?) { if hasEmailPasswordCredential { @@ -1719,15 +1716,16 @@ extension User: NSSecureCoding {} do { let (token, tokenUpdated) = try await tokenService.fetchAccessToken( user: self, - forcingRefresh: forceRefresh) + forcingRefresh: forceRefresh + ) if tokenUpdated { - if let error = self.updateKeychain() { + if let error = updateKeychain() { throw error } } return token! } catch { - self.signOutIfTokenIsInvalid(withError: error) + signOutIfTokenIsInvalid(withError: error) throw error } } diff --git a/FirebaseAuth/Tests/Unit/AuthTests.swift b/FirebaseAuth/Tests/Unit/AuthTests.swift index 68439de44cef..8be714636586 100644 --- a/FirebaseAuth/Tests/Unit/AuthTests.swift +++ b/FirebaseAuth/Tests/Unit/AuthTests.swift @@ -1921,7 +1921,7 @@ class AuthTests: RPCBaseTests { let user1 = try XCTUnwrap(auth.currentUser) let kTestAPIKey = "fakeAPIKey" user1.requestConfiguration = AuthRequestConfiguration(apiKey: kTestAPIKey, - appID: kTestFirebaseAppID) + appID: kTestFirebaseAppID) try await auth.signOut() let kTestAccessToken2 = "fakeAccessToken2" @@ -2321,25 +2321,25 @@ class AuthTests: RPCBaseTests { XCTAssertEqual(auth.tokenManagerGet().token?.type, .unknown) } - func testAppDidRegisterForRemoteNotifications_APNSTokenUpdatedAsync() async { - class FakeAuthTokenManager: AuthAPNSTokenManager { - override var token: AuthAPNSToken? { - get { - return tokenStore - } - set(setToken) { - tokenStore = setToken + func testAppDidRegisterForRemoteNotifications_APNSTokenUpdatedAsync() async { + class FakeAuthTokenManager: AuthAPNSTokenManager { + override var token: AuthAPNSToken? { + get { + return tokenStore + } + set(setToken) { + tokenStore = setToken + } } } + let apnsToken = Data() + await auth.tokenManagerInit(FakeAuthTokenManager(withApplication: UIApplication.shared)) + await auth.application(UIApplication.shared, + didRegisterForRemoteNotificationsWithDeviceToken: apnsToken) + let manager = await auth.tokenManagerGet() + XCTAssertEqual(manager.token?.data, apnsToken) + XCTAssertEqual(manager.token?.type, .unknown) } - let apnsToken = Data() - await auth.tokenManagerInit(FakeAuthTokenManager(withApplication: UIApplication.shared)) - await auth.application(UIApplication.shared, - didRegisterForRemoteNotificationsWithDeviceToken: apnsToken) - let manager = await auth.tokenManagerGet() - XCTAssertEqual(manager.token?.data, apnsToken) - XCTAssertEqual(manager.token?.type, .unknown) - } func testAppDidFailToRegisterForRemoteNotifications_TokenManagerCancels() { class FakeAuthTokenManager: AuthAPNSTokenManager { @@ -2357,21 +2357,21 @@ class AuthTests: RPCBaseTests { XCTAssertTrue(fakeTokenManager.cancelled) } - func testAppDidFailToRegisterForRemoteNotifications_TokenManagerCancelsAsync() async { - class FakeAuthTokenManager: AuthAPNSTokenManager { - var cancelled = false - override func cancel(withError error: Error) { - cancelled = true + func testAppDidFailToRegisterForRemoteNotifications_TokenManagerCancelsAsync() async { + class FakeAuthTokenManager: AuthAPNSTokenManager { + var cancelled = false + override func cancel(withError error: Error) { + cancelled = true + } } + let error = NSError(domain: "AuthTests", code: -1) + let fakeTokenManager = await FakeAuthTokenManager(withApplication: UIApplication.shared) + await auth.tokenManagerInit(fakeTokenManager) + XCTAssertFalse(fakeTokenManager.cancelled) + await auth.application(UIApplication.shared, + didFailToRegisterForRemoteNotificationsWithError: error) + XCTAssertTrue(fakeTokenManager.cancelled) } - let error = NSError(domain: "AuthTests", code: -1) - let fakeTokenManager = await FakeAuthTokenManager(withApplication: UIApplication.shared) - await auth.tokenManagerInit(fakeTokenManager) - XCTAssertFalse(fakeTokenManager.cancelled) - await auth.application(UIApplication.shared, - didFailToRegisterForRemoteNotificationsWithError: error) - XCTAssertTrue(fakeTokenManager.cancelled) - } func testAppDidReceiveRemoteNotificationWithCompletion_NotificationManagerHandleCanNotification() { class FakeNotificationManager: AuthNotificationManager { @@ -2413,22 +2413,22 @@ class AuthTests: RPCBaseTests { XCTAssertTrue(fakeURLPresenter.canHandled) } - func testAppOpenURL_AuthPresenterCanHandleURLAsync() async throws { - class FakeURLPresenter: AuthURLPresenter { - var canHandled = false - override func canHandle(url: URL) -> Bool { - canHandled = true - return true + func testAppOpenURL_AuthPresenterCanHandleURLAsync() async throws { + class FakeURLPresenter: AuthURLPresenter { + var canHandled = false + override func canHandle(url: URL) -> Bool { + canHandled = true + return true + } } + let url = try XCTUnwrap(URL(string: "https://localhost")) + let fakeURLPresenter = FakeURLPresenter() + auth.authURLPresenter = fakeURLPresenter + XCTAssertFalse(fakeURLPresenter.canHandled) + let result = await auth.application(UIApplication.shared, open: url, options: [:]) + XCTAssertTrue(result) + XCTAssertTrue(fakeURLPresenter.canHandled) } - let url = try XCTUnwrap(URL(string: "https://localhost")) - let fakeURLPresenter = FakeURLPresenter() - auth.authURLPresenter = fakeURLPresenter - XCTAssertFalse(fakeURLPresenter.canHandled) - let result = await auth.application(UIApplication.shared, open: url, options: [:]) - XCTAssertTrue(result) - XCTAssertTrue(fakeURLPresenter.canHandled) - } #endif // os(iOS) // MARK: Interoperability Tests @@ -2496,7 +2496,8 @@ class AuthTests: RPCBaseTests { assertUser(auth?.currentUser) } - private func waitForSignInWithAccessTokenAsync(fakeAccessToken: String = kAccessToken) async throws { + private func waitForSignInWithAccessTokenAsync(fakeAccessToken: String = + kAccessToken) async throws { let kRefreshToken = "fakeRefreshToken" setFakeGetAccountProvider() setFakeSecureTokenService() @@ -2525,7 +2526,7 @@ class AuthTests: RPCBaseTests { } XCTAssertEqual(user.refreshToken, kRefreshToken) XCTAssertFalse(user.isAnonymous) - XCTAssertEqual(user.email, self.kEmail) + XCTAssertEqual(user.email, kEmail) guard let additionalUserInfo = authResult?.additionalUserInfo else { XCTFail("authResult.additionalUserInfo is missing") return