Skip to content

Commit 5eb933c

Browse files
authored
feat: expose flowOutput on AuthenticationResponse (#154)
1 parent 793c7f6 commit 5eb933c

8 files changed

Lines changed: 95 additions & 23 deletions

File tree

src/flows/FlowCoordinator.swift

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -307,25 +307,40 @@ public class DescopeFlowCoordinator {
307307
}
308308

309309
// Authentication
310+
311+
private func handleData(_ data: Data?) async {
312+
let jwtResponse = await parseJWTResponse(data)
313+
if let jwtResponse {
314+
do {
315+
let authResponse = try jwtResponse.convert()
316+
logger.debug("Finishing flow with an authentication response", data)
317+
return handleSuccess(authResponse)
318+
} catch {
319+
logger.debug("Finishing flow with a partial authentication response")
320+
}
321+
}
310322

311-
private func handleAuthentication(_ data: Data) {
312-
logger.info("Finishing flow authentication")
313-
Task {
314-
guard let authResponse = await parseAuthentication(data) else { return }
315-
handleSuccess(authResponse)
323+
if let session = flow?.providedSession {
324+
if jwtResponse == nil {
325+
logger.info("Finishing flow authentication without an authentication response")
326+
}
327+
return handleSuccess(AuthenticationResponse(sessionToken: session.sessionToken, refreshToken: session.refreshToken, user: session.user, isFirstAuthentication: false, flowOutput: jwtResponse?.flowOutput ?? [:]))
316328
}
329+
330+
logger.error("Couldn't find session to finish flow", flow?.sessionProvider == nil ? "nil provider" : "custom provider")
331+
handleError(DescopeError.flowFailed.with(message: "No valid authentication tokens found"))
317332
}
318333

319-
private func parseAuthentication(_ data: Data) async -> AuthenticationResponse? {
334+
private func parseJWTResponse(_ data: Data?) async -> DescopeClient.JWTResponse? {
335+
guard let data, let webView else { return nil }
320336
do {
321-
guard let webView else { return nil }
322337
var jwtResponse = try JSONDecoder().decode(DescopeClient.JWTResponse.self, from: data)
323338
let cookies = await webView.configuration.websiteDataStore.httpCookieStore.cookies(for: jwtResponse.cookieDomain, at: webView.url)
324339
try jwtResponse.setValues(from: data, cookies: cookies, refreshCookieName: bridge.attributes.refreshCookieName)
325-
return try jwtResponse.convert()
340+
return jwtResponse
326341
} catch {
342+
// should never happen because all JWTResponse fields are optional
327343
logger.error("Unexpected error parsing authentication response", error, String(bytes: data, encoding: .utf8))
328-
handleError(DescopeError.flowFailed.with(message: "No valid authentication response found"))
329344
return nil
330345
}
331346
}
@@ -401,13 +416,8 @@ extension DescopeFlowCoordinator: FlowBridgeDelegate {
401416
}
402417

403418
func bridgeDidFinish(_ bridge: FlowBridge, data: Data?) {
404-
if let data {
405-
handleAuthentication(data)
406-
} else if let session = flow?.providedSession {
407-
handleSuccess(AuthenticationResponse(sessionToken: session.sessionToken, refreshToken: session.refreshToken, user: session.user, isFirstAuthentication: false))
408-
} else {
409-
logger.error("Couldn't find session to finish flow", flow?.sessionProvider == nil ? "nil provider" : "custom provider")
410-
handleError(DescopeError.flowFailed.with(message: "No valid authentication tokens found"))
419+
Task { @MainActor in
420+
await handleData(data)
411421
}
412422
}
413423
}

src/internal/http/DescopeClient.swift

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -443,11 +443,18 @@ final class DescopeClient: HTTPClient, @unchecked Sendable {
443443
var sessionJwt: String?
444444
var refreshJwt: String?
445445
var user: UserResponse?
446-
var firstSeen: Bool
446+
var firstSeen: Bool?
447447
var cookieDomain: String?
448448
var cookieName: String?
449449
var sessionCookieName: String?
450450
var externalToken: String?
451+
var flowOutput: [String: Any]?
452+
453+
// we enumerate the properties explicitly to skip over flowOutput, whose
454+
// arbitrary JSON object is populated separately in setValues below
455+
enum CodingKeys: String, CodingKey {
456+
case sessionJwt, refreshJwt, user, firstSeen, cookieDomain, cookieName, sessionCookieName, externalToken
457+
}
451458

452459
mutating func setValues(from data: Data, response: HTTPURLResponse) throws {
453460
guard let url = response.url, let fields = response.allHeaderFields as? [String: String] else { return }
@@ -463,6 +470,9 @@ final class DescopeClient: HTTPClient, @unchecked Sendable {
463470
if let dict = json["user"] as? [String: Any] {
464471
user?.setCustomAttributes(from: dict)
465472
}
473+
if let output = json["flowOutput"] as? [String: Any], !output.isEmpty {
474+
flowOutput = output
475+
}
466476
if sessionJwt == nil || sessionJwt == "" {
467477
let name = (sessionCookieName == "" ? nil : sessionCookieName) ?? DescopeClient.sessionCookieName
468478
sessionJwt = findTokenCookie(named: name, in: cookies)

src/internal/others/Internal.swift

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,9 +227,10 @@ extension AuthenticationResponse: Codable {
227227
enum CodingKeys: String, CodingKey {
228228
case sessionToken = "sessionJwt"
229229
case refreshToken = "refreshJwt"
230+
case externalToken
230231
case user
231232
case isFirstAuthentication
232-
case externalToken
233+
case flowOutput
233234
}
234235

235236
public init(from decoder: Decoder) throws {
@@ -239,6 +240,11 @@ extension AuthenticationResponse: Codable {
239240
user = try values.decode(DescopeUser.self, forKey: .user)
240241
isFirstAuthentication = try values.decode(Bool.self, forKey: .isFirstAuthentication)
241242
externalToken = try values.decodeIfPresent(String.self, forKey: .externalToken)
243+
if let value = try values.decodeIfPresent(String.self, forKey: .flowOutput) {
244+
flowOutput = (try? JSONSerialization.jsonObject(with: Data(value.utf8))) as? [String: Any] ?? [:]
245+
} else {
246+
flowOutput = [:]
247+
}
242248
}
243249

244250
public func encode(to encoder: Encoder) throws {
@@ -248,6 +254,11 @@ extension AuthenticationResponse: Codable {
248254
try values.encode(user, forKey: .user)
249255
try values.encode(isFirstAuthentication, forKey: .isFirstAuthentication)
250256
try values.encodeIfPresent(externalToken, forKey: .externalToken)
257+
if !flowOutput.isEmpty, JSONSerialization.isValidJSONObject(flowOutput) {
258+
let data = try JSONSerialization.data(withJSONObject: flowOutput)
259+
let value = String(bytes: data, encoding: .utf8)
260+
try values.encodeIfPresent(value, forKey: .flowOutput)
261+
}
251262
}
252263
}
253264

src/internal/routes/Auth.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ final class Auth: DescopeAuth {
2121
func migrateSession(externalToken: String) async throws(DescopeError) -> AuthenticationResponse {
2222
let response: MigrateResponse = try await client.migrate(externalToken: externalToken).convert()
2323
let user = try await me(refreshJwt: response.refreshToken.jwt)
24-
return AuthenticationResponse(sessionToken: response.sessionToken, refreshToken: response.refreshToken, user: user, isFirstAuthentication: false)
24+
return AuthenticationResponse(sessionToken: response.sessionToken, refreshToken: response.refreshToken, user: user, isFirstAuthentication: false, flowOutput: [:])
2525
}
2626

2727
func revokeSessions(_ revoke: RevokeType, refreshJwt: String) async throws(DescopeError) {

src/internal/routes/Shared.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ extension DescopeClient.JWTResponse {
7373
guard let sessionJwt, !sessionJwt.isEmpty else { throw DescopeError.decodeError.with(message: "Missing session JWT") }
7474
guard let refreshJwt, !refreshJwt.isEmpty else { throw DescopeError.decodeError.with(message: "Missing refresh JWT") }
7575
guard let user else { throw DescopeError.decodeError.with(message: "Missing user details") }
76-
return try AuthenticationResponse(sessionToken: Token(jwt: sessionJwt), refreshToken: Token(jwt: refreshJwt), user: user.convert(), isFirstAuthentication: firstSeen, externalToken: externalToken)
76+
return try AuthenticationResponse(sessionToken: Token(jwt: sessionJwt), refreshToken: Token(jwt: refreshJwt), externalToken: externalToken, user: user.convert(), isFirstAuthentication: firstSeen ?? false, flowOutput: flowOutput ?? [:])
7777
}
7878
}
7979

src/sdk/SDK.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ public extension DescopeSDK {
147147
static let name = "DescopeKit"
148148

149149
/// The Descope SDK version
150-
static let version = "0.11.1"
150+
static let version = "0.11.2"
151151
}
152152

153153
// Internal

src/types/Responses.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,13 @@ import UIKit
55
#endif
66

77
/// Returned from user authentication calls.
8-
public struct AuthenticationResponse: Sendable {
8+
public struct AuthenticationResponse: @unchecked Sendable {
99
public var sessionToken: DescopeToken
1010
public var refreshToken: DescopeToken
11+
public var externalToken: String?
1112
public var user: DescopeUser
1213
public var isFirstAuthentication: Bool
13-
public var externalToken: String?
14+
public var flowOutput: [String: Any]
1415
}
1516

1617
/// Returned from the ``DescopeAuth/refreshSession(refreshJwt:)`` call.

test/http/JWTResponse.swift

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,32 @@ class TestJWTResponse: XCTestCase {
4848
XCTAssertNil(authResponse.externalToken)
4949
}
5050

51+
func testFlowOutput() async throws {
52+
// with flow output
53+
var data = Data(flowOutputPayload.utf8)
54+
var jwtResponse = try JSONDecoder().decode(DescopeClient.JWTResponse.self, from: data)
55+
try jwtResponse.setValues(from: data, cookies: [], refreshCookieName: nil)
56+
var authResponse: AuthenticationResponse = try jwtResponse.convert()
57+
XCTAssertEqual("value", authResponse.flowOutput["key"] as? String)
58+
XCTAssertEqual(3, authResponse.flowOutput["count"] as? Int)
59+
60+
// survives a Codable round-trip, serialized as a JSON string like customAttributes
61+
let encoded = try JSONEncoder().encode(authResponse)
62+
let object = try JSONSerialization.jsonObject(with: encoded) as? [String: Any]
63+
XCTAssertTrue(object?["flowOutput"] is String)
64+
let decoded = try JSONDecoder().decode(AuthenticationResponse.self, from: encoded)
65+
XCTAssertEqual("value", decoded.flowOutput["key"] as? String)
66+
XCTAssertEqual(3, decoded.flowOutput["count"] as? Int)
67+
XCTAssertEqual(true, (decoded.flowOutput["nested"] as? [String: Any])?["inner"] as? Bool)
68+
69+
// no flow output
70+
data = Data(noExternalTokenPayload.utf8)
71+
jwtResponse = try JSONDecoder().decode(DescopeClient.JWTResponse.self, from: data)
72+
try jwtResponse.setValues(from: data, cookies: [], refreshCookieName: nil)
73+
authResponse = try jwtResponse.convert()
74+
XCTAssertTrue(authResponse.flowOutput.isEmpty)
75+
}
76+
5177
func testPageCookie() async throws {
5278
let data = Data(authPayload.utf8)
5379

@@ -118,6 +144,20 @@ private let externalTokenPayload = """
118144
}
119145
"""
120146

147+
private let flowOutputPayload = """
148+
{
149+
"sessionJwt": "\(sessionJwt)",
150+
"refreshJwt": "\(refreshJwt)",
151+
"user": \(userPayload),
152+
"firstSeen": true,
153+
"flowOutput": {
154+
"key": "value",
155+
"count": 3,
156+
"nested": { "inner": true }
157+
}
158+
}
159+
"""
160+
121161
private let noExternalTokenPayload = """
122162
{
123163
"sessionJwt": "\(sessionJwt)",

0 commit comments

Comments
 (0)