From ab739398cfb5efd7623a3565ad909256a3c28118 Mon Sep 17 00:00:00 2001 From: Matt Reeves Date: Wed, 14 Oct 2026 14:26:46 -0500 Subject: [PATCH] Handles pushed 2FA codes when Apple omits "au" Apple's GSA endpoint may now respond with status hsc 409 and no "au" field, meaning it already pushed a code to trusted devices. Treat this like trustedDeviceSecondaryAuth, but prompt for the code without requesting another, and fail instead of re-prompting if Apple asks again. Based on SideSign: https://github.com/SideStore/SideSign/pull/5 Co-Authored-By: Claude Opus 5.5 --- .../Sources/ALTAppleAPI+Authentication.swift | 55 ++++++++++++++++--- 1 file changed, 46 insertions(+), 9 deletions(-) diff --git a/AltSign/Sources/ALTAppleAPI+Authentication.swift b/AltSign/Sources/ALTAppleAPI+Authentication.swift index 2e5afe0e..438cfbb0 100644 --- a/AltSign/Sources/ALTAppleAPI+Authentication.swift +++ b/AltSign/Sources/ALTAppleAPI+Authentication.swift @@ -31,6 +31,20 @@ public extension ALTAppleAPI anisetteData: ALTAnisetteData, verificationHandler: ((@escaping (String?) -> Void) -> Void)?, completionHandler: @escaping (ALTAccount?, ALTAppleAPISession?, Error?) -> Void) + { + self.authenticate(appleID: unsanitizedAppleID, password: password, anisetteData: anisetteData, hasEnteredPushedCode: false, + verificationHandler: verificationHandler, completionHandler: completionHandler) + } +} + +private extension ALTAppleAPI +{ + func authenticate(appleID unsanitizedAppleID: String, + password: String, + anisetteData: ALTAnisetteData, + hasEnteredPushedCode: Bool, + verificationHandler: ((@escaping (String?) -> Void) -> Void)?, + completionHandler: @escaping (ALTAccount?, ALTAppleAPISession?, Error?) -> Void) { // Authenticating only works with lowercase email address, even if Apple ID contains capital letters. let sanitizedAppleID = unsanitizedAppleID.lowercased() @@ -115,21 +129,30 @@ public extension ALTAppleAPI context.dsid = dsid - let authType = statusDictionary["au"] as? String + // Apple may now respond with HTTP status 409 and no "au" field, + // in which case it has already pushed a code to trusted devices. + // Based on SideSign: https://github.com/SideStore/SideSign/pull/5 + let authType = statusDictionary["au"] as? String ?? ((statusDictionary["hsc"] as? Int) == 409 ? "pushedCode" : nil) switch authType { - case "trustedDeviceSecondaryAuth": + case "trustedDeviceSecondaryAuth", "pushedCode": guard let verificationHandler = verificationHandler else { throw ALTAppleAPIError(.requiresTwoFactorAuthentication) } - - self.requestTrustedDeviceTwoFactorCode(dsid: dsid, idmsToken: idmsToken, anisetteData: anisetteData, verificationHandler: verificationHandler) { (result) in + + let isCodeAlreadySent = (authType == "pushedCode") + + // Don't prompt for a pushed code again if Apple still asks for one after we've entered it. + guard !isCodeAlreadySent || !hasEnteredPushedCode else { throw ALTAppleAPIError(.requiresTwoFactorAuthentication) } + + self.requestTrustedDeviceTwoFactorCode(dsid: dsid, idmsToken: idmsToken, anisetteData: anisetteData, isCodeAlreadySent: isCodeAlreadySent, verificationHandler: verificationHandler) { (result) in switch result { case .failure(let error): completionHandler(nil, nil, error) case .success: - self.authenticate(appleID: unsanitizedAppleID, password: password, anisetteData: anisetteData, verificationHandler: verificationHandler, completionHandler: completionHandler) + self.authenticate(appleID: unsanitizedAppleID, password: password, anisetteData: anisetteData, hasEnteredPushedCode: hasEnteredPushedCode || isCodeAlreadySent, + verificationHandler: verificationHandler, completionHandler: completionHandler) } } - + case "secondaryAuth": guard let verificationHandler = verificationHandler else { throw ALTAppleAPIError(.requiresTwoFactorAuthentication) } @@ -138,7 +161,8 @@ public extension ALTAppleAPI { case .failure(let error): completionHandler(nil, nil, error) case .success: - self.authenticate(appleID: unsanitizedAppleID, password: password, anisetteData: anisetteData, verificationHandler: verificationHandler, completionHandler: completionHandler) + self.authenticate(appleID: unsanitizedAppleID, password: password, anisetteData: anisetteData, hasEnteredPushedCode: hasEnteredPushedCode, + verificationHandler: verificationHandler, completionHandler: completionHandler) } } @@ -232,6 +256,7 @@ private extension ALTAppleAPI func requestTrustedDeviceTwoFactorCode(dsid: String, idmsToken: String, anisetteData: ALTAnisetteData, + isCodeAlreadySent: Bool, verificationHandler: @escaping (@escaping (String?) -> Void) -> Void, completionHandler: @escaping (Result) -> Void) { @@ -240,7 +265,7 @@ private extension ALTAppleAPI let request = self.makeTwoFactorCodeRequest(url: requestURL, dsid: dsid, idmsToken: idmsToken, anisetteData: anisetteData) - let requestCodeTask = self.session.dataTask(with: request) { (data, response, error) in + let requestCodeHandler = { (error: Error?) in do { guard error == nil else { throw error! } @@ -317,7 +342,19 @@ private extension ALTAppleAPI } } - requestCodeTask.resume() + if isCodeAlreadySent + { + // Apple already sent the code, so prompt for it without requesting another. + requestCodeHandler(nil) + } + else + { + let requestCodeTask = self.session.dataTask(with: request) { (data, response, error) in + requestCodeHandler(error) + } + + requestCodeTask.resume() + } } func requestSMSTwoFactorCode(dsid: String,