Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
8 changes: 5 additions & 3 deletions FirebaseAuth/Sources/Swift/SystemService/AuthAPNSToken.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@
#if !os(macOS)
import Foundation

// TODO(ncooke3): I believe this could be made a struct now.

/// A data structure for an APNs token.
class AuthAPNSToken {
final class AuthAPNSToken: Sendable {
let data: Data
let type: AuthAPNSTokenType

Expand All @@ -30,13 +32,13 @@
}

/// The uppercase hexadecimal string form of the APNs token data.
lazy var string: String = {
var string: String {
let byteArray = [UInt8](data)
var s = ""
for byte in byteArray {
s.append(String(format: "%02X", byte))
}
return s
}()
}
}
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -24,38 +24,41 @@

// Protocol to help with unit tests.
protocol AuthAPNSTokenApplication {
func registerForRemoteNotifications()
@MainActor func registerForRemoteNotifications()
}

extension UIApplication: AuthAPNSTokenApplication {}

/// A class to manage APNs token in memory.
@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
class AuthAPNSTokenManager {
class AuthAPNSTokenManager: @unchecked Sendable /* TODO: sendable */ {
/// The timeout for registering for remote notification.
///
/// Only tests should access this property.
var timeout: TimeInterval = 5
let timeout: TimeInterval

/// Initializes the instance.
/// - Parameter application: The `UIApplication` to request the token from.
/// - Returns: The initialized instance.
init(withApplication application: AuthAPNSTokenApplication) {
init(withApplication application: sending AuthAPNSTokenApplication, timeout: TimeInterval = 5) {
self.application = application
self.timeout = timeout
}

/// Attempts to get the APNs token.
/// - Parameter callback: The block to be called either immediately or in future, either when a
/// token becomes available, or when timeout occurs, whichever happens earlier.
///
/// This function is internal to make visible for tests.
func getTokenInternal(callback: @escaping (Result<AuthAPNSToken, Error>) -> Void) {
func getTokenInternal(callback: @escaping @Sendable (Result<AuthAPNSToken, Error>) -> Void) {
if let token = tokenStore {
callback(.success(token))
return
}
if pendingCallbacks.count > 0 {
pendingCallbacks.append(callback)
// TODO(ncooke3): This is likely a bug in that the async wrapper method
// cannot make forward progress.
return
}
pendingCallbacks = [callback]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
///
/// This enum is available on iOS, macOS Catalyst, tvOS, and watchOS only.

@objc(FIRAuthAPNSTokenType) public enum AuthAPNSTokenType: Int {
@objc(FIRAuthAPNSTokenType) public enum AuthAPNSTokenType: Int, Sendable {
/// Unknown token type.
///
/// The actual token type will be detected from the provisioning profile in the app's bundle.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,12 @@ import Foundation

/// A class represents a credential that proves the identity of the app.
@objc(FIRAuthAppCredential) // objc Needed for decoding old versions
class AuthAppCredential: NSObject, NSSecureCoding {
final class AuthAppCredential: NSObject, NSSecureCoding, Sendable {
/// The server acknowledgement of receiving client's claim of identity.
var receipt: String
let receipt: String

/// The secret that the client received from server via a trusted channel, if ever.
var secret: String?
let secret: String?

/// Initializes the instance.
/// - Parameter receipt: The server acknowledgement of receiving client's claim of identity.
Expand Down
40 changes: 27 additions & 13 deletions FirebaseAuth/Sources/Swift/SystemService/SecureTokenService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.

import Foundation
import FirebaseCoreInternal

private let kFiveMinutes = 5 * 60.0

Expand Down Expand Up @@ -114,12 +114,17 @@ actor SecureTokenServiceInternal {
/// 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) // objc Needed for decoding old versions
class SecureTokenService: NSObject, NSSecureCoding {
final class SecureTokenService: NSObject, NSSecureCoding, Sendable {
/// Internal actor to enforce serialization
private let internalService: SecureTokenServiceInternal

/// The configuration for making requests to server.
var requestConfiguration: AuthRequestConfiguration?
var requestConfiguration: AuthRequestConfiguration? {
get { _requestConfiguration.withLock { $0 } }
set { _requestConfiguration.withLock { $0 = newValue } }
}

let _requestConfiguration: FIRAllocatedUnfairLock<AuthRequestConfiguration?>

/// The cached access token.
///
Expand All @@ -130,20 +135,29 @@ class SecureTokenService: NSObject, NSSecureCoding {
/// - Note: The atomic wrapper can be removed when the SDK is fully
/// synchronized with structured concurrency.
var accessToken: String {
get { accessTokenLock.withLock { _accessToken } }
set { accessTokenLock.withLock { _accessToken = newValue } }
get { _accessToken.withLock { $0 } }
set { _accessToken.withLock { $0 = newValue } }
}

private var _accessToken: String
private let accessTokenLock = NSLock()
private let _accessToken: FIRAllocatedUnfairLock<String>

/// The refresh token for the user, or `nil` if the user has yet completed sign-in flow.
///
/// This property needs to be set manually after the instance is decoded from archive.
var refreshToken: String?
var refreshToken: String? {
get { _refreshToken.withLock { $0 } }
set { _refreshToken.withLock { $0 = newValue } }
}

private let _refreshToken: FIRAllocatedUnfairLock<String?>

/// The expiration date of the cached access token.
var accessTokenExpirationDate: Date?
var accessTokenExpirationDate: Date? {
get { _accessTokenExpirationDate.withLock { $0 } }
set { _accessTokenExpirationDate.withLock { $0 = newValue } }
}

private let _accessTokenExpirationDate: FIRAllocatedUnfairLock<Date?>

/// Creates a `SecureTokenService` with access and refresh tokens.
/// - Parameter requestConfiguration: The configuration for making requests to server.
Expand All @@ -155,10 +169,10 @@ class SecureTokenService: NSObject, NSSecureCoding {
accessTokenExpirationDate: Date?,
refreshToken: String) {
internalService = SecureTokenServiceInternal()
self.requestConfiguration = requestConfiguration
_accessToken = accessToken
self.accessTokenExpirationDate = accessTokenExpirationDate
self.refreshToken = refreshToken
_requestConfiguration = FIRAllocatedUnfairLock(initialState: requestConfiguration)
_accessToken = FIRAllocatedUnfairLock(initialState: accessToken)
_accessTokenExpirationDate = FIRAllocatedUnfairLock(initialState: accessTokenExpirationDate)
_refreshToken = FIRAllocatedUnfairLock(initialState: refreshToken)
}

/// Fetch a fresh ephemeral access token for the ID associated with this instance. The token
Expand Down
47 changes: 27 additions & 20 deletions FirebaseAuth/Tests/Unit/AuthAPNSTokenManagerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import XCTest

@testable import FirebaseAuth
import FirebaseCoreInternal

@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
class AuthAPNSTokenManagerTests: XCTestCase {
Expand Down Expand Up @@ -61,10 +62,10 @@
func testCallback() throws {
let expectation = self.expectation(description: #function)
XCTAssertFalse(fakeApplication!.registerCalled)
var firstCallbackCalled = false
let firstCallbackCalled = FIRAllocatedUnfairLock(initialState: false)
let manager = try XCTUnwrap(manager)
manager.getTokenInternal { result in
firstCallbackCalled = true
firstCallbackCalled.withLock { $0 = true }
switch result {
case let .success(token):
XCTAssertEqual(token.data, self.data)
Expand All @@ -73,12 +74,12 @@
XCTFail("Unexpected error: \(error)")
}
}
XCTAssertFalse(firstCallbackCalled)
XCTAssertFalse(firstCallbackCalled.value())

// Add second callback, which is yet to be called either.
var secondCallbackCalled = false
let secondCallbackCalled = FIRAllocatedUnfairLock(initialState: false)
manager.getTokenInternal { result in
secondCallbackCalled = true
secondCallbackCalled.withLock { $0 = true }
switch result {
case let .success(token):
XCTAssertEqual(token.data, self.data)
Expand All @@ -87,25 +88,25 @@
XCTFail("Unexpected error: \(error)")
}
}
XCTAssertFalse(secondCallbackCalled)
XCTAssertFalse(secondCallbackCalled.value())

// Setting nil token shouldn't trigger either callbacks.
manager.token = nil
XCTAssertFalse(firstCallbackCalled)
XCTAssertFalse(secondCallbackCalled)
XCTAssertFalse(firstCallbackCalled.value())
XCTAssertFalse(secondCallbackCalled.value())
XCTAssertNil(manager.token)

// Setting a real token should trigger both callbacks.
manager.token = AuthAPNSToken(withData: data!, type: .sandbox)
XCTAssertTrue(firstCallbackCalled)
XCTAssertTrue(secondCallbackCalled)
XCTAssertTrue(firstCallbackCalled.value())
XCTAssertTrue(secondCallbackCalled.value())
XCTAssertEqual(manager.token?.data, data)
XCTAssertEqual(manager.token?.type, .sandbox)

// Add third callback, which should be called back immediately.
var thirdCallbackCalled = false
let thirdCallbackCalled = FIRAllocatedUnfairLock(initialState: false)
manager.getTokenInternal { result in
thirdCallbackCalled = true
thirdCallbackCalled.withLock { $0 = true }
switch result {
case let .success(token):
XCTAssertEqual(token.data, self.data)
Expand All @@ -114,7 +115,7 @@
XCTFail("Unexpected error: \(error)")
}
}
XCTAssertTrue(thirdCallbackCalled)
XCTAssertTrue(thirdCallbackCalled.value())

// In the main thread, Verify the that the fake `registerForRemoteNotifications` was called.
DispatchQueue.main.async {
Expand All @@ -129,9 +130,12 @@
*/
func testTimeout() throws {
// Set up timeout.
manager = AuthAPNSTokenManager(
withApplication: fakeApplication!,
timeout: kRegistrationTimeout
)
let manager = try XCTUnwrap(manager)
XCTAssertGreaterThan(try XCTUnwrap(manager.timeout), 0)
manager.timeout = kRegistrationTimeout

// Add callback to time out.
let expectation = self.expectation(description: #function)
Expand Down Expand Up @@ -166,25 +170,28 @@
*/
func testCancel() throws {
// Set up timeout.
manager = AuthAPNSTokenManager(
withApplication: fakeApplication!,
timeout: kRegistrationTimeout
)
let manager = try XCTUnwrap(manager)
XCTAssertGreaterThan(try XCTUnwrap(manager.timeout), 0)
manager.timeout = kRegistrationTimeout

// Add callback to cancel.
var callbackCalled = false
let callbackCalled = FIRAllocatedUnfairLock(initialState: false)
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
XCTAssertFalse(callbackCalled.value()) // verify callback is not called twice
callbackCalled.withLock { $0 = true }
}
XCTAssertFalse(callbackCalled)
XCTAssertFalse(callbackCalled.value())

// Call cancel.
// Call cancel.gi
manager.cancel(withError: error)

// In the main thread, Verify the that the fake `registerForRemoteNotifications` was called.
Expand Down
4 changes: 2 additions & 2 deletions FirebaseAuth/Tests/Unit/AuthTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2291,7 +2291,7 @@ class AuthTests: RPCBaseTests {

#if os(iOS)
func testAppDidRegisterForRemoteNotifications_APNSTokenUpdated() {
class FakeAuthTokenManager: AuthAPNSTokenManager {
class FakeAuthTokenManager: AuthAPNSTokenManager, @unchecked Sendable {
override var token: AuthAPNSToken? {
get {
return tokenStore
Expand All @@ -2310,7 +2310,7 @@ class AuthTests: RPCBaseTests {
}

func testAppDidFailToRegisterForRemoteNotifications_TokenManagerCancels() {
class FakeAuthTokenManager: AuthAPNSTokenManager {
class FakeAuthTokenManager: AuthAPNSTokenManager, @unchecked Sendable {
var cancelled = false
override func cancel(withError error: Error) {
cancelled = true
Expand Down
2 changes: 1 addition & 1 deletion FirebaseAuth/Tests/Unit/PhoneAuthProviderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -923,7 +923,7 @@
}
}

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