Skip to content

Commit 08efd35

Browse files
committed
Rework MatrixLoginFlow and MatrixLoginFlowType
1 parent da116df commit 08efd35

File tree

5 files changed

+127
-38
lines changed

5 files changed

+127
-38
lines changed

Sources/MatrixClient/API/Auth/Interactive.swift

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ public struct MatrixInteractiveAuth: MatrixResponse {
1313
flows: [MatrixInteractiveAuth.Flow],
1414
params: [String: AnyCodable],
1515
session: String? = nil,
16-
completed: [MatrixLoginFlow]? = nil
16+
completed: [MatrixLoginFlowType]? = nil
1717
) {
1818
self.flows = flows
1919
self.params = params
@@ -32,14 +32,14 @@ public struct MatrixInteractiveAuth: MatrixResponse {
3232
/// in subsequent attempts to authenticate in the same API call.
3333
public var session: String?
3434

35-
public var completed: [MatrixLoginFlow]?
35+
public var completed: [MatrixLoginFlowType]?
3636

3737
// MARK: Dynamic vars
3838

3939
public var notCompletedStages: [Flow] {
4040
var ret: [Flow] = []
4141
for flow in flows {
42-
var stages: [MatrixLoginFlow] = []
42+
var stages: [MatrixLoginFlowType] = []
4343
for stage in flow.stages {
4444
if !(completed?.contains(stage) ?? false) {
4545
stages.append(stage)
@@ -51,7 +51,7 @@ public struct MatrixInteractiveAuth: MatrixResponse {
5151
}
5252

5353
/// Return the next stage, which did not yet complete, from the first login flow
54-
public var nextStage: MatrixLoginFlow? {
54+
public var nextStage: MatrixLoginFlowType? {
5555
notCompletedStages[0].stages[0]
5656
}
5757

@@ -69,20 +69,20 @@ public struct MatrixInteractiveAuth: MatrixResponse {
6969

7070
/// Test if the given login flow is supported by the home server.
7171
/// This returns true, the flow is contained in one or more stages. This means the flow could be required.
72-
public func isOptional(_ flow: MatrixLoginFlow) -> Bool {
72+
public func isOptional(_ flow: MatrixLoginFlowType) -> Bool {
7373
flows.first { $0.stages.contains(flow) } != nil
7474
}
7575

76-
public func isOptional(notCompletedFlow flow: MatrixLoginFlow) -> Bool {
76+
public func isOptional(notCompletedFlow flow: MatrixLoginFlowType) -> Bool {
7777
notCompletedStages.first { $0.stages.contains(flow) } != nil
7878
}
7979

8080
/// Test if th given flow is required by every flow supported by the homeserver.
81-
public func isRequierd(_ flow: MatrixLoginFlow) -> Bool {
81+
public func isRequierd(_ flow: MatrixLoginFlowType) -> Bool {
8282
flows.allSatisfy { $0.stages.contains(flow) }
8383
}
8484

85-
public func isRequierd(notCompletedFlow flow: MatrixLoginFlow) -> Bool {
85+
public func isRequierd(notCompletedFlow flow: MatrixLoginFlowType) -> Bool {
8686
notCompletedStages.allSatisfy { $0.stages.contains(flow) }
8787
}
8888

@@ -96,39 +96,39 @@ public struct MatrixInteractiveAuth: MatrixResponse {
9696
}
9797

9898
public struct LoginFlowWithParams {
99-
public let flow: MatrixLoginFlow
99+
public let flow: MatrixLoginFlowType
100100
public let params: AnyCodable?
101101
}
102102
}
103103

104104
public extension MatrixInteractiveAuth {
105105
struct Flow: Codable {
106-
public var stages: [MatrixLoginFlow] = []
106+
public var stages: [MatrixLoginFlowType] = []
107107
}
108108
}
109109

110110
/// struct to return to server
111111
public struct MatrixInteractiveAuthResponse: Codable {
112112
public var session: String?
113113

114-
public var type: MatrixLoginFlow?
114+
public var type: MatrixLoginFlowType?
115115

116116
public var extraInfo: [String: AnyCodable]
117117

118-
public init(session: String? = nil, type: MatrixLoginFlow?, extraInfo: [String: AnyCodable] = [:]) {
118+
public init(session: String? = nil, type: MatrixLoginFlowType?, extraInfo: [String: AnyCodable] = [:]) {
119119
self.session = session
120120
self.type = type
121121
self.extraInfo = extraInfo
122122
}
123123

124124
public init(recaptchaResponse: String, session: String? = nil) {
125-
type = MatrixLoginFlow.recaptcha
125+
type = MatrixLoginFlowType.recaptcha
126126
self.session = session
127127
extraInfo = ["response": AnyCodable(stringLiteral: recaptchaResponse)]
128128
}
129129

130130
public init(emailClientSecret clientSecret: String, emailSID sid: String, session: String? = nil) {
131-
type = MatrixLoginFlow.email
131+
type = MatrixLoginFlowType.email
132132
self.session = session
133133
extraInfo = [
134134
"threepid_creds": [
@@ -165,7 +165,7 @@ public extension MatrixInteractiveAuthResponse {
165165
init(from decoder: Decoder) throws {
166166
let container = try decoder.container(keyedBy: KnownCodingKeys.self)
167167
session = try container.decodeIfPresent(String.self, forKey: .session)
168-
type = try container.decode(MatrixLoginFlow.self, forKey: .type)
168+
type = try container.decode(MatrixLoginFlowType.self, forKey: .type)
169169

170170
extraInfo = [:]
171171
let extraContainer = try decoder.container(keyedBy: DynamicCodingKeys.self)

Sources/MatrixClient/API/Auth/Login.swift

Lines changed: 104 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,11 @@
66
//
77

88
import Foundation
9+
import AnyCodable
910

1011
public struct MatrixLoginFlowRequest {
1112
public struct ResponseStruct: MatrixResponse {
12-
var flows: [FlowType]
13-
14-
struct FlowType: Codable {
15-
var type: MatrixLoginFlow
16-
}
13+
var flows: [MatrixLoginFlow]
1714
}
1815
}
1916

@@ -35,7 +32,7 @@ extension MatrixLoginFlowRequest: MatrixRequest {
3532

3633
@frozen
3734
/// A login type supported by the homeserver.
38-
public struct MatrixLoginFlow: RawRepresentable, Codable, Equatable, Hashable {
35+
public struct MatrixLoginFlowType: RawRepresentable, Codable, Equatable, Hashable {
3936
public typealias RawValue = String
4037

4138
public var rawValue: String
@@ -68,7 +65,7 @@ public struct MatrixLoginFlow: RawRepresentable, Codable, Equatable, Hashable {
6865
/// }
6966
/// ```
7067
/// In the case that the homeserver does not know about the supplied 3PID, the homeserver must respond with 403 Forbidden.
71-
public static let password: MatrixLoginFlow = "m.login.password"
68+
public static let password: MatrixLoginFlowType = "m.login.password"
7269

7370
/// The user completes a Google ReCaptcha 2.0 challenge
7471
///
@@ -80,8 +77,8 @@ public struct MatrixLoginFlow: RawRepresentable, Codable, Equatable, Hashable {
8077
/// "session": "<session ID>"
8178
/// }
8279
/// ```
83-
public static let recaptcha: MatrixLoginFlow = "m.login.recaptcha"
84-
public static let oauth2: MatrixLoginFlow = "m.login.oauth2"
80+
public static let recaptcha: MatrixLoginFlowType = "m.login.recaptcha"
81+
public static let oauth2: MatrixLoginFlowType = "m.login.oauth2"
8582

8683
/// Authentication is supported by authorising with an external single sign-on provider.
8784
///
@@ -95,12 +92,12 @@ public struct MatrixLoginFlow: RawRepresentable, Codable, Equatable, Hashable {
9592
/// The homeserver then validates the response from the single sign-on provider and updates the user-interactive authentication session to mark the single sign-on stage has been completed. The browser is shown the fallback authentication completion page.
9693
///
9794
/// Once the flow has completed, the client retries the request with the session only, as above.
98-
public static let sso: MatrixLoginFlow = "m.login.sso"
99-
public static let email: MatrixLoginFlow = "m.login.email.identity"
100-
public static let msisdn: MatrixLoginFlow = "m.login.msisdn"
101-
public static let token: MatrixLoginFlow = "m.login.token"
102-
public static let dummy: MatrixLoginFlow = "m.login.dummy"
103-
public static let terms: MatrixLoginFlow = "m.login.terms"
95+
public static let sso: MatrixLoginFlowType = "m.login.sso"
96+
public static let email: MatrixLoginFlowType = "m.login.email.identity"
97+
public static let msisdn: MatrixLoginFlowType = "m.login.msisdn"
98+
public static let token: MatrixLoginFlowType = "m.login.token"
99+
public static let dummy: MatrixLoginFlowType = "m.login.dummy"
100+
public static let terms: MatrixLoginFlowType = "m.login.terms"
104101

105102
public init(from decoder: Decoder) throws {
106103
let container = try decoder.singleValueContainer()
@@ -121,12 +118,103 @@ public struct MatrixLoginFlow: RawRepresentable, Codable, Equatable, Hashable {
121118
}
122119
}
123120

124-
extension MatrixLoginFlow: ExpressibleByStringLiteral {
121+
extension MatrixLoginFlowType: ExpressibleByStringLiteral {
125122
public init(stringLiteral value: StringLiteralType) {
126123
rawValue = value
127124
}
128125
}
129126

127+
public struct MatrixLoginFlow {
128+
var type: MatrixLoginFlowType
129+
130+
var identiyProviders: [IdentityProvider]?
131+
132+
var extraInfo: [String: AnyCodable]
133+
134+
public struct IdentityProvider: Codable {
135+
public init(brand: String? = nil, icon: String? = nil, id: String, name: String) {
136+
self.brand = brand
137+
self.icon = icon
138+
self.id = id
139+
self.name = name
140+
}
141+
142+
/// Optional UI hint for what kind of common SSO provider is being described in this ``IdentityProvider``.
143+
///
144+
/// Matrix maintains a registry of identifiers in the
145+
/// [matrix-spec repo](https://github.com/matrix-org/matrix-spec/blob/main/informal/idp-brands.md) to ensure clients and servers are aligned on major/common brands.
146+
///
147+
/// Clients should prefer the brand over the icon, when both are provided.
148+
/// Clients are not required to support any particular brand, including those in the registry, though are expected to be able to present any IdP based off the name/icon to the user regardless.
149+
///
150+
/// Unregistered brands are permitted using the Common Namespaced Identifier Grammar, though excluding the namespace requirements. For example, examplesso is a valid brand which is not in the registry but still permitted. Servers should be mindful that clients might not support their unregistered brand usage as intended by the server.
151+
public var brand: String?
152+
153+
/// Optional MXC URI to provide an image/icon representing the IdP. Intended to be shown alongside the name if provided.
154+
public var icon: String?
155+
156+
/// Opaque string chosen by the homeserver, uniquely identifying the IdP from other IdPs the homeserver might support.
157+
///
158+
/// Should be between 1 and 255 characters in length, containing unreserved characters under RFC 3986 (ALPHA DIGIT "-" / "." / "_" / "~"). Clients are not intended to parse or infer meaning from opaque strings.
159+
public var id: String
160+
161+
/// Human readable description for the ``IdentityProvider``, intended to be shown to the user.
162+
public var name: String
163+
}
164+
}
165+
166+
extension MatrixLoginFlow: Codable {
167+
private enum KnownCodingKeys: String, CodingKey, CaseIterable {
168+
case type
169+
case identiyProviders = "identity_providers"
170+
171+
static func doesNotContain(_ key: DynamicCodingKeys) -> Bool {
172+
!Self.allCases.map(\.stringValue).contains(key.stringValue)
173+
}
174+
}
175+
176+
struct DynamicCodingKeys: CodingKey {
177+
var stringValue: String
178+
init?(stringValue: String) {
179+
self.stringValue = stringValue
180+
}
181+
182+
// not used here, but a protocol requirement
183+
var intValue: Int?
184+
init?(intValue _: Int) {
185+
nil
186+
}
187+
}
188+
189+
public init(from decoder: Decoder) throws {
190+
let container = try decoder.container(keyedBy: KnownCodingKeys.self)
191+
type = try container.decode(MatrixLoginFlowType.self, forKey: .type)
192+
identiyProviders = try container.decodeIfPresent([IdentityProvider].self, forKey: .identiyProviders)
193+
194+
extraInfo = [:]
195+
let extraContainer = try decoder.container(keyedBy: DynamicCodingKeys.self)
196+
197+
for key in extraContainer.allKeys where KnownCodingKeys.doesNotContain(key) {
198+
let decoded = try extraContainer.decode(
199+
AnyCodable.self,
200+
forKey: DynamicCodingKeys(stringValue: key.stringValue)!
201+
)
202+
self.extraInfo[key.stringValue] = decoded
203+
}
204+
}
205+
206+
public func encode(to encoder: Encoder) throws {
207+
var container = encoder.container(keyedBy: KnownCodingKeys.self)
208+
try container.encode(type, forKey: .type)
209+
try container.encodeIfPresent(identiyProviders, forKey: .identiyProviders)
210+
211+
var extraContainer = encoder.container(keyedBy: DynamicCodingKeys.self)
212+
for (name, value) in extraInfo {
213+
try extraContainer.encode(value, forKey: .init(stringValue: name)!)
214+
}
215+
}
216+
}
217+
130218
public enum MatrixLoginUserIdentifier: Codable {
131219
/// 5.4.6.1 Matrix User id
132220
///

Sources/MatrixClient/MatrixClient/MatrixClient+Auth.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,15 @@ public extension MatrixClient {
2121
func getLoginFlows() async throws -> [MatrixLoginFlow] {
2222
try await MatrixLoginFlowRequest()
2323
.response(on: homeserver, withToken: accessToken, with: (), withUrlSession: urlSession)
24-
.flows.map(\.type)
24+
.flows
2525
}
2626

2727
/// Test if the server supports password authentication.
2828
@available(swift, introduced: 5.5)
2929
@available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *)
3030
func supportsPasswordAuth() async throws -> Bool {
3131
let flows = try await getLoginFlows()
32-
return flows.contains(where: { $0 == MatrixLoginFlow.password })
32+
return flows.contains(where: { $0.type == MatrixLoginFlowType.password })
3333
}
3434

3535
// MARK: - Register
@@ -120,7 +120,7 @@ public extension MatrixClient {
120120
displayName: String? = nil,
121121
deviceId: String? = nil
122122
) async throws -> MatrixLogin {
123-
let flow: MatrixLoginFlow
123+
let flow: MatrixLoginFlowType
124124
if token {
125125
flow = .token
126126
} else {

Sources/MatrixCore/MatrixStore.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ public protocol MatrixStore {
2020
//associatedtype AccountMapping: MatrixStoreAccountRoom
2121

2222
func saveAccountInfo(account: AccountInfo) async throws
23+
func saveAccountInfo(_ mxID: MatrixFullUserIdentifier, name: String, homeServer: MatrixHomeserver, deviceId: String, accessToken: String?, saveToKeychain: Bool, extraKeychainArguments: [String: Any]) async throws -> AccountInfo
2324

2425
func getAccountInfo(accountID: AccountInfo.AccountIdentifier) async throws -> AccountInfo
2526

Tests/MatrixClientTests/ApiAuthInteractiveTests.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@ import XCTest
1111

1212
final class ApiAuthInteractiveTests: XCTestCase {
1313
var flow = MatrixInteractiveAuth(flows: [
14-
MatrixInteractiveAuth.Flow(stages: [MatrixLoginFlow.recaptcha, MatrixLoginFlow.terms, MatrixLoginFlow.email]),
14+
MatrixInteractiveAuth.Flow(stages: [MatrixLoginFlowType.recaptcha, MatrixLoginFlowType.terms, MatrixLoginFlowType.email]),
1515
MatrixInteractiveAuth
16-
.Flow(stages: [MatrixLoginFlow.recaptcha, MatrixLoginFlow.token, MatrixLoginFlow.oauth2,
17-
MatrixLoginFlow.email]),
18-
], params: [:], session: nil, completed: [MatrixLoginFlow.email, MatrixLoginFlow.terms])
16+
.Flow(stages: [MatrixLoginFlowType.recaptcha, MatrixLoginFlowType.token, MatrixLoginFlowType.oauth2,
17+
MatrixLoginFlowType.email]),
18+
], params: [:], session: nil, completed: [MatrixLoginFlowType.email, MatrixLoginFlowType.terms])
1919

2020
func testIsOptional() throws {
2121
XCTAssertTrue(flow.isOptional(.recaptcha))

0 commit comments

Comments
 (0)