Implement login with scanned QR code flows

This commit is contained in:
Stefan Ceriu
2022-10-11 15:56:37 +03:00
committed by Stefan Ceriu
parent bbd3470659
commit fa3866ea76
19 changed files with 442 additions and 139 deletions
@@ -259,7 +259,7 @@ class AuthenticationService: NSObject {
}
let loginFlow = try await getLoginFlowResult(client: client)
let supportsQRLogin = try await QRLoginService(client: client,
mode: .notAuthenticated).isServiceAvailable()
@@ -30,6 +30,8 @@ enum AuthenticationLoginCoordinatorResult: CustomStringConvertible {
case continueWithSSO(SSOIdentityProvider)
/// Login was successful with the associated session created.
case success(session: MXSession, password: String)
/// Login was successful with the associated session created.
case loggedInWithQRCode(session: MXSession)
/// Login requested a fallback
case fallback
@@ -40,6 +42,8 @@ enum AuthenticationLoginCoordinatorResult: CustomStringConvertible {
return "continueWithSSO: \(provider)"
case .success:
return "success"
case .loggedInWithQRCode:
return "loggedInWithQRCode"
case .fallback:
return "fallback"
}
@@ -294,8 +298,13 @@ final class AuthenticationLoginCoordinator: Coordinator, Presentable {
let parameters = AuthenticationQRLoginStartCoordinatorParameters(navigationRouter: navigationRouter,
qrLoginService: service)
let coordinator = AuthenticationQRLoginStartCoordinator(parameters: parameters)
coordinator.callback = { [weak self, weak coordinator] _ in
coordinator.callback = { [weak self, weak coordinator] callback in
guard let self = self, let coordinator = coordinator else { return }
switch callback {
case .done(let session):
self.callback?(.loggedInWithQRCode(session: session))
}
self.remove(childCoordinator: coordinator)
}
@@ -17,23 +17,78 @@
import Foundation
struct QRLoginCode: Codable {
var user: String?
var initiator: QRLoginDataInitiatorDevice?
var rendezvous: QRLoginRendezvous?
let rendezvous: RendezvousDetails
let intent: String
}
enum QRLoginDataInitiatorDevice: String, Codable {
case new = "new_device"
case existing = "existing_device"
}
struct QRLoginRendezvous: Codable {
var transport: QRLoginRendezvousTransportDetails
var algorithm: String?
struct RendezvousDetails: Codable {
let algorithm: String
var transport: RendezvousTransportDetails?
var key: String?
}
struct QRLoginRendezvousTransportDetails: Codable {
var type: String
var uri: String?
struct RendezvousTransportDetails: Codable {
let type: String
let uri: String
}
struct RendezvousMessage: Codable {
let iv: String
let ciphertext: String
}
struct QRLoginRendezvousPayload: Codable {
let type: `Type`
var intent: Intent?
var outcome: Outcome?
var protocols: [`Protocol`]?
var `protocol`: `Protocol`?
var homeserver: String?
var user: String?
var loginToken: String?
var deviceId: String?
var deviceKey: String?
var verifyingDeviceId: String?
var verifyingDeviceKey: String?
var masterKey: String?
enum CodingKeys: String, CodingKey {
case type
case intent
case outcome
case homeserver
case user
case protocols
case `protocol`
case loginToken = "login_token"
case deviceId = "device_id"
case deviceKey = "device_key"
case verifyingDeviceId = "verifying_device_id"
case verifyingDeviceKey = "verifying_device_key"
case masterKey = "master_key"
}
enum `Type`: String, Codable {
case loginStart = "m.login.start"
case loginProgress = "m.login.progress"
case loginFinish = "m.login.finish"
}
enum Intent: String, Codable {
case loginStart = "login.start"
}
enum Outcome: String, Codable {
case success = "success"
case declined = "declined"
}
enum `Protocol`: String, Codable {
case loginToken = "login_token"
}
}
@@ -25,15 +25,19 @@ import ZXingObjC
class QRLoginService: NSObject, QRLoginServiceProtocol {
private let client: AuthenticationRestClient
private let sessionCreator: SessionCreatorProtocol
private var isCameraReady = false
private lazy var zxCapture = ZXCapture()
private let cameraAccessManager = CameraAccessManager()
private var rendezvousService: RendezvousService?
init(client: AuthenticationRestClient,
mode: QRLoginServiceMode,
state: QRLoginServiceState = .initial) {
self.client = client
self.sessionCreator = SessionCreator()
self.mode = mode
self.state = state
super.init()
@@ -72,16 +76,9 @@ class QRLoginService: NSObject, QRLoginServiceProtocol {
}
func generateQRCode() async throws -> QRLoginCode {
let transport = QRLoginRendezvousTransportDetails(type: "http.v1",
uri: "")
let rendezvous = QRLoginRendezvous(transport: transport,
algorithm: "m.rendezvous.v1.curve25519-aes-sha256",
key: "")
return QRLoginCode(user: client.credentials.userId,
initiator: .new,
rendezvous: rendezvous)
fatalError("Not implemented")
}
func scannerView() -> AnyView {
let frame = UIScreen.main.bounds
let view = UIView(frame: frame)
@@ -109,6 +106,8 @@ class QRLoginService: NSObject, QRLoginServiceProtocol {
}
func stopScanning(destroy: Bool) {
zxCapture.delegate = nil
guard zxCapture.running else {
return
}
@@ -120,20 +119,21 @@ class QRLoginService: NSObject, QRLoginServiceProtocol {
}
}
@MainActor
func processScannedQR(_ data: Data) {
state = .connectingToDevice
do {
let code = try JSONDecoder().decode(QRLoginCode.self, from: data)
MXLog.debug("[QRLoginService] processScannedQR: \(code)")
// TODO: implement
} catch {
guard let code = try? JSONDecoder().decode(QRLoginCode.self, from: data) else {
state = .failed(error: .invalidQR)
return
}
Task {
await processQRLoginCode(code)
}
}
func confirmCode() {
switch state {
case .waitingForConfirmation(let code):
case .waitingForConfirmation:
// TODO: implement
break
default:
@@ -143,11 +143,19 @@ class QRLoginService: NSObject, QRLoginServiceProtocol {
func restart() {
state = .initial
Task {
await declineRendezvous()
}
}
func reset() {
stopScanning(destroy: false)
state = .initial
Task {
await declineRendezvous()
}
}
deinit {
@@ -155,6 +163,119 @@ class QRLoginService: NSObject, QRLoginServiceProtocol {
}
// MARK: Private
@MainActor
private func processQRLoginCode(_ code: QRLoginCode) async {
MXLog.debug("[QRLoginService] processQRLoginCode: \(code)")
state = .connectingToDevice
guard let uri = code.rendezvous.transport?.uri,
let rendezvousURL = URL(string: uri),
let key = code.rendezvous.key else {
MXLog.debug("[QRLoginService] QR code invalid")
state = .failed(error: .invalidQR)
return
}
let transport = RendezvousTransport(baseURL: BuildSettings.rendezvousServerBaseURL,
rendezvousURL: rendezvousURL)
let rendezvousService = RendezvousService(transport: transport)
self.rendezvousService = rendezvousService
MXLog.debug("[QRLoginService] Joining the rendezvous at \(rendezvousURL)")
guard case .success(let validationCode) = await rendezvousService.joinRendezvous(withInterlocutorPublicKey: key) else {
await teardownRendezvous(state: .failed(error: .rendezvousFailed))
return
}
state = .waitingForConfirmation(validationCode)
MXLog.debug("[QRLoginService] Requesting login")
guard let requestData = try? JSONEncoder().encode(QRLoginRendezvousPayload(type: .loginStart, intent: .loginStart)),
case .success = await rendezvousService.send(data: requestData) else {
await teardownRendezvous(state: .failed(error: .rendezvousFailed))
return
}
MXLog.debug("[QRLoginService] Waiting for available protocols")
guard case let .success(data) = await rendezvousService.receive(),
let responsePayload = try? JSONDecoder().decode(QRLoginRendezvousPayload.self, from: data) else {
await teardownRendezvous(state: .failed(error: .rendezvousFailed))
return
}
MXLog.debug("[QRLoginService] Received available protocols \(responsePayload)")
guard let protocols = responsePayload.protocols,
protocols.contains(.loginToken) else {
await teardownRendezvous(state: .failed(error: .rendezvousFailed))
return
}
MXLog.debug("[QRLoginService] Request login with `login_token`")
guard let requestData = try? JSONEncoder().encode(QRLoginRendezvousPayload(type: .loginProgress, protocol: .loginToken)),
case .success = await rendezvousService.send(data: requestData) else {
await teardownRendezvous(state: .failed(error: .rendezvousFailed))
return
}
state = .waitingForRemoteSignIn
MXLog.debug("[QRLoginService] Waiting for the login token")
guard case let .success(data) = await rendezvousService.receive(),
let responsePayload = try? JSONDecoder().decode(QRLoginRendezvousPayload.self, from: data),
let login_token = responsePayload.loginToken else {
await teardownRendezvous(state: .failed(error: .rendezvousFailed))
return
}
MXLog.debug("[QRLoginService] Received login token \(responsePayload)")
MXLog.debug("[QRLoginService] Logging in with the login token")
guard let credentials = try? await client.login(parameters: LoginTokenParameters(token: login_token)) else {
await teardownRendezvous(state: .failed(error: .rendezvousFailed))
return
}
MXLog.debug("[QRLoginService] Got acess token")
let session = sessionCreator.createSession(credentials: credentials, client: client, removeOtherAccounts: false)
MXLog.debug("[QRLoginService] Created session")
MXLog.debug("[QRLoginService] No E2EE support. Inform the interlocutor of finishing")
guard let requestData = try? JSONEncoder().encode(QRLoginRendezvousPayload(type: .loginFinish, outcome: .success)),
case .success = await rendezvousService.send(data: requestData) else {
await teardownRendezvous(state: .failed(error: .rendezvousFailed))
return
}
state = .completed(session: session)
}
private func declineRendezvous() async {
guard let requestData = try? JSONEncoder().encode(QRLoginRendezvousPayload(type: .loginFinish, outcome: .declined)) else {
return
}
_ = await rendezvousService?.send(data: requestData)
await teardownRendezvous()
}
private func teardownRendezvous(state: QRLoginServiceState? = nil) async {
// Stop listening for changes, try deleting the resource
_ = await rendezvousService?.tearDown()
// Try setting the new state, if necessary
if let state = state {
switch self.state {
case .completed:
return
case .initial:
return
default:
self.state = state
}
}
}
}
// MARK: - ZXCaptureDelegate
@@ -52,14 +52,12 @@ class MockQRLoginService: QRLoginServiceProtocol {
}
func generateQRCode() async throws -> QRLoginCode {
let transport = QRLoginRendezvousTransportDetails(type: "http.v1",
uri: "https://matrix.org")
let rendezvous = QRLoginRendezvous(transport: transport,
algorithm: "m.rendezvous.v1.curve25519-aes-sha256",
key: "")
return QRLoginCode(user: "@mock:matrix.org",
initiator: .new,
rendezvous: rendezvous)
let details = RendezvousDetails(algorithm: "m.rendezvous.v1.curve25519-aes-sha256",
transport: .init(type: "http.v1",
uri: "https://matrix.org"),
key: "some.public.key")
return QRLoginCode(rendezvous: details,
intent: "login.start")
}
func scannerView() -> AnyView {
@@ -33,6 +33,7 @@ enum QRLoginServiceError: Error, Equatable {
case invalidQR
case requestDenied
case requestTimedOut
case rendezvousFailed
}
// MARK: - QRLoginServiceState
@@ -44,7 +45,8 @@ enum QRLoginServiceState: Equatable {
case waitingForConfirmation(_ code: String)
case waitingForRemoteSignIn
case failed(error: QRLoginServiceError)
case completed
// This is really an MXSession but that would break RiotSwiftUI
case completed(session: Any)
static func == (lhs: QRLoginServiceState, rhs: QRLoginServiceState) -> Bool {
switch (lhs, rhs) {
@@ -94,11 +94,11 @@ struct AuthenticationQRLoginConfirmScreen: View {
.padding(.bottom, 12)
.accessibilityIdentifier("alertText")
Button(action: confirm) {
Text(VectorL10n.confirm)
}
.buttonStyle(PrimaryActionButtonStyle(font: theme.fonts.bodySB))
.accessibilityIdentifier("confirmButton")
// Button(action: confirm) {
// Text(VectorL10n.confirm)
// }
// .buttonStyle(PrimaryActionButtonStyle(font: theme.fonts.bodySB))
// .accessibilityIdentifier("confirmButton")
Button(action: cancel) {
Text(VectorL10n.cancel)
@@ -48,7 +48,7 @@ enum MockAuthenticationQRLoginLoadingScreenState: MockScreenState, CaseIterable
case .waitingForRemoteSignIn:
viewModel = .init(qrLoginService: MockQRLoginService(withState: .waitingForRemoteSignIn))
case .completed:
viewModel = .init(qrLoginService: MockQRLoginService(withState: .completed))
viewModel = .init(qrLoginService: MockQRLoginService(withState: .completed(session: "")))
}
// can simulate service and viewModel actions here if needs be.
@@ -16,6 +16,7 @@
import CommonKit
import SwiftUI
import MatrixSDK
struct AuthenticationQRLoginScanCoordinatorParameters {
let navigationRouter: NavigationRouterType
@@ -78,7 +79,10 @@ final class AuthenticationQRLoginScanCoordinator: Coordinator, Presentable {
self.showDisplayQRScreen()
case .qrScanned(let data):
self.qrLoginService.stopScanning(destroy: false)
self.qrLoginService.processScannedQR(data)
Task {
await self.qrLoginService.processScannedQR(data)
}
}
}
}
@@ -25,7 +25,7 @@ struct AuthenticationQRLoginStartCoordinatorParameters {
enum AuthenticationQRLoginStartCoordinatorResult {
/// Login with QR done
case done
case done(session: MXSession)
}
final class AuthenticationQRLoginStartCoordinator: Coordinator, Presentable {
@@ -108,18 +108,23 @@ final class AuthenticationQRLoginStartCoordinator: Coordinator, Presentable {
switch state {
case .initial:
removeAllChildren()
case .connectingToDevice, .waitingForRemoteSignIn, .completed:
case .connectingToDevice, .waitingForRemoteSignIn:
showLoadingScreenIfNeeded()
case .waitingForConfirmation:
showConfirmationScreenIfNeeded()
case .failed(let error):
switch error {
case .noCameraAccess, .noCameraAvailable:
// handled in scanning screen
break
break // handled in scanning screen
default:
showFailureScreenIfNeeded()
}
case .completed(let session):
guard let session = session as? MXSession else {
showFailureScreenIfNeeded()
return
}
callback?(.done(session: session))
default:
break
}
@@ -162,6 +167,8 @@ final class AuthenticationQRLoginStartCoordinator: Coordinator, Presentable {
/// Shows the display QR screen.
private func showDisplayQRScreen() {
MXLog.debug("[AuthenticationQRLoginStartCoordinator] showDisplayQRScreen")
removeAllChildren(animated: false)
let parameters = AuthenticationQRLoginDisplayCoordinatorParameters(navigationRouter: navigationRouter,
qrLoginService: qrLoginService)
@@ -182,6 +189,8 @@ final class AuthenticationQRLoginStartCoordinator: Coordinator, Presentable {
/// Shows the loading screen.
private func showLoadingScreenIfNeeded() {
MXLog.debug("[AuthenticationQRLoginStartCoordinator] showLoadingScreenIfNeeded")
removeAllChildren(animated: false)
if let lastCoordinator = childCoordinators.last,
lastCoordinator is AuthenticationQRLoginLoadingCoordinator {
@@ -208,6 +217,8 @@ final class AuthenticationQRLoginStartCoordinator: Coordinator, Presentable {
/// Shows the confirmation screen.
private func showConfirmationScreenIfNeeded() {
MXLog.debug("[AuthenticationQRLoginStartCoordinator] showConfirmationScreenIfNeeded")
removeAllChildren(animated: false)
if let lastCoordinator = childCoordinators.last,
lastCoordinator is AuthenticationQRLoginConfirmCoordinator {
@@ -234,6 +245,8 @@ final class AuthenticationQRLoginStartCoordinator: Coordinator, Presentable {
/// Shows the failure screen.
private func showFailureScreenIfNeeded() {
MXLog.debug("[AuthenticationQRLoginStartCoordinator] showFailureScreenIfNeeded")
removeAllChildren(animated: false)
if let lastCoordinator = childCoordinators.last,
lastCoordinator is AuthenticationQRLoginFailureCoordinator {
@@ -77,7 +77,7 @@ struct AuthenticationQRLoginStartScreen: View {
.accessibilityIdentifier("subtitleLabel")
}
}
/// The screen's footer.
var footerContent: some View {
VStack(spacing: 12) {
@@ -87,10 +87,10 @@ struct AuthenticationQRLoginStartScreen: View {
.buttonStyle(PrimaryActionButtonStyle(font: theme.fonts.bodySB))
.padding(.bottom, 8)
.accessibilityIdentifier("scanQRButton")
if context.viewState.canShowDisplayQRButton {
LabelledDivider(label: VectorL10n.authenticationQrLoginStartNeedAlternative)
Button(action: displayQR) {
Text(VectorL10n.authenticationQrLoginStartDisplayQr)
}
@@ -39,10 +39,10 @@ struct UserSessionsOverview: View {
}
.readableFrame()
if viewModel.viewState.linkDeviceButtonVisible {
linkDeviceView
.padding(.bottom, geometry.safeAreaInsets.bottom > 0 ? 20 : 36)
}
// if viewModel.viewState.linkDeviceButtonVisible {
// linkDeviceView
// .padding(.bottom, geometry.safeAreaInsets.bottom > 0 ? 20 : 36)
// }
}
}
.background(theme.colors.system.ignoresSafeArea())