fix(account): harden session refresh recovery

Persist refresh operation identity across failures and validate revoked Apple credentials so clients recover without unsafe token rotation.
This commit is contained in:
Rocky
2026-08-25 16:43:47 +08:00
parent 57844ce615
commit 309d743fd3
16 changed files with 438 additions and 33 deletions
@@ -8,12 +8,22 @@ import Foundation
actor InMemoryAccountSecurityStore: AccountSessionVault, AppAttestKeyStateStoring {
private(set) var session: AccountSession?
private(set) var refreshTransaction: AccountRefreshTransaction?
private(set) var keyState: AppAttestKeyState?
private(set) var clearSessionCount = 0
private(set) var clearRefreshTransactionCount = 0
private var remainingSessionSaveFailures: Int
init(session: AccountSession? = nil, keyState: AppAttestKeyState? = nil) {
init(
session: AccountSession? = nil,
refreshTransaction: AccountRefreshTransaction? = nil,
keyState: AppAttestKeyState? = nil,
sessionSaveFailures: Int = 0
) {
self.session = session
self.refreshTransaction = refreshTransaction
self.keyState = keyState
self.remainingSessionSaveFailures = sessionSaveFailures
}
func loadSession() async throws -> AccountSession? {
@@ -21,6 +31,10 @@ actor InMemoryAccountSecurityStore: AccountSessionVault, AppAttestKeyStateStorin
}
func saveSession(_ session: AccountSession) async throws {
if remainingSessionSaveFailures > 0 {
remainingSessionSaveFailures -= 1
throw AccountAPIError.secureStorage
}
self.session = session
}
@@ -29,6 +43,26 @@ actor InMemoryAccountSecurityStore: AccountSessionVault, AppAttestKeyStateStorin
clearSessionCount += 1
}
func beginRefreshTransaction(
refreshTokenDigest: String
) async throws -> AccountRefreshTransaction {
if let refreshTransaction,
refreshTransaction.refreshTokenDigest == refreshTokenDigest {
return refreshTransaction
}
let created = AccountRefreshTransaction(
refreshTokenDigest: refreshTokenDigest,
operationId: UUID()
)
refreshTransaction = created
return created
}
func clearRefreshTransaction() async throws {
refreshTransaction = nil
clearRefreshTransactionCount += 1
}
func loadAppAttestKeyState() async throws -> AppAttestKeyState? {
keyState
}