mirror of
https://github.com/google/app-check
synced 2026-11-09 22:05:52 +05:45
fix(backoff): allow retry when device clock moves backward (#137)
Signed-off-by: Nick Cooke <36927374+ncooke3@users.noreply.github.com> Co-authored-by: Andrew Heard <andrewheard@google.com>
This commit is contained in:
1 parent
37f72580ef
commit
4639d4e97e
5 files changed
+93
-4
No files matched your search
@@ -20,6 +20,11 @@ public class AppCheckCoreToken: NSObject, @unchecked Sendable {
|
||||
@objc public let expirationDate: Date
|
||||
@objc public let receivedAtDate: Date
|
||||
|
||||
/// Creates an App Check token.
|
||||
/// - Parameters:
|
||||
/// - token: A Firebase App Check token string.
|
||||
/// - expirationDate: The token's expiration date in the device's local clock time.
|
||||
/// - receivedAtDate: The date the token was received in the device's local clock time.
|
||||
@objc(initWithToken:expirationDate:receivedAtDate:)
|
||||
public init(token: String, expirationDate: Date, receivedAt receivedAtDate: Date) {
|
||||
self.token = token
|
||||
@@ -28,6 +33,10 @@ public class AppCheckCoreToken: NSObject, @unchecked Sendable {
|
||||
super.init()
|
||||
}
|
||||
|
||||
/// Creates an App Check token.
|
||||
/// - Parameters:
|
||||
/// - token: A Firebase App Check token string.
|
||||
/// - expirationDate: The token's expiration date in the device's local clock time.
|
||||
@objc(initWithToken:expirationDate:)
|
||||
public convenience init(token: String, expirationDate: Date) {
|
||||
self.init(token: token, expirationDate: expirationDate, receivedAt: Date())
|
||||
|
||||
@@ -141,6 +141,11 @@ public class AppCheckCoreBackoffWrapper: NSObject, AppCheckCoreBackoffWrapperPro
|
||||
private func hasTimeIntervalPassedSinceLastFailure(_ timeInterval: TimeInterval) -> Bool {
|
||||
guard let failureDate = lastFailure?.finishDate else { return true }
|
||||
let timeSinceFailure = dateProvider().timeIntervalSince(failureDate)
|
||||
// A negative interval means the device clock moved backward since the failure was recorded,
|
||||
// so `failureDate` can no longer bound the backoff window reliably.
|
||||
if timeSinceFailure < 0 {
|
||||
return true
|
||||
}
|
||||
return timeSinceFailure >= timeInterval
|
||||
}
|
||||
|
||||
|
||||
@@ -212,6 +212,76 @@ class AppCheckCoreBackoffWrapperTests: XCTestCase {
|
||||
}
|
||||
}
|
||||
|
||||
func testBackoffWhenClockMovesBackwardAllowsRetryAndReanchorsBackoff() async throws {
|
||||
// Simulate a failure recorded while the device clock was 48h ahead.
|
||||
let skew: TimeInterval = 48 * 60 * 60
|
||||
let realNow = Date()
|
||||
currentDate = realNow.addingTimeInterval(skew)
|
||||
|
||||
setUpOperationError()
|
||||
setUpErrorHandler(with: .exponential)
|
||||
|
||||
do {
|
||||
_ = try await backoffWrapper.applyBackoffToOperation(
|
||||
operationProvider,
|
||||
errorHandler: errorHandler
|
||||
)
|
||||
XCTFail("Expected error")
|
||||
} catch {
|
||||
XCTAssertEqual(error as NSError, operationResult as? NSError)
|
||||
}
|
||||
await fulfillment(of: [operationFinishExpectation, errorHandlerExpectation], timeout: 5.0)
|
||||
|
||||
// Clock is corrected back to real time (`timeSinceFailure` is now negative).
|
||||
// The wrapper should allow an immediate retry rather than locking out for 48h; if that retry
|
||||
// also fails, `finishDate` re-anchors to the corrected clock and normal backoff resumes.
|
||||
currentDate = realNow
|
||||
setUpOperationError()
|
||||
setUpErrorHandler(with: .exponential)
|
||||
|
||||
do {
|
||||
_ = try await backoffWrapper.applyBackoffToOperation(
|
||||
operationProvider,
|
||||
errorHandler: errorHandler
|
||||
)
|
||||
XCTFail("Expected error")
|
||||
} catch {
|
||||
XCTAssertEqual(error as NSError, operationResult as? NSError)
|
||||
}
|
||||
await fulfillment(of: [operationFinishExpectation, errorHandlerExpectation], timeout: 5.0)
|
||||
|
||||
// Subsequent call 0.5s later is now properly throttled against the re-anchored failure date.
|
||||
currentDate = realNow.addingTimeInterval(0.5)
|
||||
setUpOperationSuccess()
|
||||
setUpErrorHandler(with: .none)
|
||||
operationFinishExpectation.isInverted = true
|
||||
errorHandlerExpectation.isInverted = true
|
||||
|
||||
do {
|
||||
_ = try await backoffWrapper.applyBackoffToOperation(
|
||||
operationProvider,
|
||||
errorHandler: errorHandler
|
||||
)
|
||||
XCTFail("Expected backoff error")
|
||||
} catch {
|
||||
XCTAssertTrue(isBackoffError(error as NSError))
|
||||
}
|
||||
await fulfillment(of: [operationFinishExpectation, errorHandlerExpectation], timeout: 0.1)
|
||||
|
||||
// Once the re-anchored backoff window for attempt 2 ([2.0s, 3.0s)) elapses, retry succeeds.
|
||||
currentDate = realNow.addingTimeInterval(3.5)
|
||||
setUpOperationSuccess()
|
||||
setUpErrorHandler(with: .none)
|
||||
errorHandlerExpectation.isInverted = true
|
||||
|
||||
let result = try await backoffWrapper.applyBackoffToOperation(
|
||||
operationProvider,
|
||||
errorHandler: errorHandler
|
||||
)
|
||||
await fulfillment(of: [operationFinishExpectation, errorHandlerExpectation], timeout: 0.1)
|
||||
XCTAssertEqual(result as? NSObject, operationResult as? NSObject)
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private func setUpErrorHandler(with backoffType: AppCheckCoreBackoffType) {
|
||||
|
||||
@@ -132,6 +132,9 @@ class AppCheckCoreTests: XCTestCase {
|
||||
}
|
||||
|
||||
XCTAssertEqual(fakeAppCheckProvider.getTokenCallCount, 1)
|
||||
XCTAssertEqual(fakeStorage.lastSetToken, expectedToken)
|
||||
XCTAssertEqual(fakeTokenRefresher.updateWithRefreshResultCallCount, 1)
|
||||
XCTAssertEqual(fakeTokenDelegate.lastToken, expectedToken)
|
||||
}
|
||||
|
||||
func testGetToken_AppCheckProviderError() async {
|
||||
|
||||
+6
-4
@@ -12,14 +12,16 @@
|
||||
local storage. More sources may be added later; Swift `switch` statements
|
||||
over `AppCheckCoreDebugTokenSource` should include `@unknown default`.
|
||||
(#134)
|
||||
- [fixed] Fixed an issue where a cached token fetched while the device clock was ahead kept
|
||||
being served after the clock was corrected, because its stored dates inherited the offset.
|
||||
Cached tokens received in the future are now refreshed.
|
||||
(https://github.com/firebase/firebase-ios-sdk/issues/16939)
|
||||
- [fixed] Fixed an issue where a cached token fetched while the device clock was
|
||||
ahead kept being served after the clock was corrected, because its stored
|
||||
dates inherited the offset. Cached tokens received in the future are now
|
||||
refreshed. (https://github.com/firebase/firebase-ios-sdk/issues/16939)
|
||||
- [fixed] Fixed an issue where a debug token exchange using the
|
||||
`AppCheckDebugToken` or `FIRAAppCheckDebugToken` environment variable
|
||||
updated the locally generated debug token's registered state, which could
|
||||
suppress or repeat its registration instructions. (#135)
|
||||
- [fixed] Fixed an issue where provider request backoff could block retries if
|
||||
the device clock moved backward after a failed request. (#137)
|
||||
|
||||
# 12.0.0
|
||||
- [changed] Ported AppCheckCore from Objective-C to Swift.
|
||||
|
||||
Reference in new issue
Block a user