Fixes #239 - Use new restoration token format, pass stored homeserver url to the client builder when restoring, dropping the need for a network request

- bumped the rust sdk to v1.0.17-alpha
- move all UserSession related files to Services/UserSession
This commit is contained in:
Stefan Ceriu
2022-11-04 13:24:53 +02:00
parent 2e86313965
commit 37cfb8f50e
18 changed files with 271 additions and 179 deletions
@@ -303,7 +303,7 @@ class AppCoordinator: Coordinator {
self.stateMachine.processEvent(.remoteSignOut(isSoft: isSoftLogout))
case .updateRestoreTokenNeeded:
if let userSession = self.userSession {
_ = self.userSessionStore.refreshRestoreToken(for: userSession)
_ = self.userSessionStore.refreshRestorationToken(for: userSession)
}
default:
break
@@ -95,7 +95,7 @@ class ClientProxy: ClientProxyProtocol {
.build()
self.slidingSync = try slidingSyncBuilder
.addView(view: slidingSyncView)
.addView(v: slidingSyncView)
.withCommonExtensions()
.build()
@@ -136,9 +136,9 @@ class ClientProxy: ClientProxyProtocol {
client.homeserver()
}
var restoreToken: String? {
var restorationToken: RestorationToken? {
do {
return try client.restoreToken()
return try RestorationToken(session: client.session())
} catch {
MXLog.error("Failed retrieving restore token with error: \(error)")
return nil
@@ -44,7 +44,7 @@ protocol ClientProxyProtocol {
var homeserver: String { get }
var restoreToken: String? { get }
var restorationToken: RestorationToken? { get }
var roomSummaryProvider: RoomSummaryProviderProtocol { get }
@@ -24,7 +24,7 @@ struct MockClientProxy: ClientProxyProtocol {
let isSoftLogout = false
let deviceId: String? = nil
let homeserver = ""
let restoreToken: String? = nil
let restorationToken: RestorationToken? = nil
var roomSummaryProvider: RoomSummaryProviderProtocol = MockRoomSummaryProvider()
@@ -0,0 +1,85 @@
//
// Copyright 2022 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
import KeychainAccess
class KeychainController: KeychainControllerProtocol {
private let keychain: Keychain
init(identifier: String) {
keychain = Keychain(service: identifier)
}
func setRestorationToken(_ restorationToken: RestorationToken, forUsername username: String) {
do {
let tokenData = try JSONEncoder().encode(restorationToken)
try keychain.set(tokenData, key: username)
} catch {
MXLog.error("Failed storing user restore token with error: \(error)")
}
}
func restorationTokenForUsername(_ username: String) -> RestorationToken? {
do {
guard let tokenData = try keychain.getData(username) else {
return nil
}
// Handle the previous restoration token format as we don't want users to have to log in again
// It will automatically be updated to the new version after login
if let legacyRestorationToken = try? JSONDecoder().decode(LegacyRestorationToken.self, from: tokenData) {
return .init(session: .init(accessToken: legacyRestorationToken.session.accessToken,
refreshToken: nil,
userId: legacyRestorationToken.session.userId,
deviceId: legacyRestorationToken.session.deviceId,
homeserverUrl: legacyRestorationToken.homeURL,
isSoftLogout: legacyRestorationToken.isSoftLogout ?? false))
}
return try JSONDecoder().decode(RestorationToken.self, from: tokenData)
} catch {
MXLog.error("Failed retrieving user restore token")
return nil
}
}
func restorationTokens() -> [KeychainCredentials] {
keychain.allKeys().compactMap { username in
guard let restorationToken = restorationTokenForUsername(username) else {
return nil
}
return KeychainCredentials(userID: username, restorationToken: restorationToken)
}
}
func removeRestorationTokenForUsername(_ username: String) {
do {
try keychain.remove(username)
} catch {
MXLog.error("Failed removing restore token with error: \(error)")
}
}
func removeAllRestorationTokens() {
do {
try keychain.removeAll()
} catch {
MXLog.error("Failed removing all tokens")
}
}
}
@@ -18,13 +18,13 @@ import Foundation
struct KeychainCredentials {
let userID: String
let restoreToken: String
let restorationToken: RestorationToken
}
protocol KeychainControllerProtocol {
func setRestoreToken(_ accessToken: String, forUsername username: String)
func restoreTokenForUsername(_ username: String) -> String?
func restoreTokens() -> [KeychainCredentials]
func removeRestoreTokenForUsername(_ username: String)
func removeAllRestoreTokens()
func setRestorationToken(_ restorationToken: RestorationToken, forUsername: String)
func restorationTokenForUsername(_ username: String) -> RestorationToken?
func restorationTokens() -> [KeychainCredentials]
func removeRestorationTokenForUsername(_ username: String)
func removeAllRestorationTokens()
}
@@ -0,0 +1,76 @@
//
// Copyright 2022 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
import MatrixRustSDK
struct RestorationToken: Codable, Equatable {
let session: MatrixRustSDK.Session
}
extension MatrixRustSDK.Session: Codable {
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self = .init(accessToken: try container.decode(String.self, forKey: .accessToken),
refreshToken: try container.decodeIfPresent(String.self, forKey: .refreshToken),
userId: try container.decode(String.self, forKey: .userId),
deviceId: try container.decode(String.self, forKey: .deviceId),
homeserverUrl: try container.decode(String.self, forKey: .homeserverUrl),
isSoftLogout: try container.decode(Bool.self, forKey: .isSoftLogout))
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(accessToken, forKey: .accessToken)
try container.encode(refreshToken, forKey: .refreshToken)
try container.encode(userId, forKey: .userId)
try container.encode(deviceId, forKey: .deviceId)
try container.encode(homeserverUrl, forKey: .homeserverUrl)
try container.encode(isSoftLogout, forKey: .isSoftLogout)
}
enum CodingKeys: String, CodingKey {
case accessToken, refreshToken, userId, deviceId, homeserverUrl, isSoftLogout
}
}
#warning("Remove this in a couple of releases - sceriu 03.11.2022")
struct LegacyRestorationToken: Decodable {
let isGuest: Bool?
let isSoftLogout: Bool?
let homeURL: String
let session: Session
enum CodingKeys: String, CodingKey {
case isGuest = "is_guest"
case isSoftLogout = "is_soft_logout"
case homeURL = "homeurl"
case session
}
struct Session: Decodable {
let accessToken: String
let userId: String
let deviceId: String
// swiftlint:disable:next nesting
enum CodingKeys: String, CodingKey {
case accessToken = "access_token"
case userId = "user_id"
case deviceId = "device_id"
}
}
}
@@ -23,7 +23,7 @@ class UserSessionStore: UserSessionStoreProtocol {
private let backgroundTaskService: BackgroundTaskServiceProtocol
/// Whether or not there are sessions in the store.
var hasSessions: Bool { !keychainController.restoreTokens().isEmpty }
var hasSessions: Bool { !keychainController.restorationTokens().isEmpty }
/// The base directory where all session data is stored.
private(set) lazy var baseDirectory: URL = {
@@ -49,7 +49,7 @@ class UserSessionStore: UserSessionStoreProtocol {
}
func restoreUserSession() async -> Result<UserSession, UserSessionStoreError> {
let availableCredentials = keychainController.restoreTokens()
let availableCredentials = keychainController.restorationTokens()
guard let credentials = availableCredentials.first else {
return .failure(.missingCredentials)
@@ -65,7 +65,7 @@ class UserSessionStore: UserSessionStoreProtocol {
MXLog.error("Failed restoring login with error: \(error)")
// On any restoration failure reset the token and restart
keychainController.removeAllRestoreTokens()
keychainController.removeAllRestorationTokens()
deleteSessionDirectory(for: credentials.userID)
return .failure(error)
@@ -85,19 +85,19 @@ class UserSessionStore: UserSessionStoreProtocol {
}
}
func refreshRestoreToken(for userSession: UserSessionProtocol) -> Result<Void, UserSessionStoreError> {
guard let accessToken = userSession.clientProxy.restoreToken else {
func refreshRestorationToken(for userSession: UserSessionProtocol) -> Result<Void, UserSessionStoreError> {
guard let restorationToken = userSession.clientProxy.restorationToken else {
return .failure(.failedRefreshingRestoreToken)
}
keychainController.setRestoreToken(accessToken, forUsername: userSession.clientProxy.userIdentifier)
keychainController.setRestorationToken(restorationToken, forUsername: userSession.clientProxy.userIdentifier)
return .success(())
}
func logout(userSession: UserSessionProtocol) {
let userID = userSession.clientProxy.userIdentifier
keychainController.removeRestoreTokenForUsername(userID)
keychainController.removeRestorationTokenForUsername(userID)
deleteSessionDirectory(for: userID)
}
@@ -109,12 +109,13 @@ class UserSessionStore: UserSessionStoreProtocol {
let builder = ClientBuilder()
.basePath(path: baseDirectory.path)
.username(username: credentials.userID)
.homeserverUrl(url: credentials.restorationToken.session.homeserverUrl)
.userAgent(userAgent: UserAgentBuilder.makeASCIIUserAgent() ?? "unknown")
do {
let client: Client = try await Task.dispatch(on: .global()) {
let client = try builder.build()
try client.restoreLogin(restoreToken: credentials.restoreToken)
try client.restoreSession(session: credentials.restorationToken.session)
return client
}
return await setupProxyForClient(client)
@@ -126,10 +127,10 @@ class UserSessionStore: UserSessionStoreProtocol {
private func setupProxyForClient(_ client: Client) async -> Result<ClientProxyProtocol, UserSessionStoreError> {
do {
let accessToken = try client.restoreToken()
let session = try client.session()
let userId = try client.userId()
keychainController.setRestoreToken(accessToken, forUsername: userId)
keychainController.setRestorationToken(RestorationToken(session: session), forUsername: userId)
} catch {
MXLog.error("Failed setting up user session with error: \(error)")
return .failure(.failedSettingUpSession)
@@ -38,7 +38,7 @@ protocol UserSessionStoreProtocol {
func userSession(for client: Client) async -> Result<UserSession, UserSessionStoreError>
/// Refresh the restore token of the client for a given session.
func refreshRestoreToken(for userSession: UserSessionProtocol) -> Result<Void, UserSessionStoreError>
func refreshRestorationToken(for userSession: UserSessionProtocol) -> Result<Void, UserSessionStoreError>
/// Logs out of the specified session.
func logout(userSession: UserSessionProtocol)
@@ -1,69 +0,0 @@
//
// Copyright 2022 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
import KeychainAccess
class KeychainController: KeychainControllerProtocol {
private let keychain: Keychain
init(identifier: String) {
keychain = Keychain(service: identifier)
}
func setRestoreToken(_ restoreToken: String, forUsername username: String) {
do {
try keychain.set(restoreToken, key: username)
} catch {
MXLog.error("Failed storing user restore token with error: \(error)")
}
}
func restoreTokenForUsername(_ username: String) -> String? {
do {
return try keychain.get(username)
} catch {
MXLog.error("Failed retrieving user restore token")
return nil
}
}
func restoreTokens() -> [KeychainCredentials] {
keychain.allKeys().compactMap { username in
guard let restoreToken = restoreTokenForUsername(username) else {
return nil
}
return KeychainCredentials(userID: username, restoreToken: restoreToken)
}
}
func removeRestoreTokenForUsername(_ username: String) {
do {
try keychain.remove(username)
} catch {
MXLog.error("Failed removing restore token with error: \(error)")
}
}
func removeAllRestoreTokens() {
do {
try keychain.removeAll()
} catch {
MXLog.error("Failed removing all tokens")
}
}
}