From fa31c88453896e1ae49f14a38f401c26935fb197 Mon Sep 17 00:00:00 2001 From: Thomas Rademaker Date: Fri, 6 Mar 2026 15:26:24 -0500 Subject: [PATCH] bug fixes --- Package.resolved | 2 +- Sources/CoreATProtocol/APEnvironment.swift | 1 + Sources/CoreATProtocol/Networking.swift | 51 +++++++++++++++++++--- 3 files changed, 48 insertions(+), 6 deletions(-) diff --git a/Package.resolved b/Package.resolved index 8be9680..1474430 100644 --- a/Package.resolved +++ b/Package.resolved @@ -16,7 +16,7 @@ "location" : "https://github.com/SparrowTek/NetworkingKit.git", "state" : { "branch" : "main", - "revision" : "453c81dc2edcafe63e44b17502e36fa2eb10ecfb" + "revision" : "d5a53afb86b64a70b3611590dec2dd21d6d84333" } }, { diff --git a/Sources/CoreATProtocol/APEnvironment.swift b/Sources/CoreATProtocol/APEnvironment.swift index 19e10f7..e161ba8 100644 --- a/Sources/CoreATProtocol/APEnvironment.swift +++ b/Sources/CoreATProtocol/APEnvironment.swift @@ -18,6 +18,7 @@ public class APEnvironment { public var tokenRefreshHandler: (@Sendable () async throws -> Bool)? public var dpopPrivateKey: ES256PrivateKey? public var dpopKeys: JWTKeyCollection? + public var dpopNonce: String? public let routerDelegate = APRouterDelegate() private init() {} diff --git a/Sources/CoreATProtocol/Networking.swift b/Sources/CoreATProtocol/Networking.swift index 94b660c..daafe98 100644 --- a/Sources/CoreATProtocol/Networking.swift +++ b/Sources/CoreATProtocol/Networking.swift @@ -78,12 +78,14 @@ public class APRouterDelegate: NetworkRouterDelegate { components?.fragment = nil let htu = components?.url?.absoluteString ?? url.absoluteString + let nonce = await APEnvironment.current.dpopNonce + let payload = DPoPProofPayload( htm: method, htu: htu, iat: .init(value: .now), jti: .init(value: UUID().uuidString), - nonce: nil + nonce: nonce ) var header = JWTHeader() @@ -111,7 +113,21 @@ public class APRouterDelegate: NetworkRouterDelegate { return try? await keys.sign(payload, header: header) } + public func didReceiveErrorResponse(_ response: HTTPURLResponse) async { + if let nonce = response.value(forHTTPHeaderField: "DPoP-Nonce") + ?? response.value(forHTTPHeaderField: "dpop-nonce") { + await storeDPoPNonce(nonce) + } + } + + @APActor + private func storeDPoPNonce(_ nonce: String) { + APEnvironment.current.dpopNonce = nonce + } + public func shouldRetry(error: Error, attempts: Int) async throws -> Bool { + guard attempts == 1 else { return false } + func refreshViaOAuth() async throws -> Bool { guard let handler = await APEnvironment.current.tokenRefreshHandler else { return false @@ -129,22 +145,47 @@ public class APRouterDelegate: NetworkRouterDelegate { return try await task.value } - if attempts == 1, - case .network(let networkError) = error as? AtError, + // Check for DPoP nonce challenge — retry immediately without token refresh + // since didReceiveErrorResponse already stored the nonce. + if let networkError = error as? NetworkError, + case .statusCode(_, let data, _) = networkError, + isDPoPNonceError(data) { + return true + } + + if case .network(let networkError) = error as? AtError, + case .statusCode(_, let data, _) = networkError, + isDPoPNonceError(data) { + return true + } + + // Token expired or unauthorized — attempt OAuth refresh + if case .network(let networkError) = error as? AtError, case .statusCode(let statusCode, _, _) = networkError, let statusCode = statusCode?.rawValue, statusCode == 401 || statusCode == 403 { return try await refreshViaOAuth() } + if case .statusCode(let statusCode, _, _) = error as? NetworkError, + let statusCode = statusCode?.rawValue, + statusCode == 401 || statusCode == 403 { + return try await refreshViaOAuth() + } + if case .message(let message) = error as? AtError, - message.error == AtErrorType.expiredToken.rawValue, - attempts == 1 { + message.error == AtErrorType.expiredToken.rawValue { return try await refreshViaOAuth() } return false } + + private func isDPoPNonceError(_ data: Data) -> Bool { + guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any], + let error = json["error"] as? String else { return false } + return error == "use_dpop_nonce" + } } // MARK: - DPoP Proof Payload -- 2.51.2