QR Code Login Flow (#2767)

Co-authored-by: Hugh Nimmo-Smith <hughns@users.noreply.github.com>
Co-authored-by: Hugh Nimmo-Smith <hughns@element.io>
This commit is contained in:
Mauro
2024-05-29 17:07:17 +02:00
committed by GitHub
parent 66a8f6f3d1
commit 4e23f7be22
72 changed files with 1241 additions and 226 deletions
@@ -32,22 +32,13 @@ class AuthenticationServiceProxy: AuthenticationServiceProxyProtocol {
homeserverSubject = .init(LoginHomeserver(address: appSettings.defaultHomeserverAddress,
loginMode: .unknown))
let oidcConfiguration = OidcConfiguration(clientName: InfoPlistReader.main.bundleDisplayName,
redirectUri: appSettings.oidcRedirectURL.absoluteString,
clientUri: appSettings.websiteURL.absoluteString,
logoUri: appSettings.logoURL.absoluteString,
tosUri: appSettings.acceptableUseURL.absoluteString,
policyUri: appSettings.privacyURL.absoluteString,
contacts: [appSettings.supportEmailAddress],
staticRegistrations: appSettings.oidcStaticRegistrations.mapKeys { $0.absoluteString })
authenticationService = AuthenticationService(basePath: userSessionStore.baseDirectory.path,
passphrase: passphrase,
userAgent: UserAgentBuilder.makeASCIIUserAgent(),
additionalRootCertificates: [],
proxy: appSettings.websiteURL.globalProxy,
oidcConfiguration: oidcConfiguration,
oidcConfiguration: appSettings.oidcConfiguration.rustValue,
customSlidingSyncProxy: appSettings.slidingSyncProxyURL?.absoluteString,
sessionDelegate: userSessionStore.clientSessionDelegate,
crossProcessRefreshLockId: InfoPlistReader.main.bundleIdentifier)
@@ -42,7 +42,7 @@ enum AuthenticationServiceError: Error {
protocol AuthenticationServiceProxyProtocol {
/// The currently configured homeserver.
var homeserver: CurrentValuePublisher<LoginHomeserver, Never> { get }
/// Sets up the service for login on the specified homeserver address.
func configure(for homeserverAddress: String) async -> Result<Void, AuthenticationServiceError>
/// Performs login using OIDC for the current homeserver.
@@ -0,0 +1,45 @@
//
// Copyright 2024 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
struct OIDCConfigurationProxy {
let clientName: String
let redirectURI: URL
let clientURI: URL
let logoURI: URL
let tosURI: URL
let policyURI: URL
let contacts: [String]
let staticRegistrations: [String: String]
}
#if canImport(MatrixRustSDK)
import MatrixRustSDK
extension OIDCConfigurationProxy {
var rustValue: OidcConfiguration {
OidcConfiguration(clientName: clientName,
redirectUri: redirectURI.absoluteString,
clientUri: clientURI.absoluteString,
logoUri: logoURI.absoluteString,
tosUri: tosURI.absoluteString,
policyUri: policyURI.absoluteString,
contacts: contacts,
staticRegistrations: staticRegistrations)
}
}
#endif
@@ -1,37 +0,0 @@
//
// Copyright 2024 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import AVFoundation
final class QRCodeLoginService: QRCodeLoginServiceProtocol {
func requestAuthorizationIfNeeded() async -> Bool {
let status = AVCaptureDevice.authorizationStatus(for: .video)
// Determine if the user previously authorized camera access.
if status == .authorized {
return true
}
var isAuthorized = false
// If the system hasn't determined the user's authorization status,
// explicitly prompt them for approval.
if status == .notDetermined {
isAuthorized = await AVCaptureDevice.requestAccess(for: .video)
}
return isAuthorized
}
}
@@ -0,0 +1,118 @@
//
// Copyright 2024 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Combine
import Foundation
import MatrixRustSDK
final class QRCodeLoginService: QRCodeLoginServiceProtocol {
private let oidcConfiguration: OidcConfiguration
private let passphrase: String
private let userSessionStore: UserSessionStoreProtocol
private let qrLoginProgressSubject = PassthroughSubject<QrLoginProgress, Never>()
var qrLoginProgressPublisher: AnyPublisher<QrLoginProgress, Never> {
qrLoginProgressSubject.eraseToAnyPublisher()
}
init(oidcConfiguration: OidcConfiguration,
encryptionKeyProvider: EncryptionKeyProviderProtocol,
userSessionStore: UserSessionStoreProtocol) {
self.oidcConfiguration = oidcConfiguration
self.userSessionStore = userSessionStore
passphrase = encryptionKeyProvider.generateKey().base64EncodedString()
}
func loginWithQRCode(data: Data) async -> Result<UserSessionProtocol, QRCodeLoginServiceError> {
let qrData: QrCodeData
do {
qrData = try QrCodeData.fromBytes(bytes: data)
} catch {
MXLog.error("QRCode decode error: \(error)")
return .failure(.invalidQRCode)
}
let listener = QrLoginProgressListenerProxy { [weak self] progress in
self?.qrLoginProgressSubject.send(progress)
}
do {
let client = try await ClientBuilder()
.basePath(path: userSessionStore.baseDirectory.path(percentEncoded: false))
.passphrase(passphrase: passphrase)
.userAgent(userAgent: UserAgentBuilder.makeASCIIUserAgent())
.enableCrossProcessRefreshLock(processId: InfoPlistReader.main.bundleIdentifier,
sessionDelegate: userSessionStore.clientSessionDelegate)
.serverVersions(versions: ["v1.0", "v1.1", "v1.2", "v1.3", "v1.4", "v1.5"]) // FIXME: Quick and dirty fix for stopping version requests on startup https://github.com/matrix-org/matrix-rust-sdk/pull/1376
.buildWithQrCode(qrCodeData: qrData, oidcConfiguration: oidcConfiguration, progressListener: listener)
return await login(client: client)
} catch let error as HumanQrLoginError {
MXLog.error("QRCode login error: \(error)")
return .failure(error.serviceError)
} catch {
MXLog.error("QRCode login unknown error: \(error)")
return .failure(.unknown)
}
}
private func login(client: Client) async -> Result<UserSessionProtocol, QRCodeLoginServiceError> {
switch await userSessionStore.userSession(for: client, passphrase: passphrase) {
case .success(let session):
return .success(session)
case .failure(let error):
MXLog.error("QRCode login failed error: \(error)")
return .failure(.failedLoggingIn)
}
}
}
final class QrLoginProgressListenerProxy: QrLoginProgressListener {
private let onUpdateClosure: (QrLoginProgress) -> Void
init(onUpdateClosure: @escaping (QrLoginProgress) -> Void) {
self.onUpdateClosure = onUpdateClosure
}
func onUpdate(state: QrLoginProgress) {
onUpdateClosure(state)
}
}
private extension HumanQrLoginError {
var serviceError: QRCodeLoginServiceError {
switch self {
case .Cancelled:
return .cancelled
case .ConnectionInsecure:
return .connectionInsecure
case .Declined:
return .declined
case .LinkingNotSupported:
return .linkingNotSupported
case .Expired:
return .expired
case .InvalidQrCode:
return .invalidQRCode
case .SlidingSyncNotAvailable:
return .deviceNotSupported
case .OtherDeviceNotSignedIn:
return .deviceNotSignedIn
case .Unknown, .OidcMetadataInvalid:
return .unknown
}
}
}
@@ -14,7 +14,27 @@
// limitations under the License.
//
import Combine
import Foundation
import MatrixRustSDK
enum QRCodeLoginServiceError: Error {
case failedLoggingIn
case invalidQRCode
case cancelled
case connectionInsecure
case declined
case linkingNotSupported
case expired
case deviceNotSupported
case deviceNotSignedIn
case unknown
}
// sourcery: AutoMockable
protocol QRCodeLoginServiceProtocol {
func requestAuthorizationIfNeeded() async -> Bool
var qrLoginProgressPublisher: AnyPublisher<QrLoginProgress, Never> { get }
func loginWithQRCode(data: Data) async -> Result<UserSessionProtocol, QRCodeLoginServiceError>
}