Add timeouts after verifying the device owner when linking a new device. (#5970)

* Support for waitingForAuth continuation sender during Link new device

* Stop automatically regenerating new QR codes when they expire.

This reverts part of commit e235e3ba81.

* Add a timeout after the Face/TouchID verification when linking a desktop computer.

---------

Co-authored-by: Hugh Nimmo-Smith <hughns@element.io>
This commit is contained in:
Doug
2026-08-06 14:56:34 +01:00
committed by GitHub
parent a0521d1fdf
commit a82726a991
11 changed files with 125 additions and 112 deletions
@@ -377,7 +377,7 @@ private extension HumanQrLoginError {
.qrCodeError(.deviceNotSignedIn)
case .UnsupportedQrCodeType:
.qrCodeError(.invalidQRCode)
case .Unknown, .OAuthMetadataInvalid, .CheckCodeAlreadySent, .CheckCodeCannotBeSent, .ContinuationCannotBeSent, .ContinuationAlreadySent:
case .Unknown, .OAuthMetadataInvalid, .CheckCodeAlreadySent, .CheckCodeCannotBeSent, .ContinuationAlreadySent, .ContinuationCannotBeSent:
.qrCodeError(.unknown)
}
}
@@ -29,7 +29,7 @@ class LinkNewDeviceService: LinkNewDeviceServiceProtocol {
case starting
case qrReady(UIImage)
case qrScanned(CheckCodeSenderProxy)
case waitingForAuthorisation(verificationURL: URL)
case waitingForAuthorisation(verificationURL: URL, continuationSender: ContinuationMessageSenderProxy)
case syncingSecrets
case done
}
@@ -38,7 +38,7 @@ class LinkNewDeviceService: LinkNewDeviceServiceProtocol {
enum LinkDesktopProgress: Equatable {
case starting
case establishingSecureChannel(checkCodeString: String)
case waitingForAuthorisation(verificationURL: URL)
case waitingForAuthorisation(verificationURL: URL, continuationSender: ContinuationMessageSenderProxy)
case syncingSecrets
case done
}
@@ -163,10 +163,10 @@ extension LinkNewDeviceService.LinkMobileProgress: CustomStringConvertible {
throw Error.invalidQRCodeData
}
case .qrScanned(let checkCodeSender): .qrScanned(.init(underlyingSender: checkCodeSender))
case .waitingForAuth(let verificationURI, _):
case .waitingForAuth(let verificationURI, let continuationSender):
// verificationURI is a String; ASWebAuthenticationSession requires a URL.
if let url = URL(string: verificationURI) {
.waitingForAuthorisation(verificationURL: url)
.waitingForAuthorisation(verificationURL: url, continuationSender: .init(underlyingSender: continuationSender))
} else {
throw Error.invalidVerificationURI(verificationURI)
}
@@ -194,10 +194,10 @@ extension LinkNewDeviceService.LinkDesktopProgress: CustomStringConvertible {
self = switch rustProgress {
case .starting: .starting
case .establishingSecureChannel(_, let checkCodeString): .establishingSecureChannel(checkCodeString: checkCodeString)
case .waitingForAuth(let verificationURI, _):
case .waitingForAuth(let verificationURI, let continuationSender):
// verificationURI is a String; ASWebAuthenticationSession requires a URL.
if let url = URL(string: verificationURI) {
.waitingForAuthorisation(verificationURL: url)
.waitingForAuthorisation(verificationURL: url, continuationSender: .init(underlyingSender: continuationSender))
} else {
throw Error.invalidVerificationURI(verificationURI)
}
@@ -240,8 +240,9 @@ private extension QRCodeLoginError {
}
}
nonisolated class CheckCodeSenderProxy: Equatable {
final nonisolated class CheckCodeSenderProxy: Equatable, Sendable {
static func == (lhs: CheckCodeSenderProxy, rhs: CheckCodeSenderProxy) -> Bool {
// Not ideal but good enough as the equality check is purely for removing duplicates from the progress stream.
lhs.underlyingSender === rhs.underlyingSender
}
@@ -256,6 +257,35 @@ nonisolated class CheckCodeSenderProxy: Equatable {
}
}
final nonisolated class ContinuationMessageSenderProxy: Equatable, Sendable {
static func == (lhs: ContinuationMessageSenderProxy, rhs: ContinuationMessageSenderProxy) -> Bool {
// Not ideal but good enough as the equality check is purely for removing duplicates from the progress stream.
lhs.underlyingSender === rhs.underlyingSender
}
let underlyingSender: ContinuationMessageSenderProtocol
init(underlyingSender: ContinuationMessageSenderProtocol) {
self.underlyingSender = underlyingSender
}
func confirm() async {
do {
try await underlyingSender.confirm()
} catch {
MXLog.error("Failed to confirm the continuation of the login grant: \(error)")
}
}
func cancel() async {
do {
try await underlyingSender.cancel()
} catch {
MXLog.error("Failed to cancel the login grant: \(error)")
}
}
}
extension UIImage {
convenience init?(qrCodeData: Data) {
let qrContext = CIContext()