From 7afc289a68a737822d50e6e736752c57d1e9d824 Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Tue, 10 Oct 2023 14:12:27 -0700 Subject: [PATCH 1/8] Add httpMethod and clean up some wrong publics --- .../Sources/Swift/Backend/AuthBackend.swift | 1 + .../Backend/AuthRequestConfiguration.swift | 41 ++++++------ .../Backend/IdentityToolkitRequest.swift | 16 ++--- .../Backend/RPC/CreateAuthURIRequest.swift | 22 +++---- .../Backend/RPC/DeleteAccountRequest.swift | 12 ++-- .../Backend/RPC/EmailLinkSignInRequest.swift | 14 ++--- .../Backend/RPC/GetAccountInfoRequest.swift | 8 +-- .../RPC/GetOOBConfirmationCodeRequest.swift | 62 ++++++++----------- .../SystemService/SecureTokenService.swift | 18 +++--- FirebaseAuth/Sources/Swift/User/User.swift | 9 +-- 10 files changed, 96 insertions(+), 107 deletions(-) diff --git a/FirebaseAuth/Sources/Swift/Backend/AuthBackend.swift b/FirebaseAuth/Sources/Swift/Backend/AuthBackend.swift index fcc2abca054..244387d1d08 100644 --- a/FirebaseAuth/Sources/Swift/Backend/AuthBackend.swift +++ b/FirebaseAuth/Sources/Swift/Backend/AuthBackend.swift @@ -121,6 +121,7 @@ class AuthBackend: NSObject { ) } #endif + request.httpMethod = requestConfiguration.httpMethod let preferredLocalizations = Bundle.main.preferredLocalizations if preferredLocalizations.count > 0 { request.setValue(preferredLocalizations.first, forHTTPHeaderField: "Accept-Language") diff --git a/FirebaseAuth/Sources/Swift/Backend/AuthRequestConfiguration.swift b/FirebaseAuth/Sources/Swift/Backend/AuthRequestConfiguration.swift index 80bf039d4f5..8e4502c8b29 100644 --- a/FirebaseAuth/Sources/Swift/Backend/AuthRequestConfiguration.swift +++ b/FirebaseAuth/Sources/Swift/Backend/AuthRequestConfiguration.swift @@ -23,55 +23,60 @@ import FirebaseAppCheckInterop @brief Defines configurations to be added to a request to Firebase Auth's backend. */ @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) -@objc(FIRAuthRequestConfiguration) public class AuthRequestConfiguration: NSObject { +class AuthRequestConfiguration: NSObject { /** @property APIKey @brief The Firebase Auth API key used in the request. */ - @objc(APIKey) public let apiKey: String + let apiKey: String /** @property LanguageCode @brief The language code used in the request. */ - @objc public var languageCode: String? + var languageCode: String? - /// ** @property appID - // @brief The Firebase appID used in the request. - // */ - @objc public var appID: String + /** @property appID + @brief The Firebase appID used in the request. + */ + let appID: String /** @property auth @brief The FIRAuth instance used in the request. */ - @objc public weak var auth: Auth? + weak var auth: Auth? /// The heartbeat logger used to add heartbeats to the corresponding request's header. - @objc public var heartbeatLogger: FIRHeartbeatLoggerProtocol? + var heartbeatLogger: FIRHeartbeatLoggerProtocol? /** @property appCheck @brief The appCheck is used to generate a token. */ - @objc public var appCheck: AppCheckInterop? + var appCheck: AppCheckInterop? + + /** @property HTTPMethod + @brief The HTTP method used in the request. + */ + var httpMethod: String /** @property additionalFrameworkMarker @brief Additional framework marker that will be added as part of the header of every request. */ - @objc public var additionalFrameworkMarker: String? + var additionalFrameworkMarker: String? /** @property emulatorHostAndPort @brief If set, the local emulator host and port to point to instead of the remote backend. */ - @objc public var emulatorHostAndPort: String? + var emulatorHostAndPort: String? - @objc(initWithAPIKey:appID:auth:heartbeatLogger:appCheck:) - public init(apiKey: String, - appID: String, - auth: Auth? = nil, - heartbeatLogger: FIRHeartbeatLoggerProtocol? = nil, - appCheck: AppCheckInterop? = nil) { + init(apiKey: String, + appID: String, + auth: Auth? = nil, + heartbeatLogger: FIRHeartbeatLoggerProtocol? = nil, + appCheck: AppCheckInterop? = nil) { self.apiKey = apiKey self.appID = appID self.auth = auth self.heartbeatLogger = heartbeatLogger self.appCheck = appCheck + httpMethod = "POST" } } diff --git a/FirebaseAuth/Sources/Swift/Backend/IdentityToolkitRequest.swift b/FirebaseAuth/Sources/Swift/Backend/IdentityToolkitRequest.swift index e992752d8d9..b9ac1f0e84f 100644 --- a/FirebaseAuth/Sources/Swift/Backend/IdentityToolkitRequest.swift +++ b/FirebaseAuth/Sources/Swift/Backend/IdentityToolkitRequest.swift @@ -32,13 +32,13 @@ private let kIdentityPlatformStagingAPIHost = @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) open class IdentityToolkitRequest { /// Gets the RPC's endpoint. - public let endpoint: String + let endpoint: String /// Gets the client's API key used for the request. - public var apiKey: String + var apiKey: String /// The tenant ID of the request. nil if none is available. - public let tenantID: String? + let tenantID: String? let _requestConfiguration: AuthRequestConfiguration @@ -46,8 +46,8 @@ open class IdentityToolkitRequest { let _useStaging: Bool - public init(endpoint: String, requestConfiguration: AuthRequestConfiguration, - useIdentityPlatform: Bool = false, useStaging: Bool = false) { + init(endpoint: String, requestConfiguration: AuthRequestConfiguration, + useIdentityPlatform: Bool = false, useStaging: Bool = false) { self.endpoint = endpoint apiKey = requestConfiguration.apiKey _requestConfiguration = requestConfiguration @@ -56,12 +56,12 @@ open class IdentityToolkitRequest { tenantID = requestConfiguration.auth?.tenantID } - public func containsPostBody() -> Bool { + func containsPostBody() -> Bool { true } /// Returns the request's full URL. - public func requestURL() -> URL { + func requestURL() -> URL { let apiProtocol: String let apiHostAndPathPrefix: String let urlString: String @@ -97,7 +97,7 @@ open class IdentityToolkitRequest { } /// Returns the request's configuration. - public func requestConfiguration() -> AuthRequestConfiguration { + func requestConfiguration() -> AuthRequestConfiguration { _requestConfiguration } diff --git a/FirebaseAuth/Sources/Swift/Backend/RPC/CreateAuthURIRequest.swift b/FirebaseAuth/Sources/Swift/Backend/RPC/CreateAuthURIRequest.swift index 74d0f924a3b..6fadfcfde04 100644 --- a/FirebaseAuth/Sources/Swift/Backend/RPC/CreateAuthURIRequest.swift +++ b/FirebaseAuth/Sources/Swift/Backend/RPC/CreateAuthURIRequest.swift @@ -64,55 +64,55 @@ private let kTenantIDKey = "tenantId" @see https://developers.google.com/identity/toolkit/web/reference/relyingparty/createAuthUri */ @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) -public class CreateAuthURIRequest: IdentityToolkitRequest, AuthRPCRequest { +class CreateAuthURIRequest: IdentityToolkitRequest, AuthRPCRequest { typealias Response = CreateAuthURIResponse /** @property identifier @brief The email or federated ID of the user. */ - public let identifier: String + let identifier: String /** @property continueURI @brief The URI to which the IDP redirects the user after the federated login flow. */ - public let continueURI: String + let continueURI: String /** @property openIDRealm @brief Optional realm for OpenID protocol. The sub string "scheme://domain:port" of the param "continueUri" is used if this is not set. */ - public var openIDRealm: String? + var openIDRealm: String? /** @property providerID @brief The IdP ID. For white listed IdPs it's a short domain name e.g. google.com, aol.com, live.net and yahoo.com. For other OpenID IdPs it's the OP identifier. */ - public var providerID: String? + var providerID: String? /** @property clientID @brief The relying party OAuth client ID. */ - public var clientID: String? + var clientID: String? /** @property context @brief The opaque value used by the client to maintain context info between the authentication request and the IDP callback. */ - public var context: String? + var context: String? /** @property appID @brief The iOS client application's bundle identifier. */ - public var appID: String? + var appID: String? - public init(identifier: String, continueURI: String, - requestConfiguration: AuthRequestConfiguration) { + init(identifier: String, continueURI: String, + requestConfiguration: AuthRequestConfiguration) { self.identifier = identifier self.continueURI = continueURI super.init(endpoint: kCreateAuthURIEndpoint, requestConfiguration: requestConfiguration) } - public func unencodedHTTPRequestBody() throws -> [String: AnyHashable] { + func unencodedHTTPRequestBody() throws -> [String: AnyHashable] { var postBody: [String: AnyHashable] = [ kIdentifierKey: identifier, kContinueURIKey: continueURI, diff --git a/FirebaseAuth/Sources/Swift/Backend/RPC/DeleteAccountRequest.swift b/FirebaseAuth/Sources/Swift/Backend/RPC/DeleteAccountRequest.swift index 8e651f73c85..bf059cb7967 100644 --- a/FirebaseAuth/Sources/Swift/Backend/RPC/DeleteAccountRequest.swift +++ b/FirebaseAuth/Sources/Swift/Backend/RPC/DeleteAccountRequest.swift @@ -31,28 +31,26 @@ private let kIDTokenKey = "idToken" private let kLocalIDKey = "localId" @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) -public class DeleteAccountRequest: IdentityToolkitRequest, AuthRPCRequest { +class DeleteAccountRequest: IdentityToolkitRequest, AuthRPCRequest { typealias Response = DeleteAccountResponse /** @var _accessToken @brief The STS Access Token of the authenticated user. */ - public let accessToken: String + let accessToken: String /** @var _localID @brief The localID of the user. */ - public let localID: String + let localID: String - @objc(initWithLocalID:accessToken:requestConfiguration:) public init(localID: String, - accessToken: String, - requestConfiguration: AuthRequestConfiguration) { + init(localID: String, accessToken: String, requestConfiguration: AuthRequestConfiguration) { self.localID = localID self.accessToken = accessToken super.init(endpoint: kDeleteAccountEndpoint, requestConfiguration: requestConfiguration) } - public func unencodedHTTPRequestBody() throws -> [String: AnyHashable] { + func unencodedHTTPRequestBody() throws -> [String: AnyHashable] { [ kIDTokenKey: accessToken, kLocalIDKey: localID, diff --git a/FirebaseAuth/Sources/Swift/Backend/RPC/EmailLinkSignInRequest.swift b/FirebaseAuth/Sources/Swift/Backend/RPC/EmailLinkSignInRequest.swift index 89a2db28393..cfbf1247a25 100644 --- a/FirebaseAuth/Sources/Swift/Backend/RPC/EmailLinkSignInRequest.swift +++ b/FirebaseAuth/Sources/Swift/Backend/RPC/EmailLinkSignInRequest.swift @@ -45,30 +45,30 @@ private let kPostBodyKey = "postBody" private let kTenantIDKey = "tenantId" @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) -public class EmailLinkSignInRequest: IdentityToolkitRequest, AuthRPCRequest { +class EmailLinkSignInRequest: IdentityToolkitRequest, AuthRPCRequest { typealias Response = EmailLinkSignInResponse - public let email: String + let email: String /** @property oobCode @brief The OOB code used to complete the email link sign-in flow. */ - public let oobCode: String + let oobCode: String /** @property IDToken @brief The ID Token code potentially used to complete the email link sign-in flow. */ - @objc(IDToken) public var idToken: String? + var idToken: String? - public init(email: String, oobCode: String, - requestConfiguration: AuthRequestConfiguration) { + init(email: String, oobCode: String, + requestConfiguration: AuthRequestConfiguration) { self.email = email self.oobCode = oobCode super.init(endpoint: kEmailLinkSigninEndpoint, requestConfiguration: requestConfiguration) } - public func unencodedHTTPRequestBody() throws -> [String: AnyHashable] { + func unencodedHTTPRequestBody() throws -> [String: AnyHashable] { var postBody: [String: AnyHashable] = [ kEmailKey: email, kOOBCodeKey: oobCode, diff --git a/FirebaseAuth/Sources/Swift/Backend/RPC/GetAccountInfoRequest.swift b/FirebaseAuth/Sources/Swift/Backend/RPC/GetAccountInfoRequest.swift index 8550c1c9b2e..9a0e19af0be 100644 --- a/FirebaseAuth/Sources/Swift/Backend/RPC/GetAccountInfoRequest.swift +++ b/FirebaseAuth/Sources/Swift/Backend/RPC/GetAccountInfoRequest.swift @@ -30,25 +30,25 @@ private let kIDTokenKey = "idToken" @see https://developers.google.com/identity/toolkit/web/reference/relyingparty/getAccountInfo */ @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) -public class GetAccountInfoRequest: IdentityToolkitRequest, AuthRPCRequest { +class GetAccountInfoRequest: IdentityToolkitRequest, AuthRPCRequest { typealias Response = GetAccountInfoResponse /** @property accessToken @brief The STS Access Token for the authenticated user. */ - public let accessToken: String + let accessToken: String /** @fn initWithAccessToken:requestConfiguration @brief Designated initializer. @param accessToken The Access Token of the authenticated user. @param requestConfiguration An object containing configurations to be added to the request. */ - public init(accessToken: String, requestConfiguration: AuthRequestConfiguration) { + init(accessToken: String, requestConfiguration: AuthRequestConfiguration) { self.accessToken = accessToken super.init(endpoint: kGetAccountInfoEndpoint, requestConfiguration: requestConfiguration) } - public func unencodedHTTPRequestBody() throws -> [String: AnyHashable] { + func unencodedHTTPRequestBody() throws -> [String: AnyHashable] { return [kIDTokenKey: accessToken] } } diff --git a/FirebaseAuth/Sources/Swift/Backend/RPC/GetOOBConfirmationCodeRequest.swift b/FirebaseAuth/Sources/Swift/Backend/RPC/GetOOBConfirmationCodeRequest.swift index 5c892f7eef4..33d1ef97ffd 100644 --- a/FirebaseAuth/Sources/Swift/Backend/RPC/GetOOBConfirmationCodeRequest.swift +++ b/FirebaseAuth/Sources/Swift/Backend/RPC/GetOOBConfirmationCodeRequest.swift @@ -135,7 +135,7 @@ private let kVerifyBeforeUpdateEmailRequestTypeValue = "VERIFY_AND_CHANGE_EMAIL" private let kTenantIDKey = "tenantId" @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) -public class GetOOBConfirmationCodeRequest: IdentityToolkitRequest, AuthRPCRequest { +class GetOOBConfirmationCodeRequest: IdentityToolkitRequest, AuthRPCRequest { typealias Response = GetOOBConfirmationCodeResponse /** @property requestType @@ -147,55 +147,55 @@ public class GetOOBConfirmationCodeRequest: IdentityToolkitRequest, AuthRPCReque @brief The email of the user. @remarks For password reset. */ - public var email: String? + var email: String? /** @property updatedEmail @brief The new email to be updated. @remarks For verifyBeforeUpdateEmail. */ - public var updatedEmail: String? + var updatedEmail: String? /** @property accessToken @brief The STS Access Token of the authenticated user. @remarks For email change. */ - public var accessToken: String? + var accessToken: String? /** @property continueURL @brief This URL represents the state/Continue URL in the form of a universal link. */ - public var continueURL: String? + var continueURL: String? /** @property iOSBundleID @brief The iOS bundle Identifier, if available. */ - public var iOSBundleID: String? + var iOSBundleID: String? /** @property androidPackageName @brief The Android package name, if available. */ - public var androidPackageName: String? + var androidPackageName: String? /** @property androidMinimumVersion @brief The minimum Android version supported, if available. */ - public var androidMinimumVersion: String? + var androidMinimumVersion: String? /** @property androidInstallIfNotAvailable @brief Indicates whether or not the Android app should be installed if not already available. */ - public var androidInstallApp: Bool + var androidInstallApp: Bool /** @property handleCodeInApp @brief Indicates whether the action code link will open the app directly or after being redirected from a Firebase owned web widget. */ - public var handleCodeInApp: Bool + var handleCodeInApp: Bool /** @property dynamicLinkDomain @brief The Firebase Dynamic Link domain used for out of band code flow. */ - public var dynamicLinkDomain: String? + var dynamicLinkDomain: String? /** @fn initWithRequestType:email:APIKey: @brief Designated initializer. @@ -231,9 +231,9 @@ public class GetOOBConfirmationCodeRequest: IdentityToolkitRequest, AuthRPCReque ) } - public static func passwordResetRequest(email: String, - actionCodeSettings: ActionCodeSettings?, - requestConfiguration: AuthRequestConfiguration) -> + static func passwordResetRequest(email: String, + actionCodeSettings: ActionCodeSettings?, + requestConfiguration: AuthRequestConfiguration) -> GetOOBConfirmationCodeRequest { Self(requestType: .passwordReset, email: email, @@ -243,9 +243,9 @@ public class GetOOBConfirmationCodeRequest: IdentityToolkitRequest, AuthRPCReque requestConfiguration: requestConfiguration) } - public static func verifyEmailRequest(accessToken: String, - actionCodeSettings: ActionCodeSettings?, - requestConfiguration: AuthRequestConfiguration) -> + static func verifyEmailRequest(accessToken: String, + actionCodeSettings: ActionCodeSettings?, + requestConfiguration: AuthRequestConfiguration) -> GetOOBConfirmationCodeRequest { Self(requestType: .verifyEmail, email: nil, @@ -255,9 +255,9 @@ public class GetOOBConfirmationCodeRequest: IdentityToolkitRequest, AuthRPCReque requestConfiguration: requestConfiguration) } - public static func signInWithEmailLinkRequest(_ email: String, - actionCodeSettings: ActionCodeSettings?, - requestConfiguration: AuthRequestConfiguration) + static func signInWithEmailLinkRequest(_ email: String, + actionCodeSettings: ActionCodeSettings?, + requestConfiguration: AuthRequestConfiguration) -> Self { Self(requestType: .emailLink, email: email, @@ -267,10 +267,10 @@ public class GetOOBConfirmationCodeRequest: IdentityToolkitRequest, AuthRPCReque requestConfiguration: requestConfiguration) } - public static func verifyBeforeUpdateEmail(accessToken: String, - newEmail: String, - actionCodeSettings: ActionCodeSettings?, - requestConfiguration: AuthRequestConfiguration) + static func verifyBeforeUpdateEmail(accessToken: String, + newEmail: String, + actionCodeSettings: ActionCodeSettings?, + requestConfiguration: AuthRequestConfiguration) -> Self { Self(requestType: .verifyBeforeUpdateEmail, email: nil, @@ -280,67 +280,55 @@ public class GetOOBConfirmationCodeRequest: IdentityToolkitRequest, AuthRPCReque requestConfiguration: requestConfiguration) } - public func unencodedHTTPRequestBody() throws -> [String: AnyHashable] { + func unencodedHTTPRequestBody() throws -> [String: AnyHashable] { var body: [String: AnyHashable] = [ kRequestTypeKey: requestType.value, ] - // For password reset requests, we only need an email address in addition to the already // required fields. if case .passwordReset = requestType { body[kEmailKey] = email } - // For verify email requests, we only need an STS Access Token in addition to the already // required fields. if case .verifyEmail = requestType { body[kIDTokenKey] = accessToken } - // For email sign-in link requests, we only need an email address in addition to the already // required fields. if case .emailLink = requestType { body[kEmailKey] = email } - // For email sign-in link requests, we only need an STS Access Token, a new email address in // addition to the already required fields. if case .verifyBeforeUpdateEmail = requestType { body[kNewEmailKey] = updatedEmail body[kIDTokenKey] = accessToken } - if let continueURL = continueURL { body[kContinueURLKey] = continueURL } - if let iOSBundleID = iOSBundleID { body[kIosBundleIDKey] = iOSBundleID } - if let androidPackageName = androidPackageName { body[kAndroidPackageNameKey] = androidPackageName } - if let androidMinimumVersion = androidMinimumVersion { body[kAndroidMinimumVersionKey] = androidMinimumVersion } - if androidInstallApp { body[kAndroidInstallAppKey] = true } - if handleCodeInApp { body[kCanHandleCodeInAppKey] = true } - if let dynamicLinkDomain = dynamicLinkDomain { body[kDynamicLinkDomainKey] = dynamicLinkDomain } if let tenantID = tenantID { body[kTenantIDKey] = tenantID } - return body } } diff --git a/FirebaseAuth/Sources/Swift/SystemService/SecureTokenService.swift b/FirebaseAuth/Sources/Swift/SystemService/SecureTokenService.swift index 4035da3eea5..4117594a59a 100644 --- a/FirebaseAuth/Sources/Swift/SystemService/SecureTokenService.swift +++ b/FirebaseAuth/Sources/Swift/SystemService/SecureTokenService.swift @@ -30,11 +30,11 @@ private let kFiveMinutes = 5 * 60.0 @brief A class represents a credential that proves the identity of the app. */ @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) -@objc(FIRSecureTokenService) public class SecureTokenService: NSObject, NSSecureCoding { +class SecureTokenService: NSObject, NSSecureCoding { /** @property requestConfiguration @brief The configuration for making requests to server. */ - @objc public var requestConfiguration: AuthRequestConfiguration? + var requestConfiguration: AuthRequestConfiguration? /** @property accessToken @brief The cached access token. @@ -42,18 +42,18 @@ private let kFiveMinutes = 5 * 60.0 deserialization and sign-in events, and should not be used to retrieve the access token by anyone else. */ - @objc public var accessToken: String + var accessToken: String /** @property refreshToken @brief The refresh token for the user, or @c nil if the user has yet completed sign-in flow. @remarks This property needs to be set manually after the instance is decoded from archive. */ - @objc public var refreshToken: String? + var refreshToken: String? /** @property accessTokenExpirationDate @brief The expiration date of the cached access token. */ - @objc public var accessTokenExpirationDate: Date? + var accessTokenExpirationDate: Date? /** @fn initWithRequestConfiguration:accessToken:accessTokenExpirationDate:refreshToken @brief Creates a @c FIRSecureTokenService with access and refresh tokens. @@ -62,10 +62,10 @@ private let kFiveMinutes = 5 * 60.0 @param accessTokenExpirationDate The approximate expiration date of the access token. @param refreshToken The STS refresh token. */ - @objc public init(withRequestConfiguration requestConfiguration: AuthRequestConfiguration?, - accessToken: String, - accessTokenExpirationDate: Date?, - refreshToken: String) { + init(withRequestConfiguration requestConfiguration: AuthRequestConfiguration?, + accessToken: String, + accessTokenExpirationDate: Date?, + refreshToken: String) { self.requestConfiguration = requestConfiguration self.accessToken = accessToken self.refreshToken = refreshToken diff --git a/FirebaseAuth/Sources/Swift/User/User.swift b/FirebaseAuth/Sources/Swift/User/User.swift index 3b0cbac3c69..3a0496978de 100644 --- a/FirebaseAuth/Sources/Swift/User/User.swift +++ b/FirebaseAuth/Sources/Swift/User/User.swift @@ -1356,21 +1356,18 @@ extension User: NSSecureCoding {} /** @property requestConfiguration @brief A strong reference to a requestConfiguration instance associated with this user instance. */ - // TODO: internal - @objc public var requestConfiguration: AuthRequestConfiguration + var requestConfiguration: AuthRequestConfiguration /** @var _tokenService @brief A secure token service associated with this user. For performing token exchanges and refreshing access tokens. */ - // TODO: internal - @objc public var tokenService: SecureTokenService + var tokenService: SecureTokenService /** @property auth @brief A weak reference to a FIRAuth instance associated with this instance. */ - // TODO: internal - @objc public weak var auth: Auth? + weak var auth: Auth? // MARK: Private functions From 8c3d41b10595d820b29a6b6adec57b17723664db Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Fri, 13 Oct 2023 16:17:12 -0700 Subject: [PATCH 2/8] Library, sample and most of unit tests --- FirebaseAuth/Sources/Swift/Auth/Auth.swift | 202 +++++++++++++++--- .../Swift/Backend/AuthRPCRequest.swift | 7 +- .../Backend/IdentityToolkitRequest.swift | 39 +++- .../RPC/GetOOBConfirmationCodeRequest.swift | 61 ++++-- .../RPC/GetRecaptchaConfigRequest.swift | 90 ++++++++ .../RPC/GetRecaptchaConfigResponse.swift | 27 +++ .../Enroll/FinalizeMFAEnrollmentRequest.swift | 6 +- .../Enroll/StartMFAEnrollmentRequest.swift | 6 +- .../SignIn/FinalizeMFASignInRequest.swift | 3 +- .../SignIn/StartMFASignInRequest.swift | 3 +- .../Unenroll/WithdrawMFARequest.swift | 4 +- .../Backend/RPC/RevokeTokenRequest.swift | 11 +- .../Backend/RPC/SignUpNewUserRequest.swift | 43 +++- .../Backend/RPC/VerifyPasswordRequest.swift | 33 ++- .../Swift/Utilities/AuthErrorUtils.swift | 187 ++++++++-------- .../Sources/Swift/Utilities/AuthErrors.swift | 117 +++++++++- .../Utilities/AuthRecaptchaVerifier.swift | 192 +++++++++++++++++ .../Models/AuthMenu.swift | 14 +- .../ViewControllers/AuthViewController.swift | 14 ++ FirebaseAuth/Tests/SampleSwift/Podfile | 2 + .../Unit/GetOOBConfirmationCodeTests.swift | 47 ++++ .../Tests/Unit/SignUpNewUserTests.swift | 32 +++ FirebaseAuth/Tests/Unit/SwiftAPI.swift | 13 ++ .../Tests/Unit/VerifyPasswordTests.swift | 10 +- 24 files changed, 985 insertions(+), 178 deletions(-) create mode 100644 FirebaseAuth/Sources/Swift/Backend/RPC/GetRecaptchaConfigRequest.swift create mode 100644 FirebaseAuth/Sources/Swift/Backend/RPC/GetRecaptchaConfigResponse.swift create mode 100644 FirebaseAuth/Sources/Swift/Utilities/AuthRecaptchaVerifier.swift diff --git a/FirebaseAuth/Sources/Swift/Auth/Auth.swift b/FirebaseAuth/Sources/Swift/Auth/Auth.swift index 01659826c2c..ece77ca8fbe 100644 --- a/FirebaseAuth/Sources/Swift/Auth/Auth.swift +++ b/FirebaseAuth/Sources/Swift/Auth/Auth.swift @@ -386,12 +386,18 @@ extension Auth: AuthInterop { if request.password.count == 0 { throw AuthErrorUtils.wrongPasswordError(message: nil) } - let response = try await AuthBackend.call(with: request) - return try await completeSignIn(withAccessToken: response.idToken, - accessTokenExpirationDate: response - .approximateExpirationDate, - refreshToken: response.refreshToken, - anonymous: false) + #if os(iOS) + let response = try await injectRecaptcha(request: request, + action: AuthRecaptchaAction.signInWithPassword) + #else + let response = try await AuthBackend.call(with: request) + #endif + return try await completeSignIn( + withAccessToken: response.idToken, + accessTokenExpirationDate: response.approximateExpirationDate, + refreshToken: response.refreshToken, + anonymous: false + ) } /** @fn signInWithEmail:password:completion: @@ -932,24 +938,51 @@ extension Auth: AuthInterop { password: password, displayName: nil, requestConfiguration: self.requestConfiguration) - Task { - do { - let response = try await AuthBackend.call(with: request) - let user = try await self.completeSignIn( - withAccessToken: response.idToken, - accessTokenExpirationDate: response.approximateExpirationDate, - refreshToken: response.refreshToken, - anonymous: false - ) - let additionalUserInfo = AdditionalUserInfo(providerID: EmailAuthProvider.id, - profile: nil, - username: nil, - isNewUser: true) - decoratedCallback(AuthDataResult(withUser: user, additionalUserInfo: additionalUserInfo), - nil) - } catch { - decoratedCallback(nil, error) + + #if os(iOS) + self.wrapInjectRecaptcha(request: request, + action: AuthRecaptchaAction.signUpPassword) { response, error in + if let error { + DispatchQueue.main.async { + decoratedCallback(nil, error) + } + return + } + self.internalCreateUserWithEmail(request: request, inResponse: response, + decoratedCallback: decoratedCallback) + } + #else + self.internalCreateUserWithEmail(request: request, decoratedCallback: decoratedCallback) + #endif + } + } + + func internalCreateUserWithEmail(request: SignUpNewUserRequest, + inResponse: SignUpNewUserResponse? = nil, + decoratedCallback: @escaping (AuthDataResult?, Error?) -> Void) { + Task { + do { + var response: SignUpNewUserResponse + if let inResponse { + response = inResponse + } else { + response = try await AuthBackend.call(with: request) } + let user = try await self.completeSignIn( + withAccessToken: response.idToken, + accessTokenExpirationDate: response.approximateExpirationDate, + refreshToken: response.refreshToken, + anonymous: false + ) + let additionalUserInfo = AdditionalUserInfo(providerID: EmailAuthProvider.id, + profile: nil, + username: nil, + isNewUser: true) + decoratedCallback(AuthDataResult(withUser: user, + additionalUserInfo: additionalUserInfo), + nil) + } catch { + decoratedCallback(nil, error) } } } @@ -1240,7 +1273,18 @@ extension Auth: AuthInterop { actionCodeSettings: actionCodeSettings, requestConfiguration: self.requestConfiguration ) - self.wrapAsyncRPCTask(request, completion) + #if os(iOS) + self.wrapInjectRecaptcha(request: request, + action: AuthRecaptchaAction.signInWithPassword) { result, error in + if let completion { + DispatchQueue.main.async { + completion(error) + } + } + } + #else + self.wrapAsyncRPCTask(request, completion) + #endif } } @@ -1297,13 +1341,28 @@ extension Auth: AuthInterop { @objc public func sendSignInLink(toEmail email: String, actionCodeSettings: ActionCodeSettings, completion: ((Error?) -> Void)? = nil) { + if !actionCodeSettings.handleCodeInApp { + fatalError("The handleCodeInApp flag in ActionCodeSettings must be true for Email-link " + + "Authentication.") + } kAuthGlobalWorkQueue.async { let request = GetOOBConfirmationCodeRequest.signInWithEmailLinkRequest( email, actionCodeSettings: actionCodeSettings, requestConfiguration: self.requestConfiguration ) - self.wrapAsyncRPCTask(request, completion) + #if os(iOS) + self.wrapInjectRecaptcha(request: request, + action: AuthRecaptchaAction.getOobCode) { result, error in + if let completion { + DispatchQueue.main.async { + completion(error) + } + } + } + #else + self.wrapAsyncRPCTask(request, completion) + #endif } } @@ -1372,6 +1431,43 @@ extension Auth: AuthInterop { return false } + /** @fn initializeRecaptchaConfigWithCompletion:completion: + @brief Initializes reCAPTCHA using the settings configured for the project or + tenant. + + If you change the tenant ID of the `Auth` instance, the configuration will be + reloaded. + */ + @objc(initializeRecaptchaConfigWithCompletion:) + public func initializeRecaptchaConfig(completion: ((Error?) -> Void)?) { + Task { + do { + try await initializeRecaptchaConfig() + if let completion { + completion(nil) + } + } catch { + if let completion { + completion(error) + } + } + } + } + + /** @fn initializeRecaptchaConfig + @brief Initializes reCAPTCHA using the settings configured for the project or + tenant. + + If you change the tenant ID of the `Auth` instance, the configuration will be + reloaded. + */ + public func initializeRecaptchaConfig() async throws { + // Trigger recaptcha verification flow to initialize the recaptcha client and + // config. Recaptcha token will be returned. + let verifier = AuthRecaptchaVerifier.shared(auth: self) + let _ = try await verifier.verify(forceRefresh: true, action: AuthRecaptchaAction.defaultAction) + } + /** @fn addAuthStateDidChangeListener: @brief Registers a block as an "auth state did change" listener. To be invoked when: @@ -2260,9 +2356,8 @@ extension Auth: AuthInterop { } /** @fn signInFlowAuthDataResultCallbackByDecoratingCallback: - @brief Creates a FIRAuthDataResultCallback block which wraps another FIRAuthDataResultCallback; - trying to update the current user before forwarding it's invocations along to a subject - block. + @brief Creates a AuthDataResultCallback block which wraps another AuthDataResultCallback; + trying to update the current user before forwarding it's invocations along to a subject block. @param callback Called when the user has been updated or when an error has occurred. Invoked asynchronously on the main thread in the future. @return Returns a block that updates the current user. @@ -2319,6 +2414,57 @@ extension Auth: AuthInterop { } } + #if os(iOS) + + private func wrapInjectRecaptcha(request: T, + action: AuthRecaptchaAction, + _ callback: @escaping ( + (T.Response?, Error?) -> Void + )) { + Task { + do { + let response = try await injectRecaptcha(request: request, action: action) + callback(response, nil) + } catch { + callback(nil, error) + } + } + } + + private func injectRecaptcha(request: T, + action: AuthRecaptchaAction) async throws -> T + .Response { + let recaptchaVerifier = AuthRecaptchaVerifier.shared(auth: self) + 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: AuthRecaptchaAction + .signInWithPassword) + } else { + throw error + } + } + } + return try await AuthBackend.call(with: request) + } + #endif + // MARK: Internal properties /** @property mainBundle diff --git a/FirebaseAuth/Sources/Swift/Backend/AuthRPCRequest.swift b/FirebaseAuth/Sources/Swift/Backend/AuthRPCRequest.swift index c61ae23da8b..34f40d82f88 100644 --- a/FirebaseAuth/Sources/Swift/Backend/AuthRPCRequest.swift +++ b/FirebaseAuth/Sources/Swift/Backend/AuthRPCRequest.swift @@ -33,11 +33,16 @@ protocol AuthRPCRequest { /// The request configuration. func requestConfiguration() -> AuthRequestConfiguration + + func injectRecaptchaFields(recaptchaResponse: String?, recaptchaVersion: String) } // Default implementation of AuthRPCRequests. This produces similar behaviour to an optional method // in Obj-C. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) extension AuthRPCRequest { - public var containsPostBody: Bool { return true } + var containsPostBody: Bool { return true } + func injectRecaptchaFields(recaptchaResponse: String?, recaptchaVersion: String) { + fatalError("Internal FirebaseAuth Error: unimplemented injectRecaptchaFields") + } } diff --git a/FirebaseAuth/Sources/Swift/Backend/IdentityToolkitRequest.swift b/FirebaseAuth/Sources/Swift/Backend/IdentityToolkitRequest.swift index b9ac1f0e84f..b31292c807e 100644 --- a/FirebaseAuth/Sources/Swift/Backend/IdentityToolkitRequest.swift +++ b/FirebaseAuth/Sources/Swift/Backend/IdentityToolkitRequest.swift @@ -30,7 +30,7 @@ private let kIdentityPlatformStagingAPIHost = /// Represents a request to an identity toolkit endpoint. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) -open class IdentityToolkitRequest { +class IdentityToolkitRequest { /// Gets the RPC's endpoint. let endpoint: String @@ -40,19 +40,31 @@ open class IdentityToolkitRequest { /// The tenant ID of the request. nil if none is available. let tenantID: String? - let _requestConfiguration: AuthRequestConfiguration + /** @property useIdentityPlatform + @brief The toggle of using Identity Platform endpoints. + */ + let useIdentityPlatform: Bool - let _useIdentityPlatform: Bool + /** @property useStaging + @brief The toggle of using staging endpoints. + */ + let useStaging: Bool - let _useStaging: Bool + /** @property clientType + @brief The type of the client that the request sent from, which should be CLIENT_TYPE_IOS; + */ + var clientType: String + + private let _requestConfiguration: AuthRequestConfiguration init(endpoint: String, requestConfiguration: AuthRequestConfiguration, useIdentityPlatform: Bool = false, useStaging: Bool = false) { self.endpoint = endpoint apiKey = requestConfiguration.apiKey _requestConfiguration = requestConfiguration - _useIdentityPlatform = useIdentityPlatform - _useStaging = useStaging + self.useIdentityPlatform = useIdentityPlatform + self.useStaging = useStaging + clientType = "CLIENT_TYPE_IOS" tenantID = requestConfiguration.auth?.tenantID } @@ -60,17 +72,21 @@ open class IdentityToolkitRequest { true } + func queryParams() -> String { + return "" + } + /// Returns the request's full URL. func requestURL() -> URL { let apiProtocol: String let apiHostAndPathPrefix: String let urlString: String let emulatorHostAndPort = _requestConfiguration.emulatorHostAndPort - if _useIdentityPlatform { + if useIdentityPlatform { if let emulatorHostAndPort = emulatorHostAndPort { apiProtocol = kHttpProtocol apiHostAndPathPrefix = "\(emulatorHostAndPort)/\(kIdentityPlatformAPIHost)" - } else if _useStaging { + } else if useStaging { apiHostAndPathPrefix = kIdentityPlatformStagingAPIHost apiProtocol = kHttpsProtocol } else { @@ -83,7 +99,7 @@ open class IdentityToolkitRequest { if let emulatorHostAndPort = emulatorHostAndPort { apiProtocol = kHttpProtocol apiHostAndPathPrefix = "\(emulatorHostAndPort)/\(kFirebaseAuthAPIHost)" - } else if _useStaging { + } else if useStaging { apiProtocol = kHttpsProtocol apiHostAndPathPrefix = kFirebaseAuthStagingAPIHost } else { @@ -93,7 +109,10 @@ open class IdentityToolkitRequest { urlString = "\(apiProtocol)//\(apiHostAndPathPrefix)/identitytoolkit/v3/relyingparty/\(endpoint)?key=\(apiKey)" } - return URL(string: urlString)! + guard let returnURL = URL(string: "\(urlString)\(queryParams())") else { + fatalError("Internal Auth error: Failed to generate URL for \(urlString)") + } + return returnURL } /// Returns the request's configuration. diff --git a/FirebaseAuth/Sources/Swift/Backend/RPC/GetOOBConfirmationCodeRequest.swift b/FirebaseAuth/Sources/Swift/Backend/RPC/GetOOBConfirmationCodeRequest.swift index 33d1ef97ffd..7b33352e102 100644 --- a/FirebaseAuth/Sources/Swift/Backend/RPC/GetOOBConfirmationCodeRequest.swift +++ b/FirebaseAuth/Sources/Swift/Backend/RPC/GetOOBConfirmationCodeRequest.swift @@ -134,6 +134,21 @@ private let kVerifyBeforeUpdateEmailRequestTypeValue = "VERIFY_AND_CHANGE_EMAIL" */ private let kTenantIDKey = "tenantId" +/** @var kCaptchaResponseKey + @brief The key for the "captchaResponse" value in the request. + */ +private let kCaptchaResponseKey = "captchaResp" + +/** @var kClientType + @brief The key for the "clientType" value in the request. + */ +private let kClientType = "clientType" + +/** @var kRecaptchaVersion + @brief The key for the "recaptchaVersion" value in the request. + */ +private let kRecaptchaVersion = "recaptchaVersion" + @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) class GetOOBConfirmationCodeRequest: IdentityToolkitRequest, AuthRPCRequest { typealias Response = GetOOBConfirmationCodeResponse @@ -147,55 +162,65 @@ class GetOOBConfirmationCodeRequest: IdentityToolkitRequest, AuthRPCRequest { @brief The email of the user. @remarks For password reset. */ - var email: String? + private(set) var email: String? /** @property updatedEmail @brief The new email to be updated. @remarks For verifyBeforeUpdateEmail. */ - var updatedEmail: String? + private(set) var updatedEmail: String? /** @property accessToken @brief The STS Access Token of the authenticated user. @remarks For email change. */ - var accessToken: String? + private(set) var accessToken: String? /** @property continueURL @brief This URL represents the state/Continue URL in the form of a universal link. */ - var continueURL: String? + private(set) var continueURL: String? /** @property iOSBundleID @brief The iOS bundle Identifier, if available. */ - var iOSBundleID: String? + private(set) var iOSBundleID: String? /** @property androidPackageName @brief The Android package name, if available. */ - var androidPackageName: String? + private(set) var androidPackageName: String? /** @property androidMinimumVersion @brief The minimum Android version supported, if available. */ - var androidMinimumVersion: String? + private(set) var androidMinimumVersion: String? /** @property androidInstallIfNotAvailable @brief Indicates whether or not the Android app should be installed if not already available. */ - var androidInstallApp: Bool + private(set) var androidInstallApp: Bool /** @property handleCodeInApp @brief Indicates whether the action code link will open the app directly or after being redirected from a Firebase owned web widget. */ - var handleCodeInApp: Bool + private(set) var handleCodeInApp: Bool /** @property dynamicLinkDomain @brief The Firebase Dynamic Link domain used for out of band code flow. */ - var dynamicLinkDomain: String? + private(set) var dynamicLinkDomain: String? + + /** @property captchaResponse + @brief Response to the captcha. + */ + var captchaResponse: String? + + /** @property captchaResponse + @brief The reCAPTCHA version. + */ + var recaptchaVersion: String? /** @fn initWithRequestType:email:APIKey: @brief Designated initializer. @@ -323,12 +348,24 @@ class GetOOBConfirmationCodeRequest: IdentityToolkitRequest, AuthRPCRequest { if handleCodeInApp { body[kCanHandleCodeInAppKey] = true } - if let dynamicLinkDomain = dynamicLinkDomain { + if let dynamicLinkDomain { body[kDynamicLinkDomainKey] = dynamicLinkDomain } - if let tenantID = tenantID { + if let captchaResponse { + body[kCaptchaResponseKey] = captchaResponse + } + body[kClientType] = clientType + if let recaptchaVersion { + body[kRecaptchaVersion] = recaptchaVersion + } + if let tenantID { body[kTenantIDKey] = tenantID } return body } + + func injectRecaptchaFields(recaptchaResponse: String?, recaptchaVersion: String) { + captchaResponse = recaptchaResponse + self.recaptchaVersion = recaptchaVersion + } } diff --git a/FirebaseAuth/Sources/Swift/Backend/RPC/GetRecaptchaConfigRequest.swift b/FirebaseAuth/Sources/Swift/Backend/RPC/GetRecaptchaConfigRequest.swift new file mode 100644 index 00000000000..4b57f74d315 --- /dev/null +++ b/FirebaseAuth/Sources/Swift/Backend/RPC/GetRecaptchaConfigRequest.swift @@ -0,0 +1,90 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import Foundation + +private let kRecaptchaVersion = "RECAPTCHA_ENTERPRISE" + +private let kGetOobConfirmationCodeEndpoint = "getOobConfirmationCode" + +/** @var kRequestTypeKey + @brief The name of the required "requestType" property in the request. + */ +private let kRequestTypeKey = "requestType" + +/** @var kEmailKey + @brief The name of the "email" property in the request. + */ +private let kEmailKey = "email" + +/** @var kNewEmailKey + @brief The name of the "newEmail" property in the request. + */ +private let kNewEmailKey = "newEmail" + +/** @var kIDTokenKey + @brief The key for the "idToken" value in the request. This is actually the STS Access Token, + despite it's confusing (backwards compatiable) parameter name. + */ +private let kIDTokenKey = "idToken" + +/** @var kGetRecaptchaConfigEndpoint + @brief The "getRecaptchaConfig" endpoint. + */ +private let kGetRecaptchaConfigEndpoint = "recaptchaConfig" + +/** @var kClientType + @brief The key for the "clientType" value in the request. + */ +private let kClientTypeKey = "clientType" + +/** @var kVersionKey + @brief The key for the "version" value in the request. + */ +private let kVersionKey = "version" + +/** @var kTenantIDKey + @brief The key for the tenant id value in the request. + */ +private let kTenantIDKey = "tenantId" + +@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) +class GetRecaptchaConfigRequest: IdentityToolkitRequest, AuthRPCRequest { + typealias Response = GetRecaptchaConfigResponse + + required init(requestConfiguration: AuthRequestConfiguration) { + requestConfiguration.httpMethod = "GET" + super.init( + endpoint: kGetRecaptchaConfigEndpoint, + requestConfiguration: requestConfiguration, + useIdentityPlatform: true + ) + } + + func unencodedHTTPRequestBody() throws -> [String: AnyHashable] { + return [:] + } + + override func containsPostBody() -> Bool { + false + } + + override func queryParams() -> String { + var queryParams = "&\(kClientTypeKey)=\(clientType)&\(kVersionKey)=\(kRecaptchaVersion)" + if let tenantID { + queryParams += "&\(kTenantIDKey)=\(tenantID)" + } + return queryParams + } +} diff --git a/FirebaseAuth/Sources/Swift/Backend/RPC/GetRecaptchaConfigResponse.swift b/FirebaseAuth/Sources/Swift/Backend/RPC/GetRecaptchaConfigResponse.swift new file mode 100644 index 00000000000..0d80a135613 --- /dev/null +++ b/FirebaseAuth/Sources/Swift/Backend/RPC/GetRecaptchaConfigResponse.swift @@ -0,0 +1,27 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import Foundation + +class GetRecaptchaConfigResponse: AuthRPCResponse { + required init() {} + + private(set) var recaptchaKey: String? + private(set) var enforcementState: [[String: String]]? + + func setFields(dictionary: [String: AnyHashable]) throws { + recaptchaKey = dictionary["recaptchaKey"] as? String + enforcementState = dictionary["enforcementState"] as? [[String: String]] + } +} diff --git a/FirebaseAuth/Sources/Swift/Backend/RPC/MultiFactor/Enroll/FinalizeMFAEnrollmentRequest.swift b/FirebaseAuth/Sources/Swift/Backend/RPC/MultiFactor/Enroll/FinalizeMFAEnrollmentRequest.swift index 0e53a7ecd27..658c8828887 100644 --- a/FirebaseAuth/Sources/Swift/Backend/RPC/MultiFactor/Enroll/FinalizeMFAEnrollmentRequest.swift +++ b/FirebaseAuth/Sources/Swift/Backend/RPC/MultiFactor/Enroll/FinalizeMFAEnrollmentRequest.swift @@ -42,8 +42,7 @@ class FinalizeMFAEnrollmentRequest: IdentityToolkitRequest, AuthRPCRequest { super.init( endpoint: kFinalizeMFAEnrollmentEndPoint, requestConfiguration: requestConfiguration, - useIdentityPlatform: true, - useStaging: false + useIdentityPlatform: true ) } @@ -56,8 +55,7 @@ class FinalizeMFAEnrollmentRequest: IdentityToolkitRequest, AuthRPCRequest { super.init( endpoint: kFinalizeMFAEnrollmentEndPoint, requestConfiguration: requestConfiguration, - useIdentityPlatform: true, - useStaging: false + useIdentityPlatform: true ) } diff --git a/FirebaseAuth/Sources/Swift/Backend/RPC/MultiFactor/Enroll/StartMFAEnrollmentRequest.swift b/FirebaseAuth/Sources/Swift/Backend/RPC/MultiFactor/Enroll/StartMFAEnrollmentRequest.swift index e9260ea2f36..ffaf18f6e97 100644 --- a/FirebaseAuth/Sources/Swift/Backend/RPC/MultiFactor/Enroll/StartMFAEnrollmentRequest.swift +++ b/FirebaseAuth/Sources/Swift/Backend/RPC/MultiFactor/Enroll/StartMFAEnrollmentRequest.swift @@ -37,8 +37,7 @@ class StartMFAEnrollmentRequest: IdentityToolkitRequest, AuthRPCRequest { super.init( endpoint: kStartMFAEnrollmentEndPoint, requestConfiguration: requestConfiguration, - useIdentityPlatform: true, - useStaging: false + useIdentityPlatform: true ) } @@ -50,8 +49,7 @@ class StartMFAEnrollmentRequest: IdentityToolkitRequest, AuthRPCRequest { super.init( endpoint: kStartMFAEnrollmentEndPoint, requestConfiguration: requestConfiguration, - useIdentityPlatform: true, - useStaging: false + useIdentityPlatform: true ) } diff --git a/FirebaseAuth/Sources/Swift/Backend/RPC/MultiFactor/SignIn/FinalizeMFASignInRequest.swift b/FirebaseAuth/Sources/Swift/Backend/RPC/MultiFactor/SignIn/FinalizeMFASignInRequest.swift index 9f0736fb40e..8c94ea5dc9d 100644 --- a/FirebaseAuth/Sources/Swift/Backend/RPC/MultiFactor/SignIn/FinalizeMFASignInRequest.swift +++ b/FirebaseAuth/Sources/Swift/Backend/RPC/MultiFactor/SignIn/FinalizeMFASignInRequest.swift @@ -35,8 +35,7 @@ class FinalizeMFASignInRequest: IdentityToolkitRequest, AuthRPCRequest { self.verificationInfo = verificationInfo super.init(endpoint: kFinalizeMFASignInEndPoint, requestConfiguration: requestConfiguration, - useIdentityPlatform: true, - useStaging: false) + useIdentityPlatform: true) } func unencodedHTTPRequestBody() throws -> [String: AnyHashable] { diff --git a/FirebaseAuth/Sources/Swift/Backend/RPC/MultiFactor/SignIn/StartMFASignInRequest.swift b/FirebaseAuth/Sources/Swift/Backend/RPC/MultiFactor/SignIn/StartMFASignInRequest.swift index 0e546bdc0e9..977901fc563 100644 --- a/FirebaseAuth/Sources/Swift/Backend/RPC/MultiFactor/SignIn/StartMFASignInRequest.swift +++ b/FirebaseAuth/Sources/Swift/Backend/RPC/MultiFactor/SignIn/StartMFASignInRequest.swift @@ -38,8 +38,7 @@ class StartMFASignInRequest: IdentityToolkitRequest, AuthRPCRequest { super.init( endpoint: kStartMFASignInEndPoint, requestConfiguration: requestConfiguration, - useIdentityPlatform: true, - useStaging: false + useIdentityPlatform: true ) } diff --git a/FirebaseAuth/Sources/Swift/Backend/RPC/MultiFactor/Unenroll/WithdrawMFARequest.swift b/FirebaseAuth/Sources/Swift/Backend/RPC/MultiFactor/Unenroll/WithdrawMFARequest.swift index 5b709797bea..240f909e262 100644 --- a/FirebaseAuth/Sources/Swift/Backend/RPC/MultiFactor/Unenroll/WithdrawMFARequest.swift +++ b/FirebaseAuth/Sources/Swift/Backend/RPC/MultiFactor/Unenroll/WithdrawMFARequest.swift @@ -33,7 +33,9 @@ class WithdrawMFARequest: IdentityToolkitRequest, AuthRPCRequest { requestConfiguration: AuthRequestConfiguration) { self.idToken = idToken self.mfaEnrollmentID = mfaEnrollmentID - super.init(endpoint: kWithdrawMFAEndPoint, requestConfiguration: requestConfiguration) + super.init(endpoint: kWithdrawMFAEndPoint, + requestConfiguration: requestConfiguration, + useIdentityPlatform: true) } func unencodedHTTPRequestBody() throws -> [String: AnyHashable] { diff --git a/FirebaseAuth/Sources/Swift/Backend/RPC/RevokeTokenRequest.swift b/FirebaseAuth/Sources/Swift/Backend/RPC/RevokeTokenRequest.swift index 75828f38b65..c50151fc93c 100644 --- a/FirebaseAuth/Sources/Swift/Backend/RPC/RevokeTokenRequest.swift +++ b/FirebaseAuth/Sources/Swift/Backend/RPC/RevokeTokenRequest.swift @@ -50,22 +50,22 @@ class RevokeTokenRequest: IdentityToolkitRequest, AuthRPCRequest { /** @property providerID @brief The provider that issued the token to revoke. */ - var providerID: String + private(set) var providerID: String /** @property tokenType @brief The type of the token to revoke. */ - var tokenType: TokenType + private(set) var tokenType: TokenType /** @property token @brief The token to be revoked. */ - var token: String + private(set) var token: String /** @property idToken @brief The ID Token associated with this credential. */ - var idToken: String + private(set) var idToken: String enum TokenType: Int { case unspecified = 0, refreshToken = 1, accessToken = 2, authorizationCode = 3 @@ -87,8 +87,7 @@ class RevokeTokenRequest: IdentityToolkitRequest, AuthRPCRequest { self.idToken = idToken super.init(endpoint: kRevokeTokenEndpoint, requestConfiguration: requestConfiguration, - useIdentityPlatform: true, - useStaging: false) + useIdentityPlatform: true) } func unencodedHTTPRequestBody() throws -> [String: AnyHashable] { diff --git a/FirebaseAuth/Sources/Swift/Backend/RPC/SignUpNewUserRequest.swift b/FirebaseAuth/Sources/Swift/Backend/RPC/SignUpNewUserRequest.swift index 139669503e4..00a02368731 100644 --- a/FirebaseAuth/Sources/Swift/Backend/RPC/SignUpNewUserRequest.swift +++ b/FirebaseAuth/Sources/Swift/Backend/RPC/SignUpNewUserRequest.swift @@ -34,6 +34,21 @@ private let kPasswordKey = "password" */ private let kDisplayNameKey = "displayName" +/** @var kCaptchaResponseKey + @brief The key for the "captchaResponse" value in the request. + */ +private let kCaptchaResponseKey = "captchaResponse" + +/** @var kClientType + @brief The key for the "clientType" value in the request. + */ +private let kClientType = "clientType" + +/** @var kRecaptchaVersion + @brief The key for the "recaptchaVersion" value in the request. + */ +private let kRecaptchaVersion = "recaptchaVersion" + /** @var kReturnSecureTokenKey @brief The key for the "returnSecureToken" value in the request. */ @@ -51,17 +66,27 @@ class SignUpNewUserRequest: IdentityToolkitRequest, AuthRPCRequest { /** @property email @brief The email of the user. */ - var email: String? + private(set) var email: String? /** @property password @brief The password inputed by the user. */ - var password: String? + private(set) var password: String? /** @property displayName @brief The password inputed by the user. */ - var displayName: String? + private(set) var displayName: String? + /** @property captchaResponse + @brief Response to the captcha. + */ + + var captchaResponse: String? + + /** @property captchaResponse + @brief The reCAPTCHA version. + */ + var recaptchaVersion: String? /** @property returnSecureToken @brief Whether the response should return access token and refresh token directly. @@ -98,6 +123,13 @@ class SignUpNewUserRequest: IdentityToolkitRequest, AuthRPCRequest { if let displayName { postBody[kDisplayNameKey] = displayName } + if let captchaResponse { + postBody[kCaptchaResponseKey] = captchaResponse + } + postBody[kClientType] = clientType + if let recaptchaVersion { + postBody[kRecaptchaVersion] = recaptchaVersion + } if returnSecureToken { postBody[kReturnSecureTokenKey] = true } @@ -106,4 +138,9 @@ class SignUpNewUserRequest: IdentityToolkitRequest, AuthRPCRequest { } return postBody } + + func injectRecaptchaFields(recaptchaResponse: String?, recaptchaVersion: String) { + captchaResponse = recaptchaResponse + self.recaptchaVersion = recaptchaVersion + } } diff --git a/FirebaseAuth/Sources/Swift/Backend/RPC/VerifyPasswordRequest.swift b/FirebaseAuth/Sources/Swift/Backend/RPC/VerifyPasswordRequest.swift index 56d3e5c8975..757a1e2f1d0 100644 --- a/FirebaseAuth/Sources/Swift/Backend/RPC/VerifyPasswordRequest.swift +++ b/FirebaseAuth/Sources/Swift/Backend/RPC/VerifyPasswordRequest.swift @@ -44,6 +44,16 @@ private let kCaptchaChallengeKey = "captchaChallenge" */ private let kCaptchaResponseKey = "captchaResponse" +/** @var kClientType + @brief The key for the "clientType" value in the request. + */ +private let kClientType = "clientType" + +/** @var kRecaptchaVersion + @brief The key for the "recaptchaVersion" value in the request. + */ +private let kRecaptchaVersion = "recaptchaVersion" + /** @var kReturnSecureTokenKey @brief The key for the "returnSecureToken" value in the request. */ @@ -65,12 +75,12 @@ class VerifyPasswordRequest: IdentityToolkitRequest, AuthRPCRequest { /** @property email @brief The email of the user. */ - var email: String + private(set) var email: String /** @property password @brief The password inputed by the user. */ - var password: String + private(set) var password: String /** @property pendingIDToken @brief The GITKit token for the non-trusted IDP, which is to be confirmed by the user. @@ -87,11 +97,16 @@ class VerifyPasswordRequest: IdentityToolkitRequest, AuthRPCRequest { */ var captchaResponse: String? + /** @property captchaResponse + @brief The reCAPTCHA version. + */ + var recaptchaVersion: String? + /** @property returnSecureToken @brief Whether the response should return access token and refresh token directly. @remarks The default value is @c YES . */ - var returnSecureToken: Bool + private(set) var returnSecureToken: Bool init(email: String, password: String, requestConfiguration: AuthRequestConfiguration) { @@ -106,21 +121,25 @@ class VerifyPasswordRequest: IdentityToolkitRequest, AuthRPCRequest { kEmailKey: email, kPasswordKey: password, ] - if let pendingIDToken = pendingIDToken { + if let pendingIDToken { body[kPendingIDTokenKey] = pendingIDToken } - if let captchaChallenge = captchaChallenge { + if let captchaChallenge { body[kCaptchaChallengeKey] = captchaChallenge } - if let captchaResponse = captchaResponse { + if let captchaResponse { body[kCaptchaResponseKey] = captchaResponse } + if let recaptchaVersion { + body[kRecaptchaVersion] = recaptchaVersion + } if returnSecureToken { body[kReturnSecureTokenKey] = true } - if let tenantID = tenantID { + if let tenantID { body[kTenantIDKey] = tenantID } + body[kClientType] = clientType return body } } diff --git a/FirebaseAuth/Sources/Swift/Utilities/AuthErrorUtils.swift b/FirebaseAuth/Sources/Swift/Utilities/AuthErrorUtils.swift index 5a210a26e60..2a17b8fe87d 100644 --- a/FirebaseAuth/Sources/Swift/Utilities/AuthErrorUtils.swift +++ b/FirebaseAuth/Sources/Swift/Utilities/AuthErrorUtils.swift @@ -37,7 +37,7 @@ private let kFIRAuthErrorMessageMalformedJWT = "Failed to parse JWT. Check the userInfo dictionary for the full token." @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) -@objc(FIRAuthErrorUtils) public class AuthErrorUtils: NSObject { +class AuthErrorUtils: NSObject { static let errorDomain = "FIRAuthErrorDomain" static let internalErrorDomain = "FIRAuthInternalErrorDomain" static let userInfoDeserializedResponseKey = "FIRAuthErrorUserInfoDeserializedResponseKey" @@ -94,11 +94,11 @@ private let kFIRAuthErrorMessageMalformedJWT = error(code: SharedErrorCode.public(code), underlyingError: underlyingError) } - @objc public static func error(code: AuthErrorCode, userInfo: [String: Any]? = nil) -> Error { + static func error(code: AuthErrorCode, userInfo: [String: Any]? = nil) -> Error { error(code: SharedErrorCode.public(code), userInfo: userInfo) } - @objc public static func error(code: AuthErrorCode, message: String?) -> Error { + static func error(code: AuthErrorCode, message: String?) -> Error { let userInfo: [String: Any]? if let message { userInfo = [NSLocalizedDescriptionKey: message] @@ -108,71 +108,71 @@ private let kFIRAuthErrorMessageMalformedJWT = return error(code: SharedErrorCode.public(code), userInfo: userInfo) } - @objc public static func userDisabledError(message: String?) -> Error { + static func userDisabledError(message: String?) -> Error { error(code: .userDisabled, message: message) } - @objc public static func wrongPasswordError(message: String?) -> Error { + static func wrongPasswordError(message: String?) -> Error { error(code: .wrongPassword, message: message) } - @objc public static func tooManyRequestsError(message: String?) -> Error { + static func tooManyRequestsError(message: String?) -> Error { error(code: .tooManyRequests, message: message) } - @objc public static func invalidCustomTokenError(message: String?) -> Error { + static func invalidCustomTokenError(message: String?) -> Error { error(code: .invalidCustomToken, message: message) } - @objc public static func customTokenMismatchError(message: String?) -> Error { + static func customTokenMismatchError(message: String?) -> Error { error(code: .customTokenMismatch, message: message) } - @objc public static func invalidCredentialError(message: String?) -> Error { + static func invalidCredentialError(message: String?) -> Error { error(code: .invalidCredential, message: message) } - @objc public static func requiresRecentLoginError(message: String?) -> Error { + static func requiresRecentLoginError(message: String?) -> Error { error(code: .requiresRecentLogin, message: message) } - @objc public static func invalidUserTokenError(message: String?) -> Error { + static func invalidUserTokenError(message: String?) -> Error { error(code: .invalidUserToken, message: message) } - @objc public static func invalidEmailError(message: String?) -> Error { + static func invalidEmailError(message: String?) -> Error { error(code: .invalidEmail, message: message) } - @objc public static func providerAlreadyLinkedError() -> Error { + static func providerAlreadyLinkedError() -> Error { error(code: .providerAlreadyLinked) } - @objc public static func noSuchProviderError() -> Error { + static func noSuchProviderError() -> Error { error(code: .noSuchProvider) } - @objc public static func userTokenExpiredError(message: String?) -> Error { + static func userTokenExpiredError(message: String?) -> Error { error(code: .userTokenExpired, message: message) } - @objc public static func userNotFoundError(message: String?) -> Error { + static func userNotFoundError(message: String?) -> Error { error(code: .userNotFound, message: message) } - @objc public static func invalidAPIKeyError() -> Error { + static func invalidAPIKeyError() -> Error { error(code: .invalidAPIKey) } - @objc public static func userMismatchError() -> Error { + static func userMismatchError() -> Error { error(code: .userMismatch) } - @objc public static func operationNotAllowedError(message: String?) -> Error { + static func operationNotAllowedError(message: String?) -> Error { error(code: .operationNotAllowed, message: message) } - @objc public static func weakPasswordError(serverResponseReason reason: String?) -> Error { + static func weakPasswordError(serverResponseReason reason: String?) -> Error { let userInfo: [String: Any]? if let reason, !reason.isEmpty { userInfo = [ @@ -184,123 +184,123 @@ private let kFIRAuthErrorMessageMalformedJWT = return error(code: .weakPassword, userInfo: userInfo) } - @objc public static func appNotAuthorizedError() -> Error { + static func appNotAuthorizedError() -> Error { error(code: .appNotAuthorized) } - @objc public static func expiredActionCodeError(message: String?) -> Error { + static func expiredActionCodeError(message: String?) -> Error { error(code: .expiredActionCode, message: message) } - @objc public static func invalidActionCodeError(message: String?) -> Error { + static func invalidActionCodeError(message: String?) -> Error { error(code: .invalidActionCode, message: message) } - @objc public static func invalidMessagePayloadError(message: String?) -> Error { + static func invalidMessagePayloadError(message: String?) -> Error { error(code: .invalidMessagePayload, message: message) } - @objc public static func invalidSenderError(message: String?) -> Error { + static func invalidSenderError(message: String?) -> Error { error(code: .invalidSender, message: message) } - @objc public static func invalidRecipientEmailError(message: String?) -> Error { + static func invalidRecipientEmailError(message: String?) -> Error { error(code: .invalidRecipientEmail, message: message) } - @objc public static func missingIosBundleIDError(message: String?) -> Error { + static func missingIosBundleIDError(message: String?) -> Error { error(code: .missingIosBundleID, message: message) } - @objc public static func missingAndroidPackageNameError(message: String?) -> Error { + static func missingAndroidPackageNameError(message: String?) -> Error { error(code: .missingAndroidPackageName, message: message) } - @objc public static func unauthorizedDomainError(message: String?) -> Error { + static func unauthorizedDomainError(message: String?) -> Error { error(code: .unauthorizedDomain, message: message) } - @objc public static func invalidContinueURIError(message: String?) -> Error { + static func invalidContinueURIError(message: String?) -> Error { error(code: .invalidContinueURI, message: message) } - @objc public static func missingContinueURIError(message: String?) -> Error { + static func missingContinueURIError(message: String?) -> Error { error(code: .missingContinueURI, message: message) } - @objc public static func missingEmailError(message: String?) -> Error { + static func missingEmailError(message: String?) -> Error { error(code: .missingEmail, message: message) } - @objc public static func missingPhoneNumberError(message: String?) -> Error { + static func missingPhoneNumberError(message: String?) -> Error { error(code: .missingPhoneNumber, message: message) } - @objc public static func invalidPhoneNumberError(message: String?) -> Error { + static func invalidPhoneNumberError(message: String?) -> Error { error(code: .invalidPhoneNumber, message: message) } - @objc public static func missingVerificationCodeError(message: String?) -> Error { + static func missingVerificationCodeError(message: String?) -> Error { error(code: .missingVerificationCode, message: message) } - @objc public static func invalidVerificationCodeError(message: String?) -> Error { + static func invalidVerificationCodeError(message: String?) -> Error { error(code: .invalidVerificationCode, message: message) } - @objc public static func missingVerificationIDError(message: String?) -> Error { + static func missingVerificationIDError(message: String?) -> Error { error(code: .missingVerificationID, message: message) } - @objc public static func invalidVerificationIDError(message: String?) -> Error { + static func invalidVerificationIDError(message: String?) -> Error { error(code: .invalidVerificationID, message: message) } - @objc public static func sessionExpiredError(message: String?) -> Error { + static func sessionExpiredError(message: String?) -> Error { error(code: .sessionExpired, message: message) } - @objc public static func missingAppCredential(message: String?) -> Error { + static func missingAppCredential(message: String?) -> Error { error(code: .missingAppCredential, message: message) } - @objc public static func invalidAppCredential(message: String?) -> Error { + static func invalidAppCredential(message: String?) -> Error { error(code: .invalidAppCredential, message: message) } - @objc public static func quotaExceededError(message: String?) -> Error { + static func quotaExceededError(message: String?) -> Error { error(code: .quotaExceeded, message: message) } - @objc public static func missingAppTokenError(underlyingError: Error?) -> Error { + static func missingAppTokenError(underlyingError: Error?) -> Error { error(code: .missingAppToken, underlyingError: underlyingError) } - @objc public static func localPlayerNotAuthenticatedError() -> Error { + static func localPlayerNotAuthenticatedError() -> Error { error(code: .localPlayerNotAuthenticated) } - @objc public static func gameKitNotLinkedError() -> Error { + static func gameKitNotLinkedError() -> Error { error(code: .gameKitNotLinked) } - @objc public static func RPCRequestEncodingError(underlyingError: Error) -> Error { + static func RPCRequestEncodingError(underlyingError: Error) -> Error { error(code: .internal(.RPCRequestEncodingError), underlyingError: underlyingError) } - @objc public static func JSONSerializationErrorForUnencodableType() -> Error { + static func JSONSerializationErrorForUnencodableType() -> Error { error(code: .internal(.JSONSerializationError)) } - @objc public static func JSONSerializationError(underlyingError: Error) -> Error { + static func JSONSerializationError(underlyingError: Error) -> Error { error(code: .internal(.JSONSerializationError), underlyingError: underlyingError) } - @objc public static func networkError(underlyingError: Error) -> Error { + static func networkError(underlyingError: Error) -> Error { error(code: .networkError, underlyingError: underlyingError) } - @objc public static func emailAlreadyInUseError(email: String?) -> Error { + static func emailAlreadyInUseError(email: String?) -> Error { var userInfo: [String: Any]? if let email, !email.isEmpty { userInfo = [userInfoEmailKey: email] @@ -308,9 +308,9 @@ private let kFIRAuthErrorMessageMalformedJWT = return error(code: .emailAlreadyInUse, userInfo: userInfo) } - @objc public static func credentialAlreadyInUseError(message: String?, - credential: AuthCredential?, - email: String?) -> Error { + static func credentialAlreadyInUseError(message: String?, + credential: AuthCredential?, + email: String?) -> Error { var userInfo: [String: Any] = [:] if let credential { userInfo[userInfoUpdatedCredentialKey] = credential @@ -324,15 +324,15 @@ private let kFIRAuthErrorMessageMalformedJWT = return error(code: .credentialAlreadyInUse, message: message) } - @objc public static func webContextAlreadyPresentedError(message: String?) -> Error { + static func webContextAlreadyPresentedError(message: String?) -> Error { error(code: .webContextAlreadyPresented, message: message) } - @objc public static func webContextCancelledError(message: String?) -> Error { + static func webContextCancelledError(message: String?) -> Error { error(code: .webContextCancelled, message: message) } - @objc public static func appVerificationUserInteractionFailure(reason: String?) -> Error { + static func appVerificationUserInteractionFailure(reason: String?) -> Error { let userInfo: [String: Any]? if let reason, !reason.isEmpty { userInfo = [NSLocalizedFailureReasonErrorKey: reason] @@ -342,7 +342,7 @@ private let kFIRAuthErrorMessageMalformedJWT = return error(code: .appVerificationUserInteractionFailure, userInfo: userInfo) } - @objc public static func webSignInUserInteractionFailure(reason: String?) -> Error { + static func webSignInUserInteractionFailure(reason: String?) -> Error { let userInfo: [String: Any]? if let reason, !reason.isEmpty { userInfo = [NSLocalizedFailureReasonErrorKey: reason] @@ -352,7 +352,7 @@ private let kFIRAuthErrorMessageMalformedJWT = return error(code: .webSignInUserInteractionFailure, userInfo: userInfo) } - @objc public static func urlResponseError(code: String, message: String?) -> Error { + static func urlResponseError(code: String, message: String?) -> Error { let errorCode: AuthErrorCode switch code { case kURLResponseErrorCodeInvalidClientID: @@ -367,52 +367,56 @@ private let kFIRAuthErrorMessageMalformedJWT = return error(code: errorCode, message: message) } - @objc public static func nullUserError(message: String?) -> Error { + static func nullUserError(message: String?) -> Error { error(code: .nullUser, message: message) } - @objc public static func invalidProviderIDError(message: String?) -> Error { + static func invalidProviderIDError(message: String?) -> Error { error(code: .invalidProviderID, message: message) } - @objc public static func invalidDynamicLinkDomainError(message: String?) -> Error { + static func invalidDynamicLinkDomainError(message: String?) -> Error { error(code: .invalidDynamicLinkDomain, message: message) } - @objc public static func missingOrInvalidNonceError(message: String?) -> Error { + static func missingOrInvalidNonceError(message: String?) -> Error { error(code: .missingOrInvalidNonce, message: message) } - @objc public static func keychainError(function: String, status: OSStatus) -> Error { + static func keychainError(function: String, status: OSStatus) -> Error { let reason = "\(function) (\(status))" return error(code: .keychainError, userInfo: [NSLocalizedFailureReasonErrorKey: reason]) } - @objc public static func tenantIDMismatchError() -> Error { + static func tenantIDMismatchError() -> Error { error(code: .tenantIDMismatch) } - @objc public static func unsupportedTenantOperationError() -> Error { + static func unsupportedTenantOperationError() -> Error { error(code: .unsupportedTenantOperation) } - @objc public static func notificationNotForwardedError() -> Error { + static func notificationNotForwardedError() -> Error { error(code: .notificationNotForwarded) } - @objc public static func appNotVerifiedError(message: String?) -> Error { + static func appNotVerifiedError(message: String?) -> Error { error(code: .appNotVerified, message: message) } - @objc public static func missingClientIdentifierError(message: String?) -> Error { + static func missingClientIdentifierError(message: String?) -> Error { error(code: .missingClientIdentifier, message: message) } - @objc public static func captchaCheckFailedError(message: String?) -> Error { + static func missingClientType(message: String?) -> Error { + error(code: .missingClientType, message: message) + } + + static func captchaCheckFailedError(message: String?) -> Error { error(code: .captchaCheckFailed, message: message) } - @objc public static func unexpectedResponse(data: Data?, underlyingError: Error?) -> Error { + static func unexpectedResponse(data: Data?, underlyingError: Error?) -> Error { var userInfo: [String: Any] = [:] if let data { userInfo[userInfoDataKey] = data @@ -423,8 +427,8 @@ private let kFIRAuthErrorMessageMalformedJWT = return error(code: .internal(.unexpectedResponse), userInfo: userInfo) } - @objc public static func unexpectedErrorResponse(data: Data?, - underlyingError: Error?) -> Error { + static func unexpectedErrorResponse(data: Data?, + underlyingError: Error?) -> Error { var userInfo: [String: Any] = [:] if let data { userInfo[userInfoDataKey] = data @@ -435,7 +439,7 @@ private let kFIRAuthErrorMessageMalformedJWT = return error(code: .internal(.unexpectedErrorResponse), userInfo: userInfo) } - @objc public static func unexpectedErrorResponse(deserializedResponse: Any?) -> Error { + static func unexpectedErrorResponse(deserializedResponse: Any?) -> Error { var userInfo: [String: Any]? if let deserializedResponse { userInfo = [userInfoDeserializedResponseKey: deserializedResponse] @@ -443,7 +447,7 @@ private let kFIRAuthErrorMessageMalformedJWT = return error(code: .internal(.unexpectedErrorResponse), userInfo: userInfo) } - @objc public static func unexpectedResponse(deserializedResponse: Any?) -> Error { + static func unexpectedResponse(deserializedResponse: Any?) -> Error { var userInfo: [String: Any]? if let deserializedResponse { userInfo = [userInfoDeserializedResponseKey: deserializedResponse] @@ -451,8 +455,8 @@ private let kFIRAuthErrorMessageMalformedJWT = return error(code: .internal(.unexpectedResponse), userInfo: userInfo) } - @objc public static func unexpectedResponse(deserializedResponse: Any?, - underlyingError: Error?) -> Error { + static func unexpectedResponse(deserializedResponse: Any?, + underlyingError: Error?) -> Error { var userInfo: [String: Any] = [:] if let deserializedResponse { userInfo[userInfoDeserializedResponseKey] = deserializedResponse @@ -463,8 +467,8 @@ private let kFIRAuthErrorMessageMalformedJWT = return error(code: .internal(.unexpectedResponse), userInfo: userInfo) } - @objc public static func unexpectedErrorResponse(deserializedResponse: Any?, - underlyingError: Error?) -> Error { + static func unexpectedErrorResponse(deserializedResponse: Any?, + underlyingError: Error?) -> Error { var userInfo: [String: Any] = [:] if let deserializedResponse { userInfo[userInfoDeserializedResponseKey] = deserializedResponse @@ -478,7 +482,7 @@ private let kFIRAuthErrorMessageMalformedJWT = ) } - @objc public static func malformedJWTError(token: String, underlyingError: Error?) -> Error { + static func malformedJWTError(token: String, underlyingError: Error?) -> Error { var userInfo: [String: Any] = [ NSLocalizedDescriptionKey: kFIRAuthErrorMessageMalformedJWT, userInfoDataKey: token, @@ -489,8 +493,8 @@ private let kFIRAuthErrorMessageMalformedJWT = return error(code: .malformedJWT, userInfo: userInfo) } - @objc public static func RPCResponseDecodingError(deserializedResponse: Any?, - underlyingError: Error?) -> Error { + static func RPCResponseDecodingError(deserializedResponse: Any?, + underlyingError: Error?) -> Error { var userInfo: [String: Any] = [:] if let deserializedResponse { userInfo[userInfoDeserializedResponseKey] = deserializedResponse @@ -501,8 +505,8 @@ private let kFIRAuthErrorMessageMalformedJWT = return error(code: .internal(.RPCResponseDecodingError), userInfo: userInfo) } - @objc public static func accountExistsWithDifferentCredentialError(email: String?, - updatedCredential: AuthCredential?) + static func accountExistsWithDifferentCredentialError(email: String?, + updatedCredential: AuthCredential?) -> Error { var userInfo: [String: Any] = [:] if let email { @@ -514,7 +518,7 @@ private let kFIRAuthErrorMessageMalformedJWT = return error(code: .accountExistsWithDifferentCredential, userInfo: userInfo) } - @objc public static func blockingCloudFunctionServerResponse(message: String?) -> Error { + static func blockingCloudFunctionServerResponse(message: String?) -> Error { guard let message else { return error(code: .blockingCloudFunctionError, message: message) } @@ -537,9 +541,9 @@ private let kFIRAuthErrorMessageMalformedJWT = #if os(iOS) // TODO(ncooke3): Address the optionality of these arguments. - @objc public static func secondFactorRequiredError(pendingCredential: String?, - hints: [MultiFactorInfo]?, - auth: Auth) + static func secondFactorRequiredError(pendingCredential: String?, + hints: [MultiFactorInfo]?, + auth: Auth) -> Error { var userInfo: [String: Any] = [:] if let pendingCredential = pendingCredential, let hints = hints { @@ -550,6 +554,13 @@ private let kFIRAuthErrorMessageMalformedJWT = return error(code: .secondFactorRequired, userInfo: userInfo) } #endif // os(iOS) + + static func recaptchaSDKNotLinkedError() -> Error { + // TODO(chuanr): point the link to GCIP doc once available. + let message = "The reCAPTCHA SDK is not linked to your app. See " + + "https://cloud.google.com/recaptcha-enterprise/docs/instrument-ios-apps" + return error(code: .recaptchaSDKNotLinked, message: message) + } } -@objc public protocol MultiFactorResolverWrapper: NSObjectProtocol {} +protocol MultiFactorResolverWrapper: NSObjectProtocol {} diff --git a/FirebaseAuth/Sources/Swift/Utilities/AuthErrors.swift b/FirebaseAuth/Sources/Swift/Utilities/AuthErrors.swift index 9166c1596d3..780205b351c 100644 --- a/FirebaseAuth/Sources/Swift/Utilities/AuthErrors.swift +++ b/FirebaseAuth/Sources/Swift/Utilities/AuthErrors.swift @@ -393,6 +393,10 @@ import Foundation */ case emailChangeNeedsVerification = 17090 + /** Indicates that the request does not contain a client identifier. + */ + case missingClientIdentifier = 17093 + /** Indicates that the nonce is missing or invalid. */ case missingOrInvalidNonce = 17094 @@ -402,9 +406,41 @@ import Foundation */ case blockingCloudFunctionError = 17105 - /** Indicates an error for when the client identifier is missing. + /** Indicates that reCAPTCHA Enterprise integration is not enabled for this project. + */ + case recaptchaNotEnabled = 17200 + + /** Indicates that the reCAPTCHA token is missing from the backend request. + */ + case missingRecaptchaToken = 17201 + + /** Indicates that the reCAPTCHA token sent with the backend request is invalid. + */ + case invalidRecaptchaToken = 17202 + + /** Indicates that the requested reCAPTCHA action is invalid. + */ + case invalidRecaptchaAction = 17203 + + /** Indicates that the client type is missing from the request. + */ + case missingClientType = 17204 + + /** Indicates that the reCAPTCHA version is missing from the request. + */ + case missingRecaptchaVersion = 17205 + + /** Indicates that the reCAPTCHA version sent to the backend is invalid. + */ + case invalidRecaptchaVersion = 17206 + + /** Indicates that the request type sent to the backend is invalid. + */ + case invalidReqType = 17207 + + /** Indicates that the reCAPTCHA SDK is not linked to the app. */ - case missingClientIdentifier = 17993 + case recaptchaSDKNotLinked = 17208 /** Indicates an error occurred while attempting to access the keychain. */ @@ -581,6 +617,24 @@ import Foundation return kFIRAuthErrorMessageUnsupportedTenantOperation case .blockingCloudFunctionError: return kFIRAuthErrorMessageBlockingCloudFunctionReturnedError + case .recaptchaNotEnabled: + return kFIRAuthErrorMessageRecaptchaNotEnabled + case .missingRecaptchaToken: + return kFIRAuthErrorMessageMissingRecaptchaToken + case .invalidRecaptchaToken: + return kFIRAuthErrorMessageInvalidRecaptchaToken + case .invalidRecaptchaAction: + return kFIRAuthErrorMessageInvalidRecaptchaAction + case .missingClientType: + return kFIRAuthErrorMessageMissingClientType + case .missingRecaptchaVersion: + return kFIRAuthErrorMessageMissingRecaptchaVersion + case .invalidRecaptchaVersion: + return kFIRAuthErrorMessageInvalidRecaptchaVersion + case .invalidReqType: + return kFIRAuthErrorMessageInvalidReqType + case .recaptchaSDKNotLinked: + return kFIRAuthErrorMessageRecaptchaSDKNotLinked } } @@ -746,6 +800,24 @@ import Foundation return "ERROR_UNSUPPORTED_TENANT_OPERATION" case .blockingCloudFunctionError: return "ERROR_BLOCKING_CLOUD_FUNCTION_RETURNED_ERROR" + case .recaptchaNotEnabled: + return "ERROR_RECAPTCHA_NOT_ENABLED" + case .missingRecaptchaToken: + return "ERROR_MISSING_RECAPTCHA_TOKEN" + case .invalidRecaptchaToken: + return "ERROR_INVALID_RECAPTCHA_TOKEN" + case .invalidRecaptchaAction: + return "ERROR_INVALID_RECAPTCHA_ACTION" + case .missingClientType: + return "ERROR_MISSING_CLIENT_TYPE" + case .missingRecaptchaVersion: + return "ERROR_MISSING_RECAPTCHA_VERSION" + case .invalidRecaptchaVersion: + return "ERROR_INVALID_RECAPTCHA_VERSION" + case .invalidReqType: + return "ERROR_INVALID_REQ_TYPE" + case .recaptchaSDKNotLinked: + return "ERROR_RECAPTCHA_SDK_NOT_LINKED" } } } @@ -841,12 +913,6 @@ private let kFIRAuthErrorMessageNetworkError = private let kFIRAuthErrorMessageKeychainError = "An error occurred when accessing the keychain. The NSLocalizedFailureReasonErrorKey field in the NSError.userInfo dictionary will contain more information about the error encountered" -/** @var kFIRAuthErrorMessageMissingClientIdentifier - @brief Message for @c FIRAuthErrorCodeMissingClientIdentifier error code. - */ -private let kFIRAuthErrorMessageMissingClientIdentifier = - "The request does not contain any client identifier." - /** @var kFIRAuthErrorMessageUserTokenExpired @brief Message for @c FIRAuthErrorCodeTokenExpired error code. */ @@ -1198,6 +1264,12 @@ private let kFIRAuthErrorMessageDynamicLinkNotActivated = private let kFIRAuthErrorMessageRejectedCredential = "The request contains malformed or mismatching credentials." +/** @var kFIRAuthErrorMessageMissingClientIdentifier + @brief Error message constant describing @c FIRAuthErrorCodeMissingClientIdentifier errors. + */ +private let kFIRAuthErrorMessageMissingClientIdentifier = + "The request does not contain a client identifier." + /** @var kFIRAuthErrorMessageMissingOrInvalidNonce @brief Error message constant describing @c FIRAuthErrorCodeMissingOrInvalidNonce errors. */ @@ -1221,3 +1293,32 @@ private let kFIRAuthErrorMessageUnsupportedTenantOperation = */ private let kFIRAuthErrorMessageBlockingCloudFunctionReturnedError = "Blocking cloud function returned an error." + +private let kFIRAuthErrorMessageRecaptchaNotEnabled = + "reCAPTCHA Enterprise is not enabled for this project." + +private let kFIRAuthErrorMessageMissingRecaptchaToken = + "The backend request is missing the reCAPTCHA verification token." + +private let kFIRAuthErrorMessageInvalidRecaptchaToken = + "The reCAPTCHA verification token is invalid or has expired." + +private let kFIRAuthErrorMessageInvalidRecaptchaAction = + "The reCAPTCHA verification failed due to an invalid action." + +private let kFIRAuthErrorMessageMissingClientType = + "The request is missing a client type or the client type is invalid." + +private let kFIRAuthErrorMessageMissingRecaptchaVersion = + "The request is missing the reCAPTCHA version parameter." + +private let kFIRAuthErrorMessageInvalidRecaptchaVersion = + "The request specifies an invalid version of reCAPTCHA." + +private let kFIRAuthErrorMessageInvalidReqType = + "The request is not supported or is invalid." + +// TODO(chuanr): point the link to GCIP doc once available. +private let kFIRAuthErrorMessageRecaptchaSDKNotLinked = + "The reCAPTCHA SDK is not linked to your app. See " + + "https://cloud.google.com/recaptcha-enterprise/docs/instrument-ios-apps" diff --git a/FirebaseAuth/Sources/Swift/Utilities/AuthRecaptchaVerifier.swift b/FirebaseAuth/Sources/Swift/Utilities/AuthRecaptchaVerifier.swift new file mode 100644 index 00000000000..1f0c88df251 --- /dev/null +++ b/FirebaseAuth/Sources/Swift/Utilities/AuthRecaptchaVerifier.swift @@ -0,0 +1,192 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#if os(iOS) + + import Foundation + import RecaptchaInterop + + @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) + class AuthRecaptchaConfig { + let siteKey: String + let enablementStatus: [String: Bool] + + init(siteKey: String, enablementStatus: [String: Bool]) { + self.siteKey = siteKey + self.enablementStatus = enablementStatus + } + } + + enum AuthRecaptchaProvider { + case password + } + + enum AuthRecaptchaAction { + case defaultAction + case signInWithPassword + case getOobCode + case signUpPassword + } + + @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) + class AuthRecaptchaVerifier { + private(set) weak var auth: Auth? + private(set) var agentConfig: AuthRecaptchaConfig? + private(set) var tenantConfigs: [String: AuthRecaptchaConfig] = [:] + private(set) var recaptchaClient: RCARecaptchaClientProtocol? + + private static let _shared = AuthRecaptchaVerifier() + private let providerToStringMap = [AuthRecaptchaProvider.password: "EMAIL_PASSWORD_PROVIDER"] + private let actionToStringMap = [AuthRecaptchaAction.signInWithPassword: "signInWithPassword", + AuthRecaptchaAction.getOobCode: "getOobCode", + AuthRecaptchaAction.signUpPassword: "signUpPassword"] + private let kRecaptchaVersion = "RECAPTCHA_ENTERPRISE" + private init() {} + + class func shared(auth: Auth?) -> AuthRecaptchaVerifier { + if _shared.auth != auth { + _shared.agentConfig = nil + _shared.tenantConfigs = [:] + _shared.auth = auth + } + return _shared + } + + func siteKey() -> String? { + if let tenantID = auth?.tenantID { + if let config = tenantConfigs[tenantID] { + return config.siteKey + } + return nil + } + return agentConfig?.siteKey + } + + func enablementStatus(forProvider provider: AuthRecaptchaProvider) -> Bool { + guard let providerString = providerToStringMap[provider] else { + return false + } + if let tenantID = auth?.tenantID { + guard let tenantConfig = tenantConfigs[tenantID], + let status = tenantConfig.enablementStatus[providerString] else { + return false + } + return status + } else { + guard let agentConfig, + let status = agentConfig.enablementStatus[providerString] else { + return false + } + return status + } + } + + func verify(forceRefresh: Bool, action: AuthRecaptchaAction) async throws -> String { + try await retrieveRecaptchaConfig(forceRefresh: forceRefresh) + if recaptchaClient == nil { + guard let siteKey = siteKey(), + let RecaptchaClass = NSClassFromString("Recaptcha"), + let recaptcha = RecaptchaClass as? any RCARecaptchaProtocol.Type else { + throw AuthErrorUtils.recaptchaSDKNotLinkedError() + } + recaptchaClient = try await recaptcha.getClient(withSiteKey: siteKey) + } + return try await retrieveRecaptchaToken(withAction: action) + } + + func retrieveRecaptchaToken(withAction action: AuthRecaptchaAction) async throws -> String { + guard let actionString = actionToStringMap[action], + let RecaptchaActionClass = NSClassFromString("RecaptchaAction"), + let actionClass = RecaptchaActionClass as? any RCAActionProtocol.Type else { + throw AuthErrorUtils.recaptchaSDKNotLinkedError() + } + let customAction = actionClass.init(customAction: actionString) + do { + let token = try await recaptchaClient?.execute(withAction: customAction) + AuthLog.logInfo(code: "TODO", message: "reCAPTCHA token retrieval succeeded.") + guard let token else { + AuthLog.logInfo( + code: "TODO", + message: "reCAPTCHA token retrieval returned nil. NO_RECAPTCHA sent as the fake code." + ) + return "NO_RECAPTCHA" + } + return token + } catch { + AuthLog.logInfo(code: "TODO", + message: "reCAPTCHA token retrieval failed. NO_RECAPTCHA sent as the fake code.") + return "NO_RECAPTCHA" + } + } + + func retrieveRecaptchaConfig(forceRefresh: Bool) async throws { + if !forceRefresh { + if let tenantID = auth?.tenantID { + if tenantConfigs[tenantID] != nil { + return + } + } else if agentConfig != nil { + return + } + } + + guard let requestConfiguration = auth?.requestConfiguration 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) + AuthLog.logInfo(code: "TODO-CODE", message: "reCAPTCHA config retrieval succeeded.") + // Response's site key is of the format projects//keys/' + guard let siteKey = response.recaptchaKey?.components(separatedBy: "/")[3] else { + throw AuthErrorUtils.error(code: .recaptchaNotEnabled, + message: "Invalid siteKey") + } + var enablementStatus: [String: Bool] = [:] + if let enforcementState = response.enforcementState { + for state in enforcementState { + if let provider = state["provider"], + provider == providerToStringMap[AuthRecaptchaProvider.password] { + if let enforcement = state["enforcementState"] { + if enforcement == "ENFORCE" || enforcement == "AUDIT" { + enablementStatus[provider] = true + } else if enforcement == "OFF" { + enablementStatus[provider] = false + } + } + } + } + } + let config = AuthRecaptchaConfig(siteKey: siteKey, enablementStatus: enablementStatus) + + if let tenantID = auth?.tenantID { + tenantConfigs[tenantID] = config + } else { + agentConfig = config + } + } + + func injectRecaptchaFields(request: any AuthRPCRequest, + provider: AuthRecaptchaProvider, + action: AuthRecaptchaAction) async throws { + try await retrieveRecaptchaConfig(forceRefresh: false) + if enablementStatus(forProvider: provider) { + let token = try await verify(forceRefresh: false, action: action) + request.injectRecaptchaFields(recaptchaResponse: token, recaptchaVersion: kRecaptchaVersion) + } else { + request.injectRecaptchaFields(recaptchaResponse: nil, recaptchaVersion: kRecaptchaVersion) + } + } + } +#endif diff --git a/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/Models/AuthMenu.swift b/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/Models/AuthMenu.swift index 0ffc85c7001..d04603384fd 100644 --- a/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/Models/AuthMenu.swift +++ b/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/Models/AuthMenu.swift @@ -29,6 +29,7 @@ enum AuthMenu: String { case phoneNumber = "phone" case anonymous case custom + case initRecaptcha /// More intuitively named getter for `rawValue`. var id: String { rawValue } @@ -62,6 +63,8 @@ enum AuthMenu: String { return "Anonymous Authentication" case .custom: return "Custom Auth System" + case .initRecaptcha: + return "Initialize reCAPTCHA Enterprise" } } @@ -95,6 +98,8 @@ enum AuthMenu: String { self = .anonymous case "Custom Auth System": self = .custom + case "Initialize reCAPTCHA Enterprise": + self = .initRecaptcha default: return nil } } @@ -143,8 +148,15 @@ extension AuthMenu: DataSourceProvidable { return Section(headerDescription: header, items: otherOptions) } + static var recaptchaSection: Section { + let image = UIImage(named: "firebaseIcon") + let header = "Initialize reCAPTCHA Enterprise" + let item = Item(title: initRecaptcha.name, hasNestedContent: false, image: image) + return Section(headerDescription: header, items: [item]) + } + static var sections: [Section] { - [settingsSection, providerSection, emailPasswordSection, otherSection] + [settingsSection, providerSection, emailPasswordSection, otherSection, recaptchaSection] } static var authLinkSections: [Section] { diff --git a/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/ViewControllers/AuthViewController.swift b/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/ViewControllers/AuthViewController.swift index e5dfc4a1c21..01dd83b9ff2 100644 --- a/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/ViewControllers/AuthViewController.swift +++ b/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/ViewControllers/AuthViewController.swift @@ -87,6 +87,9 @@ class AuthViewController: UIViewController, DataSourceProviderDelegate { case .custom: performCustomAuthLoginFlow() + + case .initRecaptcha: + performInitRecaptcha() } } @@ -248,6 +251,17 @@ class AuthViewController: UIViewController, DataSourceProviderDelegate { } } + private func performInitRecaptcha() { + Task { + do { + try await AppManager.shared.auth().initializeRecaptchaConfig() + print("Initializing Recaptcha config succeeded.") + } catch { + print("Initializing Recaptcha config failed: \(error).") + } + } + } + // MARK: - Private Helpers private func configureDataSourceProvider() { diff --git a/FirebaseAuth/Tests/SampleSwift/Podfile b/FirebaseAuth/Tests/SampleSwift/Podfile index 1c1eb0349be..f7d7afa71e8 100644 --- a/FirebaseAuth/Tests/SampleSwift/Podfile +++ b/FirebaseAuth/Tests/SampleSwift/Podfile @@ -15,6 +15,8 @@ target 'AuthenticationExample' do pod 'FirebaseAuthInterop', :path => '../../..' pod 'FirebaseAppCheckInterop', :path => '../../..' + pod 'RecaptchaEnterprise', '~> 18.3' + ### For Email Link/Passwordless Auth pod 'FirebaseDynamicLinks', :path => '../../..' diff --git a/FirebaseAuth/Tests/Unit/GetOOBConfirmationCodeTests.swift b/FirebaseAuth/Tests/Unit/GetOOBConfirmationCodeTests.swift index c8cb09794c4..b9fe798f57d 100644 --- a/FirebaseAuth/Tests/Unit/GetOOBConfirmationCodeTests.swift +++ b/FirebaseAuth/Tests/Unit/GetOOBConfirmationCodeTests.swift @@ -69,6 +69,53 @@ class GetOOBConfirmationCodeTests: RPCBaseTests { } } + /** @fn testPasswordResetRequestOptionalFields + @brief Tests the encoding of a password reset request with optional fields. + */ + func testPasswordResetRequestOptionalFields() async throws { + let kCaptchaResponseKey = "captchaResp" + let kTestCaptchaResponse = "testCaptchaResponse" + let kClientTypeKey = "clientType" + let kTestClientType = "testClientType" + let kRecaptchaVersionKey = "recaptchaVersion" + let kTestRecaptchaVersion = "testRecaptchaVersion" + + for (request, requestType) in [ + (getPasswordResetRequest, kPasswordResetRequestTypeValue), + (getSignInWithEmailRequest, kEmailLinkSignInTypeValue), + (getEmailVerificationRequest, kVerifyEmailRequestTypeValue), + ] { + let request = try request() + request.captchaResponse = kTestCaptchaResponse + request.clientType = kTestClientType + request.recaptchaVersion = kTestRecaptchaVersion + + try await checkRequest( + request: request, + expected: kExpectedAPIURL, + key: "should_be_empty_dictionary", + value: nil + ) + let decodedRequest = try XCTUnwrap(rpcIssuer.decodedRequest) + XCTAssertEqual(decodedRequest[kRequestTypeKey] as? String, requestType) + if requestType == kVerifyEmailRequestTypeValue { + XCTAssertEqual(decodedRequest[kAccessTokenKey] as? String, kTestAccessToken) + } else { + XCTAssertEqual(decodedRequest[kEmailKey] as? String, kTestEmail) + } + XCTAssertEqual(decodedRequest[kContinueURLKey] as? String, kContinueURL) + XCTAssertEqual(decodedRequest[kIosBundleIDKey] as? String, kIosBundleID) + XCTAssertEqual(decodedRequest[kAndroidPackageNameKey] as? String, kAndroidPackageName) + XCTAssertEqual(decodedRequest[kAndroidMinimumVersionKey] as? String, kAndroidMinimumVersion) + XCTAssertEqual(decodedRequest[kAndroidInstallAppKey] as? Bool, true) + XCTAssertEqual(decodedRequest[kCanHandleCodeInAppKey] as? Bool, true) + XCTAssertEqual(decodedRequest[kDynamicLinkDomainKey] as? String, kDynamicLinkDomain) + XCTAssertEqual(decodedRequest[kCaptchaResponseKey] as? String, kTestCaptchaResponse) + XCTAssertEqual(decodedRequest[kClientTypeKey] as? String, kTestClientType) + XCTAssertEqual(decodedRequest[kRecaptchaVersionKey] as? String, kTestRecaptchaVersion) + } + } + func testGetOOBConfirmationCodeErrors() async throws { let kEmailNotFoundMessage = "EMAIL_NOT_FOUND: fake custom message" let kMissingEmailErrorMessage = "MISSING_EMAIL" diff --git a/FirebaseAuth/Tests/Unit/SignUpNewUserTests.swift b/FirebaseAuth/Tests/Unit/SignUpNewUserTests.swift index c72fb8d474a..818e2882e79 100644 --- a/FirebaseAuth/Tests/Unit/SignUpNewUserTests.swift +++ b/FirebaseAuth/Tests/Unit/SignUpNewUserTests.swift @@ -124,6 +124,38 @@ class SignUpNewUserTests: RPCBaseTests { ) } + /** @fn testSignUpNewUserRequestOptionalFields + @brief Tests the encoding of a sign up new user request with optional fields. + */ + func testSignUpNewUserRequestOptionalFields() async throws { + let kEmailKey = "email" + let kPasswordKey = "password" + let kCaptchaResponseKey = "captchaResponse" + let kTestCaptchaResponse = "testCaptchaResponse" + let kTestPendingToken = "testPendingToken" + let kClientTypeKey = "clientType" + let kTestClientType = "testClientType" + let kRecaptchaVersionKey = "recaptchaVersion" + let kTestRecaptchaVersion = "testRecaptchaVersion" + let request = makeSignUpNewUserRequest() + request.captchaResponse = kTestCaptchaResponse + request.clientType = kTestClientType + request.recaptchaVersion = kTestRecaptchaVersion + try await checkRequest( + request: request, + expected: kExpectedAPIURL, + key: kEmailKey, + value: kTestEmail + ) + let requestDictionary = try XCTUnwrap(rpcIssuer.decodedRequest as? [String: AnyHashable]) + XCTAssertEqual(requestDictionary[kDisplayNameKey], kTestDisplayName) + XCTAssertEqual(requestDictionary[kPasswordKey], kTestPassword) + XCTAssertTrue(try XCTUnwrap(requestDictionary[kReturnSecureTokenKey] as? Bool)) + XCTAssertEqual(requestDictionary[kCaptchaResponseKey], kTestCaptchaResponse) + XCTAssertEqual(requestDictionary[kClientTypeKey], kTestClientType) + XCTAssertEqual(requestDictionary[kRecaptchaVersionKey], kTestRecaptchaVersion) + } + private func makeSignUpNewUserRequestAnonymous() -> SignUpNewUserRequest { return SignUpNewUserRequest(requestConfiguration: makeRequestConfiguration()) } diff --git a/FirebaseAuth/Tests/Unit/SwiftAPI.swift b/FirebaseAuth/Tests/Unit/SwiftAPI.swift index ef82cb127df..e70e856d597 100644 --- a/FirebaseAuth/Tests/Unit/SwiftAPI.swift +++ b/FirebaseAuth/Tests/Unit/SwiftAPI.swift @@ -157,6 +157,9 @@ class AuthAPI_hOnlyTests: XCTestCase { auth.setAPNSToken(Data(), type: AuthAPNSTokenType(rawValue: 2)!) _ = auth.canHandleNotification([:]) #endif + auth.revokeToken(withAuthorizationCode: "A") + auth.initializeRecaptchaConfig { _ in + } try auth.useUserAccessGroup("abc") let nilUser = try auth.getStoredUser(forAccessGroup: "def") // If nilUser is not optional, this will raise a compiler error. @@ -195,6 +198,8 @@ class AuthAPI_hOnlyTests: XCTestCase { actionCodeSettings: actionCodeSettings ) _ = try await auth.sendSignInLink(toEmail: "email", actionCodeSettings: actionCodeSettings) + try await auth.revokeToken(withAuthorizationCode: "string") + try await auth.initializeRecaptchaConfig() } #if !os(macOS) @@ -292,6 +297,14 @@ class AuthAPI_hOnlyTests: XCTestCase { _ = AuthErrorCode.emailChangeNeedsVerification _ = AuthErrorCode.missingOrInvalidNonce _ = AuthErrorCode.missingClientIdentifier + _ = AuthErrorCode.recaptchaNotEnabled + _ = AuthErrorCode.missingRecaptchaToken + _ = AuthErrorCode.invalidRecaptchaToken + _ = AuthErrorCode.invalidRecaptchaAction + _ = AuthErrorCode.missingClientType + _ = AuthErrorCode.missingRecaptchaVersion + _ = AuthErrorCode.invalidRecaptchaVersion + _ = AuthErrorCode.invalidReqType _ = AuthErrorCode.keychainError _ = AuthErrorCode.internalError _ = AuthErrorCode.malformedJWT diff --git a/FirebaseAuth/Tests/Unit/VerifyPasswordTests.swift b/FirebaseAuth/Tests/Unit/VerifyPasswordTests.swift index 6e4b8b5b906..6b831f0ce80 100644 --- a/FirebaseAuth/Tests/Unit/VerifyPasswordTests.swift +++ b/FirebaseAuth/Tests/Unit/VerifyPasswordTests.swift @@ -50,15 +50,21 @@ class VerifyPasswordTests: RPCBaseTests { let kCaptchaChallengeKey = "captchaChallenge" let kTestCaptchaChallenge = "testCaptchaChallenge" let kCaptchaResponseKey = "captchaResponse" - let kTestCaptchaResponse = "captchaResponse" + let kTestCaptchaResponse = "testCaptchaResponse" let kSecureTokenKey = "returnSecureToken" let kTestPendingToken = "testPendingToken" + let kClientTypeKey = "clientType" + let kTestClientType = "testClientType" + let kRecaptchaVersionKey = "recaptchaVersion" + let kTestRecaptchaVersion = "testRecaptchaVersion" let kExpectedAPIURL = "https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyPassword?key=APIKey" let request = makeVerifyPasswordRequest() request.pendingIDToken = kTestPendingToken request.captchaChallenge = kTestCaptchaChallenge request.captchaResponse = kTestCaptchaResponse + request.clientType = kTestClientType + request.recaptchaVersion = kTestRecaptchaVersion try await checkRequest( request: request, expected: kExpectedAPIURL, @@ -69,6 +75,8 @@ class VerifyPasswordTests: RPCBaseTests { XCTAssertEqual(requestDictionary[kPasswordKey], kTestPassword) XCTAssertEqual(requestDictionary[kCaptchaChallengeKey], kTestCaptchaChallenge) XCTAssertEqual(requestDictionary[kCaptchaResponseKey], kTestCaptchaResponse) + XCTAssertEqual(requestDictionary[kClientTypeKey], kTestClientType) + XCTAssertEqual(requestDictionary[kRecaptchaVersionKey], kTestRecaptchaVersion) XCTAssertTrue(try XCTUnwrap(requestDictionary[kSecureTokenKey] as? Bool)) } From 6c29a2d3e65888aaf574111bd363fd5fe2232376 Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Fri, 13 Oct 2023 18:17:59 -0700 Subject: [PATCH 3/8] missing iOS check --- FirebaseAuth/Sources/Swift/Auth/Auth.swift | 63 +++++++++++----------- 1 file changed, 32 insertions(+), 31 deletions(-) diff --git a/FirebaseAuth/Sources/Swift/Auth/Auth.swift b/FirebaseAuth/Sources/Swift/Auth/Auth.swift index ece77ca8fbe..b7b4ab836ba 100644 --- a/FirebaseAuth/Sources/Swift/Auth/Auth.swift +++ b/FirebaseAuth/Sources/Swift/Auth/Auth.swift @@ -1431,42 +1431,44 @@ extension Auth: AuthInterop { return false } - /** @fn initializeRecaptchaConfigWithCompletion:completion: - @brief Initializes reCAPTCHA using the settings configured for the project or - tenant. + #if os(iOS) + /** @fn initializeRecaptchaConfigWithCompletion:completion: + @brief Initializes reCAPTCHA using the settings configured for the project or + tenant. - If you change the tenant ID of the `Auth` instance, the configuration will be - reloaded. - */ - @objc(initializeRecaptchaConfigWithCompletion:) - public func initializeRecaptchaConfig(completion: ((Error?) -> Void)?) { - Task { - do { - try await initializeRecaptchaConfig() - if let completion { - completion(nil) - } - } catch { - if let completion { - completion(error) + If you change the tenant ID of the `Auth` instance, the configuration will be + reloaded. + */ + @objc(initializeRecaptchaConfigWithCompletion:) + public func initializeRecaptchaConfig(completion: ((Error?) -> Void)?) { + Task { + do { + try await initializeRecaptchaConfig() + if let completion { + completion(nil) + } + } catch { + if let completion { + completion(error) + } } } } - } - /** @fn initializeRecaptchaConfig - @brief Initializes reCAPTCHA using the settings configured for the project or - tenant. + /** @fn initializeRecaptchaConfig + @brief Initializes reCAPTCHA using the settings configured for the project or + tenant. - If you change the tenant ID of the `Auth` instance, the configuration will be - reloaded. - */ - public func initializeRecaptchaConfig() async throws { - // Trigger recaptcha verification flow to initialize the recaptcha client and - // config. Recaptcha token will be returned. - let verifier = AuthRecaptchaVerifier.shared(auth: self) - let _ = try await verifier.verify(forceRefresh: true, action: AuthRecaptchaAction.defaultAction) - } + If you change the tenant ID of the `Auth` instance, the configuration will be + reloaded. + */ + public func initializeRecaptchaConfig() async throws { + // Trigger recaptcha verification flow to initialize the recaptcha client and + // config. Recaptcha token will be returned. + let verifier = AuthRecaptchaVerifier.shared(auth: self) + _ = try await verifier.verify(forceRefresh: true, action: AuthRecaptchaAction.defaultAction) + } + #endif /** @fn addAuthStateDidChangeListener: @brief Registers a block as an "auth state did change" listener. To be invoked when: @@ -2415,7 +2417,6 @@ extension Auth: AuthInterop { } #if os(iOS) - private func wrapInjectRecaptcha(request: T, action: AuthRecaptchaAction, _ callback: @escaping ( From 127036b55f4fce094d9994c9b2feb41f55e69b15 Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Fri, 13 Oct 2023 20:29:17 -0700 Subject: [PATCH 4/8] fixes --- .../AuthenticationExampleUITests.swift | 2 +- FirebaseAuth/Tests/Unit/SwiftAPI.swift | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/FirebaseAuth/Tests/SampleSwift/AuthenticationExampleUITests/AuthenticationExampleUITests.swift b/FirebaseAuth/Tests/SampleSwift/AuthenticationExampleUITests/AuthenticationExampleUITests.swift index d7a211c054b..34ebfcfafd0 100644 --- a/FirebaseAuth/Tests/SampleSwift/AuthenticationExampleUITests/AuthenticationExampleUITests.swift +++ b/FirebaseAuth/Tests/SampleSwift/AuthenticationExampleUITests/AuthenticationExampleUITests.swift @@ -38,7 +38,7 @@ class AuthenticationExampleUITests: XCTestCase { func testAuthOptions() { // There are 13 sign in methods, each with its own cell - XCTAssertEqual(app.tables.cells.count, 13) + XCTAssertEqual(app.tables.cells.count, 14) } func testAuthAnonymously() { diff --git a/FirebaseAuth/Tests/Unit/SwiftAPI.swift b/FirebaseAuth/Tests/Unit/SwiftAPI.swift index e70e856d597..70e91c053c3 100644 --- a/FirebaseAuth/Tests/Unit/SwiftAPI.swift +++ b/FirebaseAuth/Tests/Unit/SwiftAPI.swift @@ -156,10 +156,10 @@ class AuthAPI_hOnlyTests: XCTestCase { _ = auth.canHandle(URL(fileURLWithPath: "/my/path")) auth.setAPNSToken(Data(), type: AuthAPNSTokenType(rawValue: 2)!) _ = auth.canHandleNotification([:]) + auth.initializeRecaptchaConfig { _ in + } #endif auth.revokeToken(withAuthorizationCode: "A") - auth.initializeRecaptchaConfig { _ in - } try auth.useUserAccessGroup("abc") let nilUser = try auth.getStoredUser(forAccessGroup: "def") // If nilUser is not optional, this will raise a compiler error. @@ -183,6 +183,7 @@ class AuthAPI_hOnlyTests: XCTestCase { let credential = try await provider.credential(with: nil) _ = try await auth.signIn(with: OAuthProvider(providerID: "abc"), uiDelegate: nil) _ = try await auth.signIn(with: credential) + try await auth.initializeRecaptchaConfig() #endif _ = try await auth.signInAnonymously() _ = try await auth.signIn(withCustomToken: "abc") @@ -199,7 +200,6 @@ class AuthAPI_hOnlyTests: XCTestCase { ) _ = try await auth.sendSignInLink(toEmail: "email", actionCodeSettings: actionCodeSettings) try await auth.revokeToken(withAuthorizationCode: "string") - try await auth.initializeRecaptchaConfig() } #if !os(macOS) From 3148e7442e4d7ed53d1c5671380bfc082afcf764 Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Wed, 25 Oct 2023 16:07:19 -0700 Subject: [PATCH 5/8] Rest of tests --- FirebaseAuth/Sources/Swift/Auth/Auth.swift | 13 +- .../Backend/RPC/VerifyPasswordRequest.swift | 5 + .../Utilities/AuthRecaptchaVerifier.swift | 4 +- FirebaseAuth/Tests/Unit/AuthTests.swift | 118 ++++++++++++++++++ .../Unit/Fakes/FakeBackendRPCIssuer.swift | 11 +- .../Tests/Unit/GetRecaptchaConfigTests.swift | 54 ++++++++ .../Tests/Unit/SignUpNewUserTests.swift | 1 - 7 files changed, 196 insertions(+), 10 deletions(-) create mode 100644 FirebaseAuth/Tests/Unit/GetRecaptchaConfigTests.swift diff --git a/FirebaseAuth/Sources/Swift/Auth/Auth.swift b/FirebaseAuth/Sources/Swift/Auth/Auth.swift index b7b4ab836ba..544ee7198d5 100644 --- a/FirebaseAuth/Sources/Swift/Auth/Auth.swift +++ b/FirebaseAuth/Sources/Swift/Auth/Auth.swift @@ -2448,15 +2448,14 @@ extension Auth: AuthInterop { if let underlyingError = nsError.userInfo[NSUnderlyingErrorKey] as? NSError, nsError.code == AuthErrorCode.internalError.rawValue, let messages = underlyingError - .userInfo[AuthErrorUtils.userInfoDeserializedResponseKey] as? - [String: AnyHashable], + .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: AuthRecaptchaAction - .signInWithPassword) + try await recaptchaVerifier.injectRecaptchaFields( + request: request, + provider: AuthRecaptchaProvider.password, + action: AuthRecaptchaAction.signInWithPassword + ) } else { throw error } diff --git a/FirebaseAuth/Sources/Swift/Backend/RPC/VerifyPasswordRequest.swift b/FirebaseAuth/Sources/Swift/Backend/RPC/VerifyPasswordRequest.swift index 757a1e2f1d0..de351d79c67 100644 --- a/FirebaseAuth/Sources/Swift/Backend/RPC/VerifyPasswordRequest.swift +++ b/FirebaseAuth/Sources/Swift/Backend/RPC/VerifyPasswordRequest.swift @@ -142,4 +142,9 @@ class VerifyPasswordRequest: IdentityToolkitRequest, AuthRPCRequest { body[kClientType] = clientType return body } + + func injectRecaptchaFields(recaptchaResponse: String?, recaptchaVersion: String) { + captchaResponse = recaptchaResponse + self.recaptchaVersion = recaptchaVersion + } } diff --git a/FirebaseAuth/Sources/Swift/Utilities/AuthRecaptchaVerifier.swift b/FirebaseAuth/Sources/Swift/Utilities/AuthRecaptchaVerifier.swift index 1f0c88df251..a75014795ee 100644 --- a/FirebaseAuth/Sources/Swift/Utilities/AuthRecaptchaVerifier.swift +++ b/FirebaseAuth/Sources/Swift/Utilities/AuthRecaptchaVerifier.swift @@ -149,10 +149,12 @@ let response = try await AuthBackend.call(with: request) AuthLog.logInfo(code: "TODO-CODE", message: "reCAPTCHA config retrieval succeeded.") // Response's site key is of the format projects//keys/' - guard let siteKey = response.recaptchaKey?.components(separatedBy: "/")[3] else { + guard let keys = response.recaptchaKey?.components(separatedBy: "/"), + keys.count == 4 else { throw AuthErrorUtils.error(code: .recaptchaNotEnabled, message: "Invalid siteKey") } + let siteKey = keys[3] var enablementStatus: [String: Bool] = [:] if let enforcementState = response.enforcementState { for state in enforcementState { diff --git a/FirebaseAuth/Tests/Unit/AuthTests.swift b/FirebaseAuth/Tests/Unit/AuthTests.swift index accf29d8a16..2eb720fd05b 100644 --- a/FirebaseAuth/Tests/Unit/AuthTests.swift +++ b/FirebaseAuth/Tests/Unit/AuthTests.swift @@ -25,6 +25,8 @@ class AuthTests: RPCBaseTests { static let kAccessToken = "TEST_ACCESS_TOKEN" static let kNewAccessToken = "NEW_ACCESS_TOKEN" static let kFakeAPIKey = "FAKE_API_KEY" + static let kFakeRecaptchaResponse = "RecaptchaResponse" + static let kFakeRecaptchaVersion = "RecaptchaVersion" var auth: Auth! static var testNum = 0 var authDispatcherCallback: (() -> Void)? @@ -284,6 +286,122 @@ class AuthTests: RPCBaseTests { XCTAssertNil(auth?.currentUser) } + /** @fn testSignInWithEmailPasswordWithRecaptchaSuccess + @brief Tests the flow of a successful @c signInWithEmail:password:completion: call. + */ + func testSignInWithEmailPasswordWithRecaptchaSuccess() throws { + let kRefreshToken = "fakeRefreshToken" + let expectation = self.expectation(description: #function) + setFakeGetAccountProvider() + setFakeSecureTokenService() + + // 1. Setup respond block to test and fake send request. + rpcIssuer.respondBlock = { + // 2. Validate the created Request instance. + let request = try XCTUnwrap(self.rpcIssuer.request as? VerifyPasswordRequest) + XCTAssertEqual(request.email, self.kEmail) + XCTAssertEqual(request.password, self.kFakePassword) + XCTAssertEqual(request.apiKey, AuthTests.kFakeAPIKey) + XCTAssertTrue(request.returnSecureToken) + request.injectRecaptchaFields(recaptchaResponse: AuthTests.kFakeRecaptchaResponse, + recaptchaVersion: AuthTests.kFakeRecaptchaVersion) + + // 3. Send the response from the fake backend. + try self.rpcIssuer.respond(withJSON: ["idToken": AuthTests.kAccessToken, + "email": self.kEmail, + "isNewUser": true, + "refreshToken": kRefreshToken]) + } + + try auth?.signOut() + 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 { + XCTFail("authResult.user is missing") + return + } + XCTAssertEqual(user.refreshToken, kRefreshToken) + XCTAssertFalse(user.isAnonymous) + XCTAssertEqual(user.email, self.kEmail) + guard let additionalUserInfo = authResult?.additionalUserInfo else { + XCTFail("authResult.additionalUserInfo is missing") + return + } + XCTAssertFalse(additionalUserInfo.isNewUser) + XCTAssertEqual(additionalUserInfo.providerID, EmailAuthProvider.id) + XCTAssertNil(error) + expectation.fulfill() + } + waitForExpectations(timeout: 5) + assertUser(auth?.currentUser) + } + + /** @fn testSignInWithEmailPasswordWithRecaptchaFallbackSuccess + @brief Tests the flow of a successful @c signInWithEmail:password:completion: call. + */ + func testSignInWithEmailPasswordWithRecaptchaFallbackSuccess() throws { + let kRefreshToken = "fakeRefreshToken" + let expectation = self.expectation(description: #function) + setFakeGetAccountProvider() + setFakeSecureTokenService() + let kTestRecaptchaKey = "projects/123/keys/456" + rpcIssuer.recaptchaSiteKey = kTestRecaptchaKey + + // 1. Setup respond block to test and fake send request. + rpcIssuer.respondBlock = { + // 2. Validate the created Request instance. + let request = try XCTUnwrap(self.rpcIssuer.request as? VerifyPasswordRequest) + XCTAssertEqual(request.email, self.kEmail) + XCTAssertEqual(request.password, self.kFakePassword) + XCTAssertEqual(request.apiKey, AuthTests.kFakeAPIKey) + XCTAssertTrue(request.returnSecureToken) + request.injectRecaptchaFields(recaptchaResponse: AuthTests.kFakeRecaptchaResponse, + recaptchaVersion: AuthTests.kFakeRecaptchaVersion) + + // 3. Send the response from the fake backend. + try self.rpcIssuer.respond(serverErrorMessage: "MISSING_RECAPTCHA_TOKEN") + } + rpcIssuer.nextRespondBlock = { + // 4. Validate again the created Request instance after the recaptcha retry. + let request = try XCTUnwrap(self.rpcIssuer.request as? VerifyPasswordRequest) + XCTAssertEqual(request.email, self.kEmail) + XCTAssertEqual(request.password, self.kFakePassword) + XCTAssertEqual(request.apiKey, AuthTests.kFakeAPIKey) + XCTAssertTrue(request.returnSecureToken) + request.injectRecaptchaFields(recaptchaResponse: AuthTests.kFakeRecaptchaResponse, + recaptchaVersion: AuthTests.kFakeRecaptchaVersion) + // 5. Send the response from the fake backend. + try self.rpcIssuer.respond(withJSON: ["idToken": AuthTests.kAccessToken, + "email": self.kEmail, + "isNewUser": true, + "refreshToken": kRefreshToken]) + } + + try auth?.signOut() + auth?.signIn(withEmail: kEmail, password: kFakePassword) { authResult, error in + // 6. After the response triggers the callback, verify the returned result. + XCTAssertTrue(Thread.isMainThread) + XCTAssertNil(error) + guard let user = authResult?.user else { + XCTFail("authResult.user is missing") + return + } + XCTAssertEqual(user.refreshToken, kRefreshToken) + XCTAssertFalse(user.isAnonymous) + XCTAssertEqual(user.email, self.kEmail) + guard let additionalUserInfo = authResult?.additionalUserInfo else { + XCTFail("authResult.additionalUserInfo is missing") + return + } + XCTAssertFalse(additionalUserInfo.isNewUser) + XCTAssertEqual(additionalUserInfo.providerID, EmailAuthProvider.id) + expectation.fulfill() + } + waitForExpectations(timeout: 5) + assertUser(auth?.currentUser) + } + /** @fn testSignInAndRetrieveDataWithEmailPasswordSuccess @brief Tests the flow of a successful @c signInAndRetrieveDataWithEmail:password:completion: call. Superset of historical testSignInWithEmailPasswordSuccess. diff --git a/FirebaseAuth/Tests/Unit/Fakes/FakeBackendRPCIssuer.swift b/FirebaseAuth/Tests/Unit/Fakes/FakeBackendRPCIssuer.swift index 948ab0fac92..d51ccd255da 100644 --- a/FirebaseAuth/Tests/Unit/Fakes/FakeBackendRPCIssuer.swift +++ b/FirebaseAuth/Tests/Unit/Fakes/FakeBackendRPCIssuer.swift @@ -69,11 +69,13 @@ class FakeBackendRPCIssuer: NSObject, AuthBackendRPCIssuer { var verifyPhoneNumberRequester: ((VerifyPhoneNumberRequest) -> Void)? var respondBlock: (() throws -> Void)? + var nextRespondBlock: (() throws -> Void)? var fakeGetAccountProviderJSON: [[String: AnyHashable]]? var fakeSecureTokenServiceJSON: [String: AnyHashable]? var secureTokenNetworkError: NSError? var secureTokenErrorString: String? + var recaptchaSiteKey = "unset recaptcha siteKey" func asyncCallToURL(with request: T, body: Data?, @@ -108,6 +110,12 @@ class FakeBackendRPCIssuer: NSObject, AuthBackendRPCIssuer { fatalError("fakeGetAccountProviderJSON respond failed") } return + } else if let _ = request as? GetRecaptchaConfigRequest { + guard let _ = try? respond(withJSON: ["recaptchaKey": recaptchaSiteKey]) + else { + fatalError("GetRecaptchaConfigRequest respond failed") + } + return } else if let _ = request as? SecureTokenRequest { if let secureTokenNetworkError { guard let _ = try? respond(withData: nil, @@ -143,7 +151,8 @@ class FakeBackendRPCIssuer: NSObject, AuthBackendRPCIssuer { } catch { XCTFail("Unexpected exception in respondBlock") } - self.respondBlock = nil + self.respondBlock = nextRespondBlock + nextRespondBlock = nil } } diff --git a/FirebaseAuth/Tests/Unit/GetRecaptchaConfigTests.swift b/FirebaseAuth/Tests/Unit/GetRecaptchaConfigTests.swift new file mode 100644 index 00000000000..eddb6f458d0 --- /dev/null +++ b/FirebaseAuth/Tests/Unit/GetRecaptchaConfigTests.swift @@ -0,0 +1,54 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License") +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import Foundation +import XCTest + +@testable import FirebaseAuth + +@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) +class GetRecaptchaConfigTests: RPCBaseTests { + /** @fn testGetRecaptchaConfigRequest + @brief Tests get Recaptcha config request. + */ + func testGetRecaptchaConfigRequest() async throws { + let request = GetRecaptchaConfigRequest(requestConfiguration: makeRequestConfiguration()) + // let _ = try await AuthBackend.call(with: request) + XCTAssertFalse(request.containsPostBody()) + + // Confirm that the request has no decoded body as it is get request. + XCTAssertNil(rpcIssuer.decodedRequest) + let urlString = "https://identitytoolkit.googleapis.com/v2/recaptchaConfig?key=\(kTestAPIKey)" + + "&clientType=CLIENT_TYPE_IOS&version=RECAPTCHA_ENTERPRISE" + try await checkRequest( + request: request, + expected: urlString, + key: "should_be_empty_dictionary", + value: nil + ) + } + + /** @fn testSuccessfulGetRecaptchaConfigRequest + @brief This test simulates a successful @c getRecaptchaConfig Flow. + */ + func testSuccessfulGetRecaptchaConfigRequest() async throws { + let kTestRecaptchaKey = "projects/123/keys/456" + let request = GetRecaptchaConfigRequest(requestConfiguration: makeRequestConfiguration()) + + rpcIssuer.recaptchaSiteKey = kTestRecaptchaKey + let response = try await AuthBackend.call(with: request) + XCTAssertEqual(response.recaptchaKey, kTestRecaptchaKey) + XCTAssertNil(response.enforcementState) + } +} diff --git a/FirebaseAuth/Tests/Unit/SignUpNewUserTests.swift b/FirebaseAuth/Tests/Unit/SignUpNewUserTests.swift index 818e2882e79..ea60663773b 100644 --- a/FirebaseAuth/Tests/Unit/SignUpNewUserTests.swift +++ b/FirebaseAuth/Tests/Unit/SignUpNewUserTests.swift @@ -132,7 +132,6 @@ class SignUpNewUserTests: RPCBaseTests { let kPasswordKey = "password" let kCaptchaResponseKey = "captchaResponse" let kTestCaptchaResponse = "testCaptchaResponse" - let kTestPendingToken = "testPendingToken" let kClientTypeKey = "clientType" let kTestClientType = "testClientType" let kRecaptchaVersionKey = "recaptchaVersion" From 88fcd3b7f8009420fc30cae44759ad9bd93015a9 Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Wed, 25 Oct 2023 16:26:33 -0700 Subject: [PATCH 6/8] clean up and macos fix --- .../RPC/FIRGetRecaptchaConfigRequest.h | 45 ---- .../RPC/FIRGetRecaptchaConfigRequest.m | 74 ------ .../RPC/FIRGetRecaptchaConfigResponse.h | 40 ---- .../RPC/FIRGetRecaptchaConfigResponse.m | 31 --- FirebaseAuth/Tests/Unit/AuthTests.swift | 210 +++++++++--------- 5 files changed, 106 insertions(+), 294 deletions(-) delete mode 100644 FirebaseAuth/Sources/Backend/RPC/FIRGetRecaptchaConfigRequest.h delete mode 100644 FirebaseAuth/Sources/Backend/RPC/FIRGetRecaptchaConfigRequest.m delete mode 100644 FirebaseAuth/Sources/Backend/RPC/FIRGetRecaptchaConfigResponse.h delete mode 100644 FirebaseAuth/Sources/Backend/RPC/FIRGetRecaptchaConfigResponse.m diff --git a/FirebaseAuth/Sources/Backend/RPC/FIRGetRecaptchaConfigRequest.h b/FirebaseAuth/Sources/Backend/RPC/FIRGetRecaptchaConfigRequest.h deleted file mode 100644 index 64c652fadd5..00000000000 --- a/FirebaseAuth/Sources/Backend/RPC/FIRGetRecaptchaConfigRequest.h +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2022 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 - -#import "FirebaseAuth/Sources/Backend/FIRAuthRPCRequest.h" -#import "FirebaseAuth/Sources/Backend/FIRIdentityToolkitRequest.h" - -NS_ASSUME_NONNULL_BEGIN - -/** @class FIRGetRecaptchaConfigRequest - @brief Represents the parameters for the getRecaptchaConfig endpoint. - */ -@interface FIRGetRecaptchaConfigRequest : FIRIdentityToolkitRequest - -/** @fn initWithEndpoint:requestConfiguration: - @brief Please use initWithClientType:version:requestConfiguration: - */ -- (nullable instancetype)initWithEndpoint:(NSString *)endpoint - requestConfiguration:(FIRAuthRequestConfiguration *)requestConfiguration - NS_UNAVAILABLE; - -/** @fn initWithEmail:password:requestConfiguration: - @brief Designated initializer. - @param requestConfiguration The config. - */ -- (nullable instancetype)initWithRequestConfiguration: - (FIRAuthRequestConfiguration *)requestConfiguration NS_DESIGNATED_INITIALIZER; - -@end - -NS_ASSUME_NONNULL_END diff --git a/FirebaseAuth/Sources/Backend/RPC/FIRGetRecaptchaConfigRequest.m b/FirebaseAuth/Sources/Backend/RPC/FIRGetRecaptchaConfigRequest.m deleted file mode 100644 index c913a44d1ef..00000000000 --- a/FirebaseAuth/Sources/Backend/RPC/FIRGetRecaptchaConfigRequest.m +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright 2022 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 "FirebaseAuth/Sources/Backend/RPC/FIRGetRecaptchaConfigRequest.h" - -NS_ASSUME_NONNULL_BEGIN - -static NSString *const kRecaptchaVersion = @"RECAPTCHA_ENTERPRISE"; - -/** @var kGetRecaptchaConfigEndpoint - @brief The "getRecaptchaConfig" endpoint. - */ -static NSString *const kGetRecaptchaConfigEndpoint = @"recaptchaConfig"; - -/** @var kClientType - @brief The key for the "clientType" value in the request. - */ -static NSString *const kClientTypeKey = @"clientType"; - -/** @var kVersionKey - @brief The key for the "version" value in the request. - */ -static NSString *const kVersionKey = @"version"; - -/** @var kTenantIDKey - @brief The key for the tenant id value in the request. - */ -static NSString *const kTenantIDKey = @"tenantId"; - -@implementation FIRGetRecaptchaConfigRequest - -- (nullable instancetype)initWithRequestConfiguration: - (nonnull FIRAuthRequestConfiguration *)requestConfiguration { - requestConfiguration.HTTPMethod = @"GET"; - self = [super initWithEndpoint:kGetRecaptchaConfigEndpoint - requestConfiguration:requestConfiguration]; - self.useIdentityPlatform = YES; - return self; -} - -- (BOOL)containsPostBody { - return NO; -} - -- (nullable NSString *)queryParams { - NSMutableString *queryParams = [[NSMutableString alloc] init]; - [queryParams appendFormat:@"&%@=%@&%@=%@", kClientTypeKey, self.clientType, kVersionKey, - kRecaptchaVersion]; - if (self.tenantID) { - [queryParams appendFormat:@"&%@=%@", kTenantIDKey, self.tenantID]; - } - return queryParams; -} - -- (nullable id)unencodedHTTPRequestBodyWithError:(NSError *_Nullable *_Nullable)error { - return nil; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/FirebaseAuth/Sources/Backend/RPC/FIRGetRecaptchaConfigResponse.h b/FirebaseAuth/Sources/Backend/RPC/FIRGetRecaptchaConfigResponse.h deleted file mode 100644 index dfa87093d2c..00000000000 --- a/FirebaseAuth/Sources/Backend/RPC/FIRGetRecaptchaConfigResponse.h +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2022 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 - -#import "FirebaseAuth/Sources/Backend/FIRAuthRPCResponse.h" - -NS_ASSUME_NONNULL_BEGIN - -/** @class FIRVerifyPasswordResponse - @brief Represents the response from the getRecaptchaConfig endpoint. - */ -@interface FIRGetRecaptchaConfigResponse : NSObject - -/** @property recaptchaKey - @brief The recaptcha key of the project. - */ -@property(nonatomic, copy, nullable) NSString *recaptchaKey; - -/** @property enforcementState - @brief The enforcement state array. - */ -@property(nonatomic, nullable) NSArray *enforcementState; - -@end - -NS_ASSUME_NONNULL_END diff --git a/FirebaseAuth/Sources/Backend/RPC/FIRGetRecaptchaConfigResponse.m b/FirebaseAuth/Sources/Backend/RPC/FIRGetRecaptchaConfigResponse.m deleted file mode 100644 index da39f8590b4..00000000000 --- a/FirebaseAuth/Sources/Backend/RPC/FIRGetRecaptchaConfigResponse.m +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright 2022 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 "FirebaseAuth/Sources/Backend/RPC/FIRGetRecaptchaConfigResponse.h" - -NS_ASSUME_NONNULL_BEGIN - -@implementation FIRGetRecaptchaConfigResponse - -- (BOOL)setWithDictionary:(NSDictionary *)dictionary error:(NSError *_Nullable *_Nullable)error { - _recaptchaKey = [dictionary[@"recaptchaKey"] copy]; - _enforcementState = dictionary[@"recaptchaEnforcementState"]; - return YES; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/FirebaseAuth/Tests/Unit/AuthTests.swift b/FirebaseAuth/Tests/Unit/AuthTests.swift index 2eb720fd05b..09f0c25b4a6 100644 --- a/FirebaseAuth/Tests/Unit/AuthTests.swift +++ b/FirebaseAuth/Tests/Unit/AuthTests.swift @@ -286,121 +286,123 @@ class AuthTests: RPCBaseTests { XCTAssertNil(auth?.currentUser) } - /** @fn testSignInWithEmailPasswordWithRecaptchaSuccess - @brief Tests the flow of a successful @c signInWithEmail:password:completion: call. - */ - func testSignInWithEmailPasswordWithRecaptchaSuccess() throws { - let kRefreshToken = "fakeRefreshToken" - let expectation = self.expectation(description: #function) - setFakeGetAccountProvider() - setFakeSecureTokenService() - - // 1. Setup respond block to test and fake send request. - rpcIssuer.respondBlock = { - // 2. Validate the created Request instance. - let request = try XCTUnwrap(self.rpcIssuer.request as? VerifyPasswordRequest) - XCTAssertEqual(request.email, self.kEmail) - XCTAssertEqual(request.password, self.kFakePassword) - XCTAssertEqual(request.apiKey, AuthTests.kFakeAPIKey) - XCTAssertTrue(request.returnSecureToken) - request.injectRecaptchaFields(recaptchaResponse: AuthTests.kFakeRecaptchaResponse, - recaptchaVersion: AuthTests.kFakeRecaptchaVersion) + #if os(iOS) + /** @fn testSignInWithEmailPasswordWithRecaptchaSuccess + @brief Tests the flow of a successful @c signInWithEmail:password:completion: call. + */ + func testSignInWithEmailPasswordWithRecaptchaSuccess() throws { + let kRefreshToken = "fakeRefreshToken" + let expectation = self.expectation(description: #function) + setFakeGetAccountProvider() + setFakeSecureTokenService() - // 3. Send the response from the fake backend. - try self.rpcIssuer.respond(withJSON: ["idToken": AuthTests.kAccessToken, - "email": self.kEmail, - "isNewUser": true, - "refreshToken": kRefreshToken]) - } + // 1. Setup respond block to test and fake send request. + rpcIssuer.respondBlock = { + // 2. Validate the created Request instance. + let request = try XCTUnwrap(self.rpcIssuer.request as? VerifyPasswordRequest) + XCTAssertEqual(request.email, self.kEmail) + XCTAssertEqual(request.password, self.kFakePassword) + XCTAssertEqual(request.apiKey, AuthTests.kFakeAPIKey) + XCTAssertTrue(request.returnSecureToken) + request.injectRecaptchaFields(recaptchaResponse: AuthTests.kFakeRecaptchaResponse, + recaptchaVersion: AuthTests.kFakeRecaptchaVersion) - try auth?.signOut() - 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 { - XCTFail("authResult.user is missing") - return + // 3. Send the response from the fake backend. + try self.rpcIssuer.respond(withJSON: ["idToken": AuthTests.kAccessToken, + "email": self.kEmail, + "isNewUser": true, + "refreshToken": kRefreshToken]) } - XCTAssertEqual(user.refreshToken, kRefreshToken) - XCTAssertFalse(user.isAnonymous) - XCTAssertEqual(user.email, self.kEmail) - guard let additionalUserInfo = authResult?.additionalUserInfo else { - XCTFail("authResult.additionalUserInfo is missing") - return + + try auth?.signOut() + 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 { + XCTFail("authResult.user is missing") + return + } + XCTAssertEqual(user.refreshToken, kRefreshToken) + XCTAssertFalse(user.isAnonymous) + XCTAssertEqual(user.email, self.kEmail) + guard let additionalUserInfo = authResult?.additionalUserInfo else { + XCTFail("authResult.additionalUserInfo is missing") + return + } + XCTAssertFalse(additionalUserInfo.isNewUser) + XCTAssertEqual(additionalUserInfo.providerID, EmailAuthProvider.id) + XCTAssertNil(error) + expectation.fulfill() } - XCTAssertFalse(additionalUserInfo.isNewUser) - XCTAssertEqual(additionalUserInfo.providerID, EmailAuthProvider.id) - XCTAssertNil(error) - expectation.fulfill() + waitForExpectations(timeout: 5) + assertUser(auth?.currentUser) } - waitForExpectations(timeout: 5) - assertUser(auth?.currentUser) - } - /** @fn testSignInWithEmailPasswordWithRecaptchaFallbackSuccess - @brief Tests the flow of a successful @c signInWithEmail:password:completion: call. - */ - func testSignInWithEmailPasswordWithRecaptchaFallbackSuccess() throws { - let kRefreshToken = "fakeRefreshToken" - let expectation = self.expectation(description: #function) - setFakeGetAccountProvider() - setFakeSecureTokenService() - let kTestRecaptchaKey = "projects/123/keys/456" - rpcIssuer.recaptchaSiteKey = kTestRecaptchaKey - - // 1. Setup respond block to test and fake send request. - rpcIssuer.respondBlock = { - // 2. Validate the created Request instance. - let request = try XCTUnwrap(self.rpcIssuer.request as? VerifyPasswordRequest) - XCTAssertEqual(request.email, self.kEmail) - XCTAssertEqual(request.password, self.kFakePassword) - XCTAssertEqual(request.apiKey, AuthTests.kFakeAPIKey) - XCTAssertTrue(request.returnSecureToken) - request.injectRecaptchaFields(recaptchaResponse: AuthTests.kFakeRecaptchaResponse, - recaptchaVersion: AuthTests.kFakeRecaptchaVersion) + /** @fn testSignInWithEmailPasswordWithRecaptchaFallbackSuccess + @brief Tests the flow of a successful @c signInWithEmail:password:completion: call. + */ + func testSignInWithEmailPasswordWithRecaptchaFallbackSuccess() throws { + let kRefreshToken = "fakeRefreshToken" + let expectation = self.expectation(description: #function) + setFakeGetAccountProvider() + setFakeSecureTokenService() + let kTestRecaptchaKey = "projects/123/keys/456" + rpcIssuer.recaptchaSiteKey = kTestRecaptchaKey - // 3. Send the response from the fake backend. - try self.rpcIssuer.respond(serverErrorMessage: "MISSING_RECAPTCHA_TOKEN") - } - rpcIssuer.nextRespondBlock = { - // 4. Validate again the created Request instance after the recaptcha retry. - let request = try XCTUnwrap(self.rpcIssuer.request as? VerifyPasswordRequest) - XCTAssertEqual(request.email, self.kEmail) - XCTAssertEqual(request.password, self.kFakePassword) - XCTAssertEqual(request.apiKey, AuthTests.kFakeAPIKey) - XCTAssertTrue(request.returnSecureToken) - request.injectRecaptchaFields(recaptchaResponse: AuthTests.kFakeRecaptchaResponse, - recaptchaVersion: AuthTests.kFakeRecaptchaVersion) - // 5. Send the response from the fake backend. - try self.rpcIssuer.respond(withJSON: ["idToken": AuthTests.kAccessToken, - "email": self.kEmail, - "isNewUser": true, - "refreshToken": kRefreshToken]) - } + // 1. Setup respond block to test and fake send request. + rpcIssuer.respondBlock = { + // 2. Validate the created Request instance. + let request = try XCTUnwrap(self.rpcIssuer.request as? VerifyPasswordRequest) + XCTAssertEqual(request.email, self.kEmail) + XCTAssertEqual(request.password, self.kFakePassword) + XCTAssertEqual(request.apiKey, AuthTests.kFakeAPIKey) + XCTAssertTrue(request.returnSecureToken) + request.injectRecaptchaFields(recaptchaResponse: AuthTests.kFakeRecaptchaResponse, + recaptchaVersion: AuthTests.kFakeRecaptchaVersion) - try auth?.signOut() - auth?.signIn(withEmail: kEmail, password: kFakePassword) { authResult, error in - // 6. After the response triggers the callback, verify the returned result. - XCTAssertTrue(Thread.isMainThread) - XCTAssertNil(error) - guard let user = authResult?.user else { - XCTFail("authResult.user is missing") - return + // 3. Send the response from the fake backend. + try self.rpcIssuer.respond(serverErrorMessage: "MISSING_RECAPTCHA_TOKEN") } - XCTAssertEqual(user.refreshToken, kRefreshToken) - XCTAssertFalse(user.isAnonymous) - XCTAssertEqual(user.email, self.kEmail) - guard let additionalUserInfo = authResult?.additionalUserInfo else { - XCTFail("authResult.additionalUserInfo is missing") - return + rpcIssuer.nextRespondBlock = { + // 4. Validate again the created Request instance after the recaptcha retry. + let request = try XCTUnwrap(self.rpcIssuer.request as? VerifyPasswordRequest) + XCTAssertEqual(request.email, self.kEmail) + XCTAssertEqual(request.password, self.kFakePassword) + XCTAssertEqual(request.apiKey, AuthTests.kFakeAPIKey) + XCTAssertTrue(request.returnSecureToken) + request.injectRecaptchaFields(recaptchaResponse: AuthTests.kFakeRecaptchaResponse, + recaptchaVersion: AuthTests.kFakeRecaptchaVersion) + // 5. Send the response from the fake backend. + try self.rpcIssuer.respond(withJSON: ["idToken": AuthTests.kAccessToken, + "email": self.kEmail, + "isNewUser": true, + "refreshToken": kRefreshToken]) } - XCTAssertFalse(additionalUserInfo.isNewUser) - XCTAssertEqual(additionalUserInfo.providerID, EmailAuthProvider.id) - expectation.fulfill() + + try auth?.signOut() + auth?.signIn(withEmail: kEmail, password: kFakePassword) { authResult, error in + // 6. After the response triggers the callback, verify the returned result. + XCTAssertTrue(Thread.isMainThread) + XCTAssertNil(error) + guard let user = authResult?.user else { + XCTFail("authResult.user is missing") + return + } + XCTAssertEqual(user.refreshToken, kRefreshToken) + XCTAssertFalse(user.isAnonymous) + XCTAssertEqual(user.email, self.kEmail) + guard let additionalUserInfo = authResult?.additionalUserInfo else { + XCTFail("authResult.additionalUserInfo is missing") + return + } + XCTAssertFalse(additionalUserInfo.isNewUser) + XCTAssertEqual(additionalUserInfo.providerID, EmailAuthProvider.id) + expectation.fulfill() + } + waitForExpectations(timeout: 5) + assertUser(auth?.currentUser) } - waitForExpectations(timeout: 5) - assertUser(auth?.currentUser) - } + #endif /** @fn testSignInAndRetrieveDataWithEmailPasswordSuccess @brief Tests the flow of a successful @c signInAndRetrieveDataWithEmail:password:completion: From a9cd3ae187eaee28c887c09f4def6a320a4cdffa Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Thu, 26 Oct 2023 10:13:38 -0700 Subject: [PATCH 7/8] Roll in one-line fix from #11698 --- .../Sources/Swift/Backend/RPC/GetProjectConfigRequest.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/FirebaseAuth/Sources/Swift/Backend/RPC/GetProjectConfigRequest.swift b/FirebaseAuth/Sources/Swift/Backend/RPC/GetProjectConfigRequest.swift index dec9ed293f9..33c9b442d21 100644 --- a/FirebaseAuth/Sources/Swift/Backend/RPC/GetProjectConfigRequest.swift +++ b/FirebaseAuth/Sources/Swift/Backend/RPC/GetProjectConfigRequest.swift @@ -24,6 +24,7 @@ class GetProjectConfigRequest: IdentityToolkitRequest, AuthRPCRequest { typealias Response = GetProjectConfigResponse init(requestConfiguration: AuthRequestConfiguration) { + requestConfiguration.httpMethod = "GET" super.init(endpoint: kGetProjectConfigEndPoint, requestConfiguration: requestConfiguration) } From 993e56ef6e2d542be8f17625933abcf881e7b02b Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Thu, 26 Oct 2023 13:14:50 -0700 Subject: [PATCH 8/8] review --- FirebaseAuth/Sources/Swift/Auth/Auth.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FirebaseAuth/Sources/Swift/Auth/Auth.swift b/FirebaseAuth/Sources/Swift/Auth/Auth.swift index 544ee7198d5..cca35725a10 100644 --- a/FirebaseAuth/Sources/Swift/Auth/Auth.swift +++ b/FirebaseAuth/Sources/Swift/Auth/Auth.swift @@ -1275,7 +1275,7 @@ extension Auth: AuthInterop { ) #if os(iOS) self.wrapInjectRecaptcha(request: request, - action: AuthRecaptchaAction.signInWithPassword) { result, error in + action: AuthRecaptchaAction.getOobCode) { result, error in if let completion { DispatchQueue.main.async { completion(error)