Skip to content

Commit 8a7c922

Browse files
authored
fix: SSO reconnection fails - WPB-23076 (#4268)
1 parent 4dc0675 commit 8a7c922

File tree

8 files changed

+259
-11
lines changed

8 files changed

+259
-11
lines changed

WireAuthentication/Sources/WireAuthentication/Components/ReloginViaSSOComponent.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,11 @@ extension ReloginViaSSOComponent: ReloginViaSSOViewModel.Factory {
100100
)
101101
}
102102

103+
@MainActor
104+
func validateSSOCodeUseCase() -> any ValidateSSOCodeUseCaseProtocol {
105+
ValidateSSOCodeUseCase()
106+
}
107+
103108
@MainActor
104109
func noHistoryView(result: AuthenticationResult) -> NoHistoryView {
105110
let factory = noHistoryFactory(result: result)
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
//
2+
// Wire
3+
// Copyright (C) 2026 Wire Swiss GmbH
4+
//
5+
// This program is free software: you can redistribute it and/or modify
6+
// it under the terms of the GNU General Public License as published by
7+
// the Free Software Foundation, either version 3 of the License, or
8+
// (at your option) any later version.
9+
//
10+
// This program is distributed in the hope that it will be useful,
11+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
// GNU General Public License for more details.
14+
//
15+
// You should have received a copy of the GNU General Public License
16+
// along with this program. If not, see http://www.gnu.org/licenses/.
17+
//
18+
19+
import Foundation
20+
21+
// sourcery: AutoMockable
22+
public protocol ValidateSSOCodeUseCaseProtocol: Sendable {
23+
24+
func invoke(ssoCode: String) throws -> UUID
25+
26+
}
27+
28+
public enum ValidateSSOCodeFailure: Error {
29+
30+
case invalidCode
31+
32+
}
33+
34+
public protocol ValidateSSOCodeUseCaseFactory {
35+
36+
@MainActor
37+
func validateSSOCodeUseCase() -> any ValidateSSOCodeUseCaseProtocol
38+
39+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
//
2+
// Wire
3+
// Copyright (C) 2026 Wire Swiss GmbH
4+
//
5+
// This program is free software: you can redistribute it and/or modify
6+
// it under the terms of the GNU General Public License as published by
7+
// the Free Software Foundation, either version 3 of the License, or
8+
// (at your option) any later version.
9+
//
10+
// This program is distributed in the hope that it will be useful,
11+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
// GNU General Public License for more details.
14+
//
15+
// You should have received a copy of the GNU General Public License
16+
// along with this program. If not, see http://www.gnu.org/licenses/.
17+
//
18+
19+
import Foundation
20+
import WireAuthenticationAPI
21+
22+
package struct ValidateSSOCodeUseCase: ValidateSSOCodeUseCaseProtocol {
23+
24+
package init() {}
25+
26+
package func invoke(ssoCode: String) throws -> UUID {
27+
28+
if let uuid = SSOCodeValidator.validate(
29+
ssoCode: ssoCode.trimmingCharacters(in: .whitespacesAndNewlines)
30+
) {
31+
return uuid
32+
}
33+
34+
throw ValidateSSOCodeFailure.invalidCode
35+
}
36+
}

WireAuthentication/Sources/WireAuthenticationUI/Mocks/FakeDetermineAuthMethodFactory.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,4 +84,5 @@ struct FakeDetermineAuthMethodFactory: DetermineAuthMethodFactory,
8484
func validateEmailOrSSOCodeUseCase() -> any WireAuthenticationAPI.ValidateEmailOrSSOCodeUseCaseProtocol {
8585
mockDependencies.validateEmailOrSSOCodeUseCase()
8686
}
87+
8788
}

WireAuthentication/Sources/WireAuthenticationUI/Mocks/FakeReloginViaSSOFactory.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import WireAuthenticationAPI
2121
import WireNetwork
2222
import WireReusableUIComponents
2323

24-
struct FakeReloginViaSSOFactory: ReloginViaSSOFactory, LoginViaSSOUseCaseFactory {
24+
struct FakeReloginViaSSOFactory: ReloginViaSSOFactory, LoginViaSSOUseCaseFactory, ValidateSSOCodeUseCaseFactory {
2525

2626
let environment: BackendEnvironment2
2727
let existsAnotherAccount: Bool
@@ -45,4 +45,7 @@ struct FakeReloginViaSSOFactory: ReloginViaSSOFactory, LoginViaSSOUseCaseFactory
4545
try await MockDependencies().loginViaSSOUseCase(environment: environment)
4646
}
4747

48+
func validateSSOCodeUseCase() -> any ValidateSSOCodeUseCaseProtocol {
49+
MockDependencies().validateSSOCodeUseCase()
50+
}
4851
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
//
2+
// Wire
3+
// Copyright (C) 2026 Wire Swiss GmbH
4+
//
5+
// This program is free software: you can redistribute it and/or modify
6+
// it under the terms of the GNU General Public License as published by
7+
// the Free Software Foundation, either version 3 of the License, or
8+
// (at your option) any later version.
9+
//
10+
// This program is distributed in the hope that it will be useful,
11+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
// GNU General Public License for more details.
14+
//
15+
// You should have received a copy of the GNU General Public License
16+
// along with this program. If not, see http://www.gnu.org/licenses/.
17+
//
18+
19+
import Foundation
20+
import WireAuthenticationAPI
21+
22+
extension MockDependencies: ValidateSSOCodeUseCaseFactory {
23+
24+
func validateSSOCodeUseCase() -> any ValidateSSOCodeUseCaseProtocol {
25+
MockValidateSSOCodeUseCase()
26+
}
27+
28+
}
29+
30+
struct MockValidateSSOCodeUseCase: ValidateSSOCodeUseCaseProtocol {
31+
32+
func invoke(ssoCode: String) throws -> UUID {
33+
UUID()
34+
}
35+
36+
}

WireAuthentication/Sources/WireAuthenticationUI/Views/Login/ReloginViaSSO/ReloginViaSSOViewModel.swift

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ package final class ReloginViaSSOViewModel: ObservableObject {
2626

2727
package typealias Factory =
2828
LoginViaSSOUseCaseFactory &
29-
ReloginViaSSOFactory
29+
ReloginViaSSOFactory &
30+
ValidateSSOCodeUseCaseFactory
3031

3132
// MARK: - View state
3233

@@ -65,24 +66,23 @@ package final class ReloginViaSSOViewModel: ObservableObject {
6566
// MARK: - Actions
6667

6768
func login() async {
68-
var code: UUID?
69-
if isSSOCodeRequired {
70-
guard let ssoCode = UUID(uuidString: rawSSOCode) else {
71-
router.presentAlert(.incorrectSSOCode)
72-
return
73-
}
74-
code = ssoCode
75-
}
7669

7770
do {
71+
var code: UUID?
72+
73+
if isSSOCodeRequired {
74+
let validateSSOCode = factory.validateSSOCodeUseCase()
75+
code = try validateSSOCode.invoke(ssoCode: rawSSOCode)
76+
}
77+
7878
let loginViaSSO = try await factory.loginViaSSOUseCase(environment: environment)
7979
let authResult = try await loginViaSSO.invoke(code: code)
8080
router.navigate(to: ReloginViaSSODestination.noHistory(authResult))
8181
} catch LoginViaSSOUseCaseError.userCancelled {
8282
// No op
8383
} catch LoginViaSSOUseCaseError.noDefaultCodeAvailable {
8484
isSSOCodeRequired = true
85-
} catch LoginViaSSOUseCaseError.invalidCode {
85+
} catch LoginViaSSOUseCaseError.invalidCode, ValidateSSOCodeFailure.invalidCode {
8686
router.presentAlert(.incorrectSSOCode)
8787
} catch LoginViaSSOUseCaseError.invalidURL {
8888
router.presentAlert(.invalidSSOLink)
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
//
2+
// Wire
3+
// Copyright (C) 2026 Wire Swiss GmbH
4+
//
5+
// This program is free software: you can redistribute it and/or modify
6+
// it under the terms of the GNU General Public License as published by
7+
// the Free Software Foundation, either version 3 of the License, or
8+
// (at your option) any later version.
9+
//
10+
// This program is distributed in the hope that it will be useful,
11+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
// GNU General Public License for more details.
14+
//
15+
// You should have received a copy of the GNU General Public License
16+
// along with this program. If not, see http://www.gnu.org/licenses/.
17+
//
18+
19+
import Foundation
20+
import Testing
21+
import WireAuthenticationAPI
22+
23+
@testable import WireAuthenticationLogic
24+
25+
@Suite
26+
struct ValidateSSOCodeUseCaseTests {
27+
28+
let sut: ValidateSSOCodeUseCase
29+
let testUUIDString = "648e79cb-88b9-42a8-8ea7-dd93e97f4da1"
30+
31+
init() {
32+
self.sut = ValidateSSOCodeUseCase()
33+
}
34+
35+
// MARK: - Valid SSO Codes
36+
37+
@Test(
38+
"Valid SSO codes with different cases",
39+
arguments: [
40+
("wire-648e79cb-88b9-42a8-8ea7-dd93e97f4da1", "lowercase"),
41+
("WIRE-648E79CB-88B9-42A8-8EA7-DD93E97F4DA1", "uppercase"),
42+
("Wire-648E79CB-88b9-42a8-8ea7-DD93E97F4DA1", "mixedCase")
43+
]
44+
)
45+
func validSSOCodes(ssoCode: String, caseName: String) throws {
46+
// when
47+
let result = try sut.invoke(ssoCode: ssoCode)
48+
49+
// then
50+
let expectedUUID = UUID(uuidString: testUUIDString)!
51+
#expect(result == expectedUUID)
52+
}
53+
54+
// MARK: - Valid SSO Codes with Whitespace
55+
56+
@Test(
57+
"Valid SSO codes with various whitespace",
58+
arguments: [
59+
(" ", " ", "leading and trailing spaces"),
60+
(" ", " ", "multiple leading and trailing spaces"),
61+
("\n", "\n", "newlines"),
62+
("\t", "\t", "tabs"),
63+
(" \n\t ", " \r\n\t ", "mixed whitespace")
64+
]
65+
)
66+
func validSSOCodesWithWhitespace(leading: String, trailing: String, description: String) throws {
67+
// given
68+
let ssoCode = "\(leading)wire-\(testUUIDString)\(trailing)"
69+
70+
// when
71+
let result = try sut.invoke(ssoCode: ssoCode)
72+
73+
// then
74+
let expectedUUID = UUID(uuidString: testUUIDString)!
75+
#expect(result == expectedUUID)
76+
}
77+
78+
// MARK: - Invalid SSO Codes
79+
80+
@Test(
81+
"Invalid SSO codes throw error",
82+
arguments: [
83+
("648e79cb-88b9-42a8-8ea7-dd93e97f4da1", "missing prefix"),
84+
("fire-648e79cb-88b9-42a8-8ea7-dd93e97f4da1", "wrong prefix"),
85+
("wire-invalid-uuid-format", "invalid UUID format"),
86+
("wire-648e79cb-88b9-42a8-8ea7-dd93e97f4da", "too short UUID"),
87+
("wire-648e79cb-88b9-42a8-8ea7-dd93e97f4da12", "too long UUID"),
88+
("", "empty string"),
89+
(" \n\t ", "only whitespace"),
90+
("wire-", "only prefix"),
91+
(" wire- ", "whitespace with prefix")
92+
]
93+
)
94+
func invalidSSOCodes(ssoCode: String, description: String) throws {
95+
// when/then
96+
#expect(throws: ValidateSSOCodeFailure.self) {
97+
try sut.invoke(ssoCode: ssoCode)
98+
}
99+
}
100+
101+
// MARK: - Specific Edge Cases
102+
103+
@Test("Valid SSO code with only leading whitespace")
104+
func validSSOCodeWithLeadingWhitespace() throws {
105+
// given
106+
let ssoCode = " wire-\(testUUIDString)"
107+
108+
// when
109+
let result = try sut.invoke(ssoCode: ssoCode)
110+
111+
// then
112+
let expectedUUID = UUID(uuidString: testUUIDString)!
113+
#expect(result == expectedUUID)
114+
}
115+
116+
@Test("Valid SSO code with only trailing whitespace")
117+
func validSSOCodeWithTrailingWhitespace() throws {
118+
// given
119+
let ssoCode = "wire-\(testUUIDString) "
120+
121+
// when
122+
let result = try sut.invoke(ssoCode: ssoCode)
123+
124+
// then
125+
let expectedUUID = UUID(uuidString: testUUIDString)!
126+
#expect(result == expectedUUID)
127+
}
128+
}

0 commit comments

Comments
 (0)