Skip to content
4 changes: 4 additions & 0 deletions FirebaseAuth/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# Unreleased
- [fixed] Improved robustness when parsing malformed reCAPTCHA config and
backend error details from the server. (#16728)

# 13.0.0
- [fixed] Fixed a timeout race in the APNs token manager that could leave concurrent phone
authentication requests waiting indefinitely.
Expand Down
10 changes: 5 additions & 5 deletions FirebaseAuth/Sources/Swift/Backend/AuthBackend.swift
Original file line number Diff line number Diff line change
Expand Up @@ -456,13 +456,13 @@ final class AuthBackend: AuthBackendProtocol {
message: serverDetailErrorMessage, credential: credential, email: email
)
default:
if let underlyingErrors = errorDictionary["errors"] as? [[String: String]] {
for underlyingError in underlyingErrors {
if let reason = underlyingError["reason"] {
if reason.starts(with: "keyInvalid") {
if let underlyingErrors = errorDictionary["errors"] as? [Any] {
for case let underlyingError as [String: Any] in underlyingErrors {
if let reason = underlyingError["reason"] as? String {
if reason.hasPrefix("keyInvalid") {
return AuthErrorUtils.invalidAPIKeyError()
}
if underlyingError["reason"] == "ipRefererBlocked" {
if reason == "ipRefererBlocked" {
return AuthErrorUtils.appNotAuthorizedError()
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@ struct GetRecaptchaConfigResponse: AuthRPCResponse {

init(dictionary: [String: AnyHashable]) throws {
recaptchaKey = dictionary["recaptchaKey"] as? String
enforcementState = dictionary["recaptchaEnforcementState"] as? [[String: String]]
// Parse each entry independently, keeping only string values, so that a single malformed
// entry or field from the server does not discard the entire enforcement state.
if let states = dictionary["recaptchaEnforcementState"] as? [Any] {
enforcementState = states.compactMap { state in
guard let state = state as? [String: Any] else { return nil }
return state.compactMapValues { $0 as? String }
}
}
Comment thread
paulb777 marked this conversation as resolved.
Resolved
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@
if isRecaptchaEnabled {
if let recaptchaKey = response.recaptchaKey {
let keys = recaptchaKey.components(separatedBy: "/")
if keys.count != 4 {
guard keys.count == 4, !keys[3].isEmpty else {
throw AuthErrorUtils.error(code: .recaptchaNotEnabled, message: "Invalid siteKey")
}
Comment thread
paulb777 marked this conversation as resolved.
Resolved
siteKey = keys[3]
Expand Down
34 changes: 34 additions & 0 deletions FirebaseAuth/Tests/Unit/AuthBackendTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -748,6 +748,40 @@ class AuthBackendTests: RPCBaseTests {
}
#endif // COCOAPODS || SWIFT_PACKAGE

/** @fn testUnderlyingErrorReasonWithMalformedSiblingFields
@brief Malformed entries or non-string fields in the server's `errors` array must not prevent
a known `reason` from being mapped to its client error.
*/
func testUnderlyingErrorReasonWithMalformedSiblingFields() async throws {
let cases: [(reason: String, expectedCode: AuthErrorCode)] = [
("keyInvalid", .invalidAPIKey),
("ipRefererBlocked", .appNotAuthorized),
]
for (reason, expectedCode) in cases {
let request = FakeRequest(withRequestBody: [:])
rpcIssuer.respondBlock = {
let errors: [Any] = [
NSNull(),
"garbage",
["reason": 42] as [String: Any],
["reason": reason, "domain": "global", "code": 400] as [String: Any],
]
return try self.rpcIssuer.respond(
withJSON: ["error": ["message": "See the reason", "errors": errors] as [String: Any]],
error: NSError(domain: self.kFakeErrorDomain, code: self.kFakeErrorCode)
)
}
do {
let _ = try await authBackend.call(with: request)
Comment thread
paulb777 marked this conversation as resolved.
Resolved
XCTFail("Expected to throw")
} catch {
let rpcError = error as NSError
XCTAssertEqual(rpcError.domain, AuthErrors.domain)
XCTAssertEqual(rpcError.code, expectedCode.rawValue)
}
}
}

private class FakeRequest: AuthRPCRequest {
typealias Response = FakeResponse

Expand Down
66 changes: 66 additions & 0 deletions FirebaseAuth/Tests/Unit/AuthTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@
return try self.rpcIssuer.respond(withJSON: ["signinMethods": allSignInMethods])
}

auth?.fetchSignInMethods(forEmail: kEmail) { signInMethods, error in

Check warning on line 89 in FirebaseAuth/Tests/Unit/AuthTests.swift

View workflow job for this annotation

GitHub Actions / spm / spm (xcode-27, macOS)

'fetchSignInMethods(forEmail:completion:)' is deprecated: `fetchSignInMethods` is deprecated and will be removed in a future release. This method returns an empty list when Email Enumeration Protection is enabled. [#DeprecatedDeclaration]

Check warning on line 89 in FirebaseAuth/Tests/Unit/AuthTests.swift

View workflow job for this annotation

GitHub Actions / spm / spm (macos-15, macOS)

'fetchSignInMethods(forEmail:completion:)' is deprecated: `fetchSignInMethods` is deprecated and will be removed in a future release. This method returns an empty list when Email Enumeration Protection is enabled.

Check warning on line 89 in FirebaseAuth/Tests/Unit/AuthTests.swift

View workflow job for this annotation

GitHub Actions / spm / spm (macos-26, macOS)

'fetchSignInMethods(forEmail:completion:)' is deprecated: `fetchSignInMethods` is deprecated and will be removed in a future release. This method returns an empty list when Email Enumeration Protection is enabled.

Check warning on line 89 in FirebaseAuth/Tests/Unit/AuthTests.swift

View workflow job for this annotation

GitHub Actions / spm / spm (macos-26, catalyst)

'fetchSignInMethods(forEmail:completion:)' is deprecated: `fetchSignInMethods` is deprecated and will be removed in a future release. This method returns an empty list when Email Enumeration Protection is enabled.

Check warning on line 89 in FirebaseAuth/Tests/Unit/AuthTests.swift

View workflow job for this annotation

GitHub Actions / spm / spm (xcode-27, tvOS)

'fetchSignInMethods(forEmail:completion:)' is deprecated: `fetchSignInMethods` is deprecated and will be removed in a future release. This method returns an empty list when Email Enumeration Protection is enabled. [#DeprecatedDeclaration]

Check warning on line 89 in FirebaseAuth/Tests/Unit/AuthTests.swift

View workflow job for this annotation

GitHub Actions / spm / spm (xcode-27, watchOS)

'fetchSignInMethods(forEmail:completion:)' is deprecated: `fetchSignInMethods` is deprecated and will be removed in a future release. This method returns an empty list when Email Enumeration Protection is enabled. [#DeprecatedDeclaration]

Check warning on line 89 in FirebaseAuth/Tests/Unit/AuthTests.swift

View workflow job for this annotation

GitHub Actions / spm / spm (xcode-27, iOS)

'fetchSignInMethods(forEmail:completion:)' is deprecated: `fetchSignInMethods` is deprecated and will be removed in a future release. This method returns an empty list when Email Enumeration Protection is enabled. [#DeprecatedDeclaration]

Check warning on line 89 in FirebaseAuth/Tests/Unit/AuthTests.swift

View workflow job for this annotation

GitHub Actions / spm / spm (macos-15, watchOS)

'fetchSignInMethods(forEmail:completion:)' is deprecated: `fetchSignInMethods` is deprecated and will be removed in a future release. This method returns an empty list when Email Enumeration Protection is enabled.

Check warning on line 89 in FirebaseAuth/Tests/Unit/AuthTests.swift

View workflow job for this annotation

GitHub Actions / spm / spm (macos-15, tvOS)

'fetchSignInMethods(forEmail:completion:)' is deprecated: `fetchSignInMethods` is deprecated and will be removed in a future release. This method returns an empty list when Email Enumeration Protection is enabled.

Check warning on line 89 in FirebaseAuth/Tests/Unit/AuthTests.swift

View workflow job for this annotation

GitHub Actions / spm / spm (macos-26, tvOS)

'fetchSignInMethods(forEmail:completion:)' is deprecated: `fetchSignInMethods` is deprecated and will be removed in a future release. This method returns an empty list when Email Enumeration Protection is enabled.

Check warning on line 89 in FirebaseAuth/Tests/Unit/AuthTests.swift

View workflow job for this annotation

GitHub Actions / spm / spm (xcode-27, visionOS)

'fetchSignInMethods(forEmail:completion:)' is deprecated: `fetchSignInMethods` is deprecated and will be removed in a future release. This method returns an empty list when Email Enumeration Protection is enabled. [#DeprecatedDeclaration]

Check warning on line 89 in FirebaseAuth/Tests/Unit/AuthTests.swift

View workflow job for this annotation

GitHub Actions / spm / spm (macos-26, visionOS)

'fetchSignInMethods(forEmail:completion:)' is deprecated: `fetchSignInMethods` is deprecated and will be removed in a future release. This method returns an empty list when Email Enumeration Protection is enabled.

Check warning on line 89 in FirebaseAuth/Tests/Unit/AuthTests.swift

View workflow job for this annotation

GitHub Actions / spm / spm (macos-26, watchOS)

'fetchSignInMethods(forEmail:completion:)' is deprecated: `fetchSignInMethods` is deprecated and will be removed in a future release. This method returns an empty list when Email Enumeration Protection is enabled.

Check warning on line 89 in FirebaseAuth/Tests/Unit/AuthTests.swift

View workflow job for this annotation

GitHub Actions / spm / spm (macos-15, visionOS)

'fetchSignInMethods(forEmail:completion:)' is deprecated: `fetchSignInMethods` is deprecated and will be removed in a future release. This method returns an empty list when Email Enumeration Protection is enabled.

Check warning on line 89 in FirebaseAuth/Tests/Unit/AuthTests.swift

View workflow job for this annotation

GitHub Actions / spm / spm (xcode-27, catalyst)

'fetchSignInMethods(forEmail:completion:)' is deprecated: `fetchSignInMethods` is deprecated and will be removed in a future release. This method returns an empty list when Email Enumeration Protection is enabled. [#DeprecatedDeclaration]
// 4. After the response triggers the callback, verify the returned signInMethods.
XCTAssertTrue(Thread.isMainThread)
XCTAssertEqual(signInMethods, allSignInMethods)
Expand All @@ -107,7 +107,7 @@
let message = "TOO_MANY_ATTEMPTS_TRY_LATER"
return try self.rpcIssuer.respond(serverErrorMessage: message)
}
auth?.fetchSignInMethods(forEmail: kEmail) { signInMethods, error in

Check warning on line 110 in FirebaseAuth/Tests/Unit/AuthTests.swift

View workflow job for this annotation

GitHub Actions / spm / spm (xcode-27, macOS)

'fetchSignInMethods(forEmail:completion:)' is deprecated: `fetchSignInMethods` is deprecated and will be removed in a future release. This method returns an empty list when Email Enumeration Protection is enabled. [#DeprecatedDeclaration]

Check warning on line 110 in FirebaseAuth/Tests/Unit/AuthTests.swift

View workflow job for this annotation

GitHub Actions / spm / spm (macos-15, macOS)

'fetchSignInMethods(forEmail:completion:)' is deprecated: `fetchSignInMethods` is deprecated and will be removed in a future release. This method returns an empty list when Email Enumeration Protection is enabled.

Check warning on line 110 in FirebaseAuth/Tests/Unit/AuthTests.swift

View workflow job for this annotation

GitHub Actions / spm / spm (macos-26, macOS)

'fetchSignInMethods(forEmail:completion:)' is deprecated: `fetchSignInMethods` is deprecated and will be removed in a future release. This method returns an empty list when Email Enumeration Protection is enabled.

Check warning on line 110 in FirebaseAuth/Tests/Unit/AuthTests.swift

View workflow job for this annotation

GitHub Actions / spm / spm (macos-26, catalyst)

'fetchSignInMethods(forEmail:completion:)' is deprecated: `fetchSignInMethods` is deprecated and will be removed in a future release. This method returns an empty list when Email Enumeration Protection is enabled.

Check warning on line 110 in FirebaseAuth/Tests/Unit/AuthTests.swift

View workflow job for this annotation

GitHub Actions / spm / spm (xcode-27, tvOS)

'fetchSignInMethods(forEmail:completion:)' is deprecated: `fetchSignInMethods` is deprecated and will be removed in a future release. This method returns an empty list when Email Enumeration Protection is enabled. [#DeprecatedDeclaration]

Check warning on line 110 in FirebaseAuth/Tests/Unit/AuthTests.swift

View workflow job for this annotation

GitHub Actions / spm / spm (xcode-27, watchOS)

'fetchSignInMethods(forEmail:completion:)' is deprecated: `fetchSignInMethods` is deprecated and will be removed in a future release. This method returns an empty list when Email Enumeration Protection is enabled. [#DeprecatedDeclaration]

Check warning on line 110 in FirebaseAuth/Tests/Unit/AuthTests.swift

View workflow job for this annotation

GitHub Actions / spm / spm (xcode-27, iOS)

'fetchSignInMethods(forEmail:completion:)' is deprecated: `fetchSignInMethods` is deprecated and will be removed in a future release. This method returns an empty list when Email Enumeration Protection is enabled. [#DeprecatedDeclaration]

Check warning on line 110 in FirebaseAuth/Tests/Unit/AuthTests.swift

View workflow job for this annotation

GitHub Actions / spm / spm (macos-15, watchOS)

'fetchSignInMethods(forEmail:completion:)' is deprecated: `fetchSignInMethods` is deprecated and will be removed in a future release. This method returns an empty list when Email Enumeration Protection is enabled.

Check warning on line 110 in FirebaseAuth/Tests/Unit/AuthTests.swift

View workflow job for this annotation

GitHub Actions / spm / spm (macos-15, tvOS)

'fetchSignInMethods(forEmail:completion:)' is deprecated: `fetchSignInMethods` is deprecated and will be removed in a future release. This method returns an empty list when Email Enumeration Protection is enabled.

Check warning on line 110 in FirebaseAuth/Tests/Unit/AuthTests.swift

View workflow job for this annotation

GitHub Actions / spm / spm (macos-26, tvOS)

'fetchSignInMethods(forEmail:completion:)' is deprecated: `fetchSignInMethods` is deprecated and will be removed in a future release. This method returns an empty list when Email Enumeration Protection is enabled.

Check warning on line 110 in FirebaseAuth/Tests/Unit/AuthTests.swift

View workflow job for this annotation

GitHub Actions / spm / spm (xcode-27, visionOS)

'fetchSignInMethods(forEmail:completion:)' is deprecated: `fetchSignInMethods` is deprecated and will be removed in a future release. This method returns an empty list when Email Enumeration Protection is enabled. [#DeprecatedDeclaration]

Check warning on line 110 in FirebaseAuth/Tests/Unit/AuthTests.swift

View workflow job for this annotation

GitHub Actions / spm / spm (macos-26, visionOS)

'fetchSignInMethods(forEmail:completion:)' is deprecated: `fetchSignInMethods` is deprecated and will be removed in a future release. This method returns an empty list when Email Enumeration Protection is enabled.

Check warning on line 110 in FirebaseAuth/Tests/Unit/AuthTests.swift

View workflow job for this annotation

GitHub Actions / spm / spm (macos-26, watchOS)

'fetchSignInMethods(forEmail:completion:)' is deprecated: `fetchSignInMethods` is deprecated and will be removed in a future release. This method returns an empty list when Email Enumeration Protection is enabled.

Check warning on line 110 in FirebaseAuth/Tests/Unit/AuthTests.swift

View workflow job for this annotation

GitHub Actions / spm / spm (macos-15, visionOS)

'fetchSignInMethods(forEmail:completion:)' is deprecated: `fetchSignInMethods` is deprecated and will be removed in a future release. This method returns an empty list when Email Enumeration Protection is enabled.

Check warning on line 110 in FirebaseAuth/Tests/Unit/AuthTests.swift

View workflow job for this annotation

GitHub Actions / spm / spm (xcode-27, catalyst)

'fetchSignInMethods(forEmail:completion:)' is deprecated: `fetchSignInMethods` is deprecated and will be removed in a future release. This method returns an empty list when Email Enumeration Protection is enabled. [#DeprecatedDeclaration]
XCTAssertTrue(Thread.isMainThread)
XCTAssertNil(signInMethods)
let rpcError = (error as? NSError)!
Expand Down Expand Up @@ -1411,6 +1411,72 @@
waitForExpectations(timeout: 5)
assertUser(auth?.currentUser)
}

private func freshRecaptchaVerifier() -> AuthRecaptchaVerifier {
AuthRecaptchaVerifier.setShared(AuthRecaptchaVerifier(), auth: auth)
return AuthRecaptchaVerifier.shared(auth: auth)
}

/** @fn testRecaptchaConfigIgnoresMalformedEnforcementStateEntries
@brief A malformed or unknown enforcement state entry must not discard the valid ones.
*/
func testRecaptchaConfigIgnoresMalformedEnforcementStateEntries() async throws {
rpcIssuer.fakeRecaptchaConfigJSON = [
"recaptchaKey": "projects/123/keys/456",
"recaptchaEnforcementState": [
["provider": "PHONE_PROVIDER", "enforcementState": "ENFORCE", "newField": 1]
as [String: Any],
["provider": "EMAIL_PASSWORD_PROVIDER", "enforcementState": "AUDIT"],
["provider": "UNKNOWN_PROVIDER", "enforcementState": "ENFORCE"],
["provider": "PHONE_PROVIDER", "enforcementState": "UNKNOWN_STATE"],
["provider": "PHONE_PROVIDER"],
"garbage",
NSNull(),
] as [Any],
]
let verifier = freshRecaptchaVerifier()
try await verifier.retrieveRecaptchaConfig(forceRefresh: true)
XCTAssertEqual(verifier.enablementStatus(forProvider: .phone), .enforce)
XCTAssertEqual(verifier.enablementStatus(forProvider: .password), .audit)
XCTAssertEqual(verifier.agentConfig?.siteKey, "456")
}

/** @fn testRecaptchaConfigWrongTypesFallsBackToOff
@brief Wrong-typed config fields are treated as missing and enforcement defaults to off.
*/
func testRecaptchaConfigWrongTypesFallsBackToOff() async throws {
rpcIssuer.fakeRecaptchaConfigJSON = [
"recaptchaKey": 123,
"recaptchaEnforcementState": ["provider": "PHONE_PROVIDER"],
]
let verifier = freshRecaptchaVerifier()
try await verifier.retrieveRecaptchaConfig(forceRefresh: true)
XCTAssertEqual(verifier.enablementStatus(forProvider: .phone), .off)
XCTAssertEqual(verifier.enablementStatus(forProvider: .password), .off)
}

/** @fn testRecaptchaConfigMalformedSiteKeyThrows
@brief Malformed recaptchaKey values are rejected with an error instead of crashing or
producing an empty site key.
*/
func testRecaptchaConfigMalformedSiteKeyThrows() async throws {
for recaptchaKey in ["", "456", "projects/123/keys/", "projects/123/keys/456/extra", "///"] {
Comment thread
paulb777 marked this conversation as resolved.
Resolved
rpcIssuer.fakeRecaptchaConfigJSON = [
"recaptchaKey": recaptchaKey,
"recaptchaEnforcementState": [
["provider": "PHONE_PROVIDER", "enforcementState": "ENFORCE"],
],
]
let verifier = freshRecaptchaVerifier()
do {
try await verifier.retrieveRecaptchaConfig(forceRefresh: true)
XCTFail("Expected an error for recaptchaKey '\(recaptchaKey)'")
} catch {
XCTAssertEqual((error as NSError).code, AuthErrorCode.recaptchaNotEnabled.rawValue)
}
XCTAssertNil(verifier.agentConfig)
}
}
#endif

/** @fn testCreateUserWithEmailPasswordSuccess
Expand Down
9 changes: 8 additions & 1 deletion FirebaseAuth/Tests/Unit/Fakes/FakeBackendRPCIssuer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ final class FakeBackendRPCIssuer: AuthBackendRPCIssuerProtocol, @unchecked Senda
var secureTokenErrorString: String?
var recaptchaSiteKey = "projects/fakeProjectId/keys/mockSiteKey"
var rceMode: String = "OFF"
/// If set, returned verbatim as the getRecaptchaConfig response, overriding `rceMode`.
var fakeRecaptchaConfigJSON: [String: Any]?

func asyncCallToURL<T>(with request: T, body: Data?,
contentType: String) async -> (Data?, Error?)
Expand Down Expand Up @@ -108,7 +110,12 @@ final class FakeBackendRPCIssuer: AuthBackendRPCIssuerProtocol, @unchecked Senda
}
return (data, error)
} else if let _ = request as? GetRecaptchaConfigRequest {
if rceMode != "OFF" { // Check if reCAPTCHA is enabled
if let fakeRecaptchaConfigJSON {
guard let (data, error) = try? respond(withJSON: fakeRecaptchaConfigJSON) else {
fatalError("fakeRecaptchaConfigJSON respond failed")
}
return (data, error)
} else if rceMode != "OFF" { // Check if reCAPTCHA is enabled
let recaptchaKey = recaptchaSiteKey // iOS key from your config
let enforcementState = [
["provider": "EMAIL_PASSWORD_PROVIDER", "enforcementState": rceMode],
Expand Down
62 changes: 62 additions & 0 deletions FirebaseAuth/Tests/Unit/GetRecaptchaConfigTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -76,4 +76,66 @@ class GetRecaptchaConfigTests: RPCBaseTests {
]
)
}

/** @fn testGetRecaptchaConfigResponseKeepsValidEntriesWhenSomeAreMalformed
@brief Malformed enforcement state entries or fields must not discard the valid entries.
*/
func testGetRecaptchaConfigResponseKeepsValidEntriesWhenSomeAreMalformed() async throws {
let request = GetRecaptchaConfigRequest(requestConfiguration: makeRequestConfiguration())
rpcIssuer.fakeRecaptchaConfigJSON = [
"recaptchaKey": "projects/123/keys/456",
"recaptchaEnforcementState": [
["provider": "PHONE_PROVIDER", "enforcementState": "ENFORCE", "newField": true]
as [String: Any],
["provider": NSNull(), "enforcementState": "AUDIT"] as [String: Any],
"not a dictionary",
42,
NSNull(),
["provider": "EMAIL_PASSWORD_PROVIDER", "enforcementState": "AUDIT"],
] as [Any],
]
let response = try await authBackend.call(with: request)
XCTAssertEqual(response.recaptchaKey, "projects/123/keys/456")
XCTAssertEqual(
response.enforcementState,
[
["provider": "PHONE_PROVIDER", "enforcementState": "ENFORCE"],
["enforcementState": "AUDIT"],
["provider": "EMAIL_PASSWORD_PROVIDER", "enforcementState": "AUDIT"],
]
)
}

/** @fn testGetRecaptchaConfigResponseWrongTypes
@brief Fields of the wrong type are treated as missing rather than failing the request.
*/
func testGetRecaptchaConfigResponseWrongTypes() async throws {
let request = GetRecaptchaConfigRequest(requestConfiguration: makeRequestConfiguration())
rpcIssuer.fakeRecaptchaConfigJSON = [
"recaptchaKey": 123,
"recaptchaEnforcementState": "ENFORCE",
]
let response = try await authBackend.call(with: request)
XCTAssertNil(response.recaptchaKey)
XCTAssertNil(response.enforcementState)
}

/** @fn testGetRecaptchaConfigResponseNullAndEmpty
@brief NSNull and empty values are handled without crashing.
*/
func testGetRecaptchaConfigResponseNullAndEmpty() async throws {
let request = GetRecaptchaConfigRequest(requestConfiguration: makeRequestConfiguration())
rpcIssuer.fakeRecaptchaConfigJSON = [
"recaptchaKey": NSNull(),
"recaptchaEnforcementState": [] as [Any],
] as [String: Any]
let response = try await authBackend.call(with: request)
XCTAssertNil(response.recaptchaKey)
XCTAssertEqual(response.enforcementState, [])

rpcIssuer.fakeRecaptchaConfigJSON = [:]
let emptyResponse = try await authBackend.call(with: request)
XCTAssertNil(emptyResponse.recaptchaKey)
XCTAssertNil(emptyResponse.enforcementState)
}
}
Loading