mirror of
https://github.com/firebase/firebase-ios-sdk
synced 2026-11-10 22:45:59 +05:45
134 lines
5.5 KiB
Swift
134 lines
5.5 KiB
Swift
// Copyright 2026 Google LLC
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
import Foundation
|
|
import XCTest
|
|
|
|
@testable import FirebaseAuth
|
|
import FirebaseCore
|
|
|
|
/// Tests that malformed multi-factor data from the backend is rejected or skipped instead of
|
|
/// crashing.
|
|
class MFAResponseDecodingTests: RPCBaseTests {
|
|
/** @fn testStartMFAEnrollmentResponseWithTOTPSessionInfo
|
|
@brief A TOTP session info with a shared secret key is decoded.
|
|
*/
|
|
func testStartMFAEnrollmentResponseWithTOTPSessionInfo() throws {
|
|
let response = try StartMFAEnrollmentResponse(dictionary: [
|
|
"totpSessionInfo": [
|
|
"sharedSecretKey": "SECRET",
|
|
"verificationCodeLength": 6,
|
|
"hashingAlgorithm": "SHA1",
|
|
"periodSec": 30,
|
|
"sessionInfo": "SESSION",
|
|
] as [String: AnyHashable],
|
|
])
|
|
let totpSessionInfo = try XCTUnwrap(response.totpSessionInfo)
|
|
XCTAssertEqual(totpSessionInfo.sharedSecretKey, "SECRET")
|
|
XCTAssertEqual(totpSessionInfo.verificationCodeLength, 6)
|
|
XCTAssertEqual(totpSessionInfo.hashingAlgorithm, "SHA1")
|
|
XCTAssertEqual(totpSessionInfo.periodSec, 30)
|
|
XCTAssertEqual(totpSessionInfo.sessionInfo, "SESSION")
|
|
}
|
|
|
|
/** @fn testStartMFAEnrollmentResponseWithoutSharedSecretKeyThrows
|
|
@brief A TOTP session info whose shared secret key is missing or isn't a string is rejected
|
|
with an error instead of crashing.
|
|
*/
|
|
func testStartMFAEnrollmentResponseWithoutSharedSecretKeyThrows() {
|
|
// `nil` leaves `sharedSecretKey` out of the response.
|
|
let sharedSecretKeys: [AnyHashable?] = [nil, NSNull(), 123]
|
|
for sharedSecretKey in sharedSecretKeys {
|
|
var totpSessionInfo: [String: AnyHashable] = ["verificationCodeLength": 6]
|
|
totpSessionInfo["sharedSecretKey"] = sharedSecretKey
|
|
XCTAssertThrowsError(
|
|
try StartMFAEnrollmentResponse(dictionary: ["totpSessionInfo": totpSessionInfo])
|
|
) { error in
|
|
XCTAssertEqual((error as NSError).code, AuthErrorCode.internalError.rawValue)
|
|
}
|
|
}
|
|
}
|
|
|
|
#if os(iOS) || os(macOS)
|
|
private let kAppName = "test-MFAResponseDecodingTests"
|
|
private var auth: Auth?
|
|
|
|
/// Two valid enrollments, a phone and a TOTP enrollment without an enrollment ID, and an
|
|
/// enrollment of an unsupported type (neither `phoneInfo` nor `totpInfo`).
|
|
private let mixedEnrollmentsJSON: [[String: AnyHashable]] = [
|
|
["phoneInfo": "+15555550100", "mfaEnrollmentId": "phone-id"],
|
|
["phoneInfo": "+15555550101"],
|
|
["totpInfo": [String: AnyHashable](), "mfaEnrollmentId": "totp-id"],
|
|
["totpInfo": [String: AnyHashable]()],
|
|
["mfaEnrollmentId": "unsupported-id"],
|
|
]
|
|
|
|
override func setUp() {
|
|
super.setUp()
|
|
let options = FirebaseOptions(googleAppID: "0:0000000000000:ios:0000000000000000",
|
|
gcmSenderID: "00000000000000000-00000000000-000000000")
|
|
options.apiKey = "FAKE_API_KEY"
|
|
options.projectID = "myProjectID"
|
|
FirebaseApp.configure(name: kAppName, options: options)
|
|
auth = Auth(app: FirebaseApp.app(name: kAppName)!,
|
|
keychainStorageProvider: FakeAuthKeychainStorage(),
|
|
backend: authBackend)
|
|
}
|
|
|
|
override func tearDown() {
|
|
auth = nil
|
|
FirebaseApp.resetApps()
|
|
super.tearDown()
|
|
}
|
|
|
|
/** @fn testMultiFactorSkipsEnrollmentsWithoutEnrollmentID
|
|
@brief Enrolled factors without an enrollment ID are skipped instead of crashing, and the
|
|
valid factors are kept.
|
|
*/
|
|
func testMultiFactorSkipsEnrollmentsWithoutEnrollmentID() {
|
|
let enrollments = mixedEnrollmentsJSON.map { AuthProtoMFAEnrollment(dictionary: $0) }
|
|
let multiFactor = MultiFactor(withMFAEnrollments: enrollments)
|
|
XCTAssertEqual(multiFactor.enrolledFactors.map(\.uid), ["phone-id", "totp-id"])
|
|
}
|
|
|
|
/** @fn testSecondFactorRequiredErrorSkipsHintsWithoutEnrollmentID
|
|
@brief When sign-in requires a second factor, hints without an enrollment ID are skipped
|
|
instead of crashing, and the valid hints are kept.
|
|
*/
|
|
func testSecondFactorRequiredErrorSkipsHintsWithoutEnrollmentID() async throws {
|
|
let auth = try XCTUnwrap(auth)
|
|
rpcIssuer.respondBlock = {
|
|
try self.rpcIssuer.respond(withJSON: [
|
|
"mfaPendingCredential": "PENDING_CREDENTIAL",
|
|
"mfaInfo": self.mixedEnrollmentsJSON,
|
|
])
|
|
}
|
|
let request = VerifyPasswordRequest(email: kEmail,
|
|
password: kFakePassword,
|
|
requestConfiguration: auth.requestConfiguration)
|
|
do {
|
|
_ = try await authBackend.call(with: request)
|
|
XCTFail("Expected secondFactorRequired")
|
|
} catch {
|
|
let nsError = error as NSError
|
|
XCTAssertEqual(nsError.code, AuthErrorCode.secondFactorRequired.rawValue)
|
|
let resolver = try XCTUnwrap(
|
|
nsError.userInfo[AuthErrors.userInfoMultiFactorResolverKey] as? MultiFactorResolver
|
|
)
|
|
XCTAssertEqual(resolver.hints.map(\.uid), ["phone-id", "totp-id"])
|
|
}
|
|
}
|
|
#endif
|
|
}
|