Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Auth] Fix async/await crash from implicitly unwrapped nil error #13472

Merged
merged 7 commits into from
Aug 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions FirebaseAuth/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
- [added] Added custom provider support to `AuthProviderID`. Note that this change will be breaking
to any code that implemented an exhaustive `switch` on `AuthProviderID` in 11.0.0 - the `switch`
will need expansion. (#13429)
- [fixed] Fix crash introduced in 11.0.0 in phone authentication flow from
implicitly unwrapping `nil` error after a token timeout. (#13470)

# 11.0.0
- [fixed] Fixed auth domain matching code to prioritize matching `firebaseapp.com` over `web.app`
Expand Down
25 changes: 13 additions & 12 deletions FirebaseAuth/Sources/Swift/SystemService/AuthAPNSTokenManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,9 @@
/// token becomes available, or when timeout occurs, whichever happens earlier.
///
/// This function is internal to make visible for tests.
func getTokenInternal(callback: @escaping (AuthAPNSToken?, Error?) -> Void) {
func getTokenInternal(callback: @escaping (Result<AuthAPNSToken, Error>) -> Void) {
if let token = tokenStore {
callback(token, nil)
callback(.success(token))
return
}
if pendingCallbacks.count > 0 {
Expand All @@ -68,18 +68,19 @@
kAuthGlobalWorkQueue.asyncAfter(deadline: deadline) {
// Only cancel if the pending callbacks remain the same, i.e., not triggered yet.
if applicableCallbacks.count == self.pendingCallbacks.count {
self.callback(withToken: nil, error: nil)
self.callback(.failure(AuthErrorUtils.missingAppTokenError(underlyingError: nil)))
Copy link

@snapxo snapxo Aug 9, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ncooke3
With this AuthErrorUtils.missingAppTokenError(underlyingError: nil), the user of the SDK has no means to know that the request timed out, apart from implementing a timeout logic on their side, right?

Would it be a solution to add an underlying timeout Error?

Copy link
Member Author

@ncooke3 ncooke3 Aug 9, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @snapxo, that's correct that the timeout error doesn't bubble up. This getToken API is called internally and in the case of an error (e.g. like from timeout), it actually attempts the recaptcha flow rather than bubble the error up to the user:

do {
token = try await auth.tokenManager.getToken()
} catch {
return try await CodeIdentity
.recaptcha(reCAPTCHAFlowWithUIDelegate(withUIDelegate: uiDelegate))
}

Line 322 may throw the error you've highlighted above.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense if the fallback upon every missingToken error is the recaptcha 👌

}
}
}

func getToken() async throws -> AuthAPNSToken {
return try await withCheckedThrowingContinuation { continuation in
self.getTokenInternal { token, error in
if let token {
self.getTokenInternal { result in
switch result {
case let .success(token):
continuation.resume(returning: token)
} else {
continuation.resume(throwing: error!)
case let .failure(error):
continuation.resume(throwing: error)
}
}
}
Expand All @@ -105,7 +106,7 @@
newToken = AuthAPNSToken(withData: setToken.data, type: detectedTokenType)
}
tokenStore = newToken
callback(withToken: newToken, error: nil)
callback(.success(newToken))
}
}

Expand All @@ -115,18 +116,18 @@
/// Cancels any pending `getTokenWithCallback:` request.
/// - Parameter error: The error to return .
func cancel(withError error: Error) {
callback(withToken: nil, error: error)
callback(.failure(error))
}

/// Enable unit test faking.
var application: AuthAPNSTokenApplication
private var pendingCallbacks: [(AuthAPNSToken?, Error?) -> Void] = []
private var pendingCallbacks: [(Result<AuthAPNSToken, Error>) -> Void] = []

private func callback(withToken token: AuthAPNSToken?, error: Error?) {
private func callback(_ result: Result<AuthAPNSToken, Error>) {
let pendingCallbacks = self.pendingCallbacks
self.pendingCallbacks = []
for callback in pendingCallbacks {
callback(token, error)
callback(result)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -468,14 +468,14 @@ class AuthViewController: UIViewController, DataSourceProviderDelegate {
}

private func verifyClient() {
AppManager.shared.auth().tokenManager.getTokenInternal { token, error in
if token == nil {
AppManager.shared.auth().tokenManager.getTokenInternal { result in
guard case let .success(token) = result else {
print("Verify iOS Client failed.")
return
}
let request = VerifyClientRequest(
withAppToken: token?.string,
isSandbox: token?.type == .sandbox,
withAppToken: token.string,
isSandbox: token.type == .sandbox,
requestConfiguration: AppManager.shared.auth().requestConfiguration
)

Expand Down
59 changes: 41 additions & 18 deletions FirebaseAuth/Tests/Unit/AuthAPNSTokenManagerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -63,21 +63,29 @@
XCTAssertFalse(fakeApplication!.registerCalled)
var firstCallbackCalled = false
let manager = try XCTUnwrap(manager)
manager.getTokenInternal { token, error in
manager.getTokenInternal { result in
firstCallbackCalled = true
XCTAssertEqual(token?.data, self.data)
XCTAssertEqual(token?.type, .sandbox)
XCTAssertNil(error)
switch result {
case let .success(token):
XCTAssertEqual(token.data, self.data)
XCTAssertEqual(token.type, .sandbox)
case let .failure(error):
XCTFail("Unexpected error: \(error)")
}
}
XCTAssertFalse(firstCallbackCalled)

// Add second callback, which is yet to be called either.
var secondCallbackCalled = false
manager.getTokenInternal { token, error in
manager.getTokenInternal { result in
secondCallbackCalled = true
XCTAssertEqual(token?.data, self.data)
XCTAssertEqual(token?.type, .sandbox)
XCTAssertNil(error)
switch result {
case let .success(token):
XCTAssertEqual(token.data, self.data)
XCTAssertEqual(token.type, .sandbox)
case let .failure(error):
XCTFail("Unexpected error: \(error)")
}
}
XCTAssertFalse(secondCallbackCalled)

Expand All @@ -96,11 +104,15 @@

// Add third callback, which should be called back immediately.
var thirdCallbackCalled = false
manager.getTokenInternal { token, error in
manager.getTokenInternal { result in
thirdCallbackCalled = true
XCTAssertEqual(token?.data, self.data)
XCTAssertEqual(token?.type, .sandbox)
XCTAssertNil(error)
switch result {
case let .success(token):
XCTAssertEqual(token.data, self.data)
XCTAssertEqual(token.type, .sandbox)
case let .failure(error):
XCTFail("Unexpected error: \(error)")
}
}
XCTAssertTrue(thirdCallbackCalled)

Expand All @@ -123,9 +135,16 @@

// Add callback to time out.
let expectation = self.expectation(description: #function)
manager.getTokenInternal { token, error in
XCTAssertNil(token)
XCTAssertNil(error)
manager.getTokenInternal { result in
switch result {
case let .success(token):
XCTFail("Unexpected success: \(token)")
case let .failure(error):
XCTAssertEqual(
error as NSError,
AuthErrorUtils.missingAppTokenError(underlyingError: nil) as NSError
)
}
expectation.fulfill()
}
// Time out.
Expand Down Expand Up @@ -153,9 +172,13 @@

// Add callback to cancel.
var callbackCalled = false
manager.getTokenInternal { token, error in
XCTAssertNil(token)
XCTAssertEqual(error as? NSError, self.error as NSError)
manager.getTokenInternal { result in
switch result {
case let .success(token):
XCTFail("Unexpected success: \(token)")
case let .failure(error):
XCTAssertEqual(error as NSError, self.error as NSError)
}
XCTAssertFalse(callbackCalled) // verify callback is not called twice
callbackCalled = true
}
Expand Down
4 changes: 2 additions & 2 deletions FirebaseAuth/Tests/Unit/PhoneAuthProviderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -690,9 +690,9 @@
}

class FakeTokenManager: AuthAPNSTokenManager {
override func getTokenInternal(callback: @escaping (AuthAPNSToken?, Error?) -> Void) {
override func getTokenInternal(callback: @escaping (Result<AuthAPNSToken, Error>) -> Void) {
let error = NSError(domain: "dummy domain", code: AuthErrorCode.missingAppToken.rawValue)
callback(nil, error)
callback(.failure(error))
}
}

Expand Down
Loading