Simplify authentication logic

matrix.org now supports MAS so we don't need the web registration helper anymore.
This commit is contained in:
Doug
2025-04-10 15:09:40 +01:00
committed by Doug
parent b2c647b1af
commit 1557054753
32 changed files with 86 additions and 709 deletions
@@ -13,21 +13,13 @@ struct LoginHomeserver: Equatable {
let address: String
/// The types login supported by the homeserver.
var loginMode: LoginMode
/// A temporary helper URL that can be used for registration.
var registrationHelperURL: URL?
/// Creates a new homeserver value.
init(address: String, loginMode: LoginMode, registrationHelperURL: URL? = nil) {
init(address: String, loginMode: LoginMode) {
let address = Self.sanitized(address).components(separatedBy: "://").last ?? address
self.address = address
self.loginMode = loginMode
self.registrationHelperURL = registrationHelperURL
}
/// Whether or not the app is able to register on this homeserver.
var supportsRegistration: Bool {
loginMode == .oidc || (address == "matrix.org" && registrationHelperURL != nil)
}
/// Sanitizes a user entered homeserver address with the following rules
@@ -53,7 +45,7 @@ struct LoginHomeserver: Equatable {
extension LoginHomeserver {
/// A mock homeserver that is configured just like matrix.org.
static var mockMatrixDotOrg: LoginHomeserver {
LoginHomeserver(address: "matrix.org", loginMode: .password, registrationHelperURL: "https://develop.element.io/#/mobile_register")
LoginHomeserver(address: "matrix.org", loginMode: .oidc(supportsCreatePrompt: true))
}
/// A mock homeserver that supports login and registration via a password but has no SSO providers.
@@ -63,7 +55,7 @@ extension LoginHomeserver {
/// A mock homeserver that supports only supports authentication via a single SSO provider.
static var mockOIDC: LoginHomeserver {
LoginHomeserver(address: "company.com", loginMode: .oidc)
LoginHomeserver(address: "company.com", loginMode: .oidc(supportsCreatePrompt: false))
}
/// A mock homeserver that only with no supported login flows.
@@ -12,7 +12,7 @@ enum LoginMode: Equatable {
/// The login mode hasn't been determined yet.
case unknown
/// The homeserver supports login via OpenID Connect.
case oidc
case oidc(supportsCreatePrompt: Bool)
/// The homeserver supports login with a password.
case password
/// The homeserver only allows login with unsupported mechanisms. Use fallback instead.
@@ -69,7 +69,7 @@ class LoginScreenViewModel: LoginScreenViewModelType, LoginScreenViewModelProtoc
Task {
switch await authenticationService.configure(for: homeserverDomain, flow: .login) {
case .success:
if authenticationService.homeserver.value.loginMode == .oidc {
if authenticationService.homeserver.value.loginMode.supportsOIDCFlow {
actionsSubject.send(.configuredForOIDC)
}
stopLoading()
@@ -1,139 +0,0 @@
//
// Copyright 2022-2024 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
// Please see LICENSE files in the repository root for full details.
//
import Compound
import SwiftUI
import WebKit
struct WebRegistrationScreen: View {
@ObservedObject var context: WebRegistrationScreenViewModel.Context
var body: some View {
NavigationStack {
WebRegistrationWebView(url: context.viewState.url, viewModelContext: context)
.navigationTitle(L10n.screenCreateAccountTitle)
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button(L10n.actionCancel) {
context.send(viewAction: .cancel)
}
}
}
}
}
}
struct WebRegistrationWebView: UIViewRepresentable {
let url: URL
let viewModelContext: WebRegistrationScreenViewModel.Context
func makeUIView(context: Context) -> WKWebView {
context.coordinator.webView
}
func updateUIView(_ webView: WKWebView, context: Context) { }
func makeCoordinator() -> Coordinator {
Coordinator(url: url, viewModelContext: viewModelContext)
}
class Coordinator: NSObject, WKUIDelegate {
private let url: URL
private let viewModelContext: WebRegistrationScreenViewModel.Context
private(set) var webView: WKWebView!
init(url: URL, viewModelContext: WebRegistrationScreenViewModel.Context) {
self.url = url
self.viewModelContext = viewModelContext
super.init()
let eventHandlerName = "elementx"
let userContentController = WKUserContentController()
userContentController.add(WKScriptMessageHandlerWrapper(self), name: eventHandlerName)
let eventHandlerScript = """
window.addEventListener(
"mobileregistrationresponse",
(event) => {
window.webkit.messageHandlers.\(eventHandlerName).postMessage(JSON.stringify(event.detail));
},
false,
);
"""
let userScript = WKUserScript(source: eventHandlerScript, injectionTime: .atDocumentEnd, forMainFrameOnly: false)
userContentController.addUserScript(userScript)
let configuration = WKWebViewConfiguration()
configuration.userContentController = userContentController
configuration.preferences.javaScriptCanOpenWindowsAutomatically = true
webView = WKWebView(frame: .zero, configuration: configuration)
webView.uiDelegate = self
webView.load(URLRequest(url: url))
}
func userContentController(_ userContentController: WKUserContentController,
didReceive message: WKScriptMessage) {
guard let jsonString = message.body as? String, let jsonData = jsonString.data(using: .utf8) else {
MXLog.error("Unexpected response.")
return
}
guard let credentials = try? JSONDecoder().decode(WebRegistrationCredentials.self, from: jsonData) else {
MXLog.error("Invalid response.")
return
}
MXLog.info("Received login credentials.")
viewModelContext.send(viewAction: .signedIn(credentials))
}
// MARK: WKUIDelegate
func webView(_ webView: WKWebView,
createWebViewWith configuration: WKWebViewConfiguration,
for navigationAction: WKNavigationAction,
windowFeatures: WKWindowFeatures) -> WKWebView? {
if let url = navigationAction.request.url, UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url)
}
return nil
}
}
/// Avoids retain loops between the configuration and webView coordinator
private class WKScriptMessageHandlerWrapper: NSObject, WKScriptMessageHandler {
private weak var coordinator: Coordinator?
init(_ coordinator: Coordinator) {
self.coordinator = coordinator
}
// MARK: WKScriptMessageHandler
func userContentController(_ userContentController: WKUserContentController,
didReceive message: WKScriptMessage) {
coordinator?.userContentController(userContentController, didReceive: message)
}
}
}
// MARK: - Previews
struct WebRegistrationScreen_Previews: PreviewProvider {
static let viewModel = WebRegistrationScreenViewModel(registrationHelperURL: "https://develop.element.io/#/mobile_register")
static var previews: some View {
NavigationStack {
WebRegistrationScreen(context: viewModel.context)
}
}
}
@@ -1,83 +0,0 @@
//
// Copyright 2022-2024 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
// Please see LICENSE files in the repository root for full details.
//
import Combine
import MatrixRustSDK
import SwiftUI
struct WebRegistrationScreenCoordinatorParameters {
/// The service used to authenticate the user.
let authenticationService: AuthenticationServiceProtocol
let userIndicatorController: UserIndicatorControllerProtocol
}
enum WebRegistrationScreenCoordinatorAction: CustomStringConvertible {
case cancel
case signedIn(UserSessionProtocol)
var description: String {
switch self {
case .cancel: "cancel"
case .signedIn: "signedIn"
}
}
}
// Note: This code was based on the LoginScreen, we should move the authentication service logic into the view model.
final class WebRegistrationScreenCoordinator: CoordinatorProtocol {
private let parameters: WebRegistrationScreenCoordinatorParameters
private let viewModel: WebRegistrationScreenViewModelProtocol
private var cancellables = Set<AnyCancellable>()
private let actionsSubject: PassthroughSubject<WebRegistrationScreenCoordinatorAction, Never> = .init()
var actionsPublisher: AnyPublisher<WebRegistrationScreenCoordinatorAction, Never> {
actionsSubject.eraseToAnyPublisher()
}
init(parameters: WebRegistrationScreenCoordinatorParameters) {
self.parameters = parameters
guard let registrationHelperURL = parameters.authenticationService.homeserver.value.registrationHelperURL else {
MXLog.error("Attempted registration without a helper URL.")
fatalError("A helper URL is required.")
}
viewModel = WebRegistrationScreenViewModel(registrationHelperURL: registrationHelperURL)
}
func start() {
viewModel.actionsPublisher.sink { [weak self] action in
MXLog.info("Coordinator: received view model action: \(action)")
guard let self else { return }
switch action {
case .cancel:
actionsSubject.send(.cancel)
case .signedIn(let credentials):
Task { await self.completeRegistration(using: credentials) }
}
}
.store(in: &cancellables)
}
func toPresentable() -> AnyView {
AnyView(WebRegistrationScreen(context: viewModel.context))
}
// MARK: - Private
private func completeRegistration(using credentials: WebRegistrationCredentials) async {
switch await parameters.authenticationService.completeWebRegistration(using: credentials) {
case .success(let userSession):
actionsSubject.send(.signedIn(userSession))
case .failure(let error):
MXLog.error("Failed registration: \(error)")
parameters.userIndicatorController.alertInfo = .init(id: UUID(), title: L10n.errorUnknown, message: String(describing: error))
}
}
}
@@ -1,51 +0,0 @@
//
// Copyright 2022-2024 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
// Please see LICENSE files in the repository root for full details.
//
import Foundation
enum WebRegistrationScreenViewModelAction: CustomStringConvertible {
case cancel
case signedIn(WebRegistrationCredentials)
var description: String {
switch self {
case .cancel: "cancel"
case .signedIn: "signedIn"
}
}
}
struct WebRegistrationScreenViewState: BindableState {
var url: URL
var bindings = WebRegistrationScreenViewStateBindings()
}
struct WebRegistrationScreenViewStateBindings { }
enum WebRegistrationScreenViewAction: CustomStringConvertible {
case cancel
case signedIn(WebRegistrationCredentials)
var description: String {
switch self {
case .cancel: "cancel"
case .signedIn: "signedIn"
}
}
}
struct WebRegistrationCredentials: Decodable {
let userID: String
let accessToken: String
let deviceID: String
enum CodingKeys: String, CodingKey {
case userID = "user_id"
case accessToken = "access_token"
case deviceID = "device_id"
}
}
@@ -1,34 +0,0 @@
//
// Copyright 2022-2024 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
// Please see LICENSE files in the repository root for full details.
//
import Combine
import MatrixRustSDK
import SwiftUI
typealias WebRegistrationScreenViewModelType = StateStoreViewModel<WebRegistrationScreenViewState, WebRegistrationScreenViewAction>
class WebRegistrationScreenViewModel: WebRegistrationScreenViewModelType, WebRegistrationScreenViewModelProtocol {
private let actionsSubject: PassthroughSubject<WebRegistrationScreenViewModelAction, Never> = .init()
var actionsPublisher: AnyPublisher<WebRegistrationScreenViewModelAction, Never> {
actionsSubject.eraseToAnyPublisher()
}
init(registrationHelperURL: URL) {
super.init(initialViewState: WebRegistrationScreenViewState(url: registrationHelperURL))
}
override func process(viewAction: WebRegistrationScreenViewAction) {
MXLog.info("View model: received view action: \(viewAction)")
switch viewAction {
case .cancel:
actionsSubject.send(.cancel)
case .signedIn(let credentials):
actionsSubject.send(.signedIn(credentials))
}
}
}
@@ -1,14 +0,0 @@
//
// Copyright 2022-2024 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
// Please see LICENSE files in the repository root for full details.
//
import Combine
@MainActor
protocol WebRegistrationScreenViewModelProtocol {
var actionsPublisher: AnyPublisher<WebRegistrationScreenViewModelAction, Never> { get }
var context: WebRegistrationScreenViewModelType.Context { get }
}