diff --git a/OktaIdx.podspec b/OktaIdx.podspec index 14495493..eb0240bf 100644 --- a/OktaIdx.podspec +++ b/OktaIdx.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |spec| spec.name = 'OktaIdx' - spec.version = '3.0.4' + spec.version = '3.0.5' spec.summary = 'SDK to easily integrate the Okta Identity Engine' spec.description = <<-DESC Integrate your native app with Okta using the Okta Identity Engine library. @@ -24,5 +24,5 @@ Integrate your native app with Okta using the Okta Identity Engine library. spec.source_files = 'Sources/OktaIdx/**/*.swift' spec.swift_version = "5.5" - spec.dependency "OktaAuthFoundation", "1.1.4" + spec.dependency "OktaAuthFoundation", "~> 1.1" end diff --git a/Package.swift b/Package.swift index 85602443..acd4d4e9 100644 --- a/Package.swift +++ b/Package.swift @@ -18,7 +18,7 @@ var package = Package( dependencies: [ .package(name: "AuthFoundation", url: "https://github.com/okta/okta-mobile-swift", - from: "1.1.4") + from: "1.1.5") ], targets: [ .target(name: "OktaIdx", diff --git a/README.md b/README.md index 35df348f..5354c443 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ This library uses semantic versioning and follows Okta's [Library Version Policy | ------- | ---------------------------------- | | 1.0.0 | | | 2.0.1 | | -| 3.0.4 | ✔️ Stable | +| 3.0.5 | ✔️ Stable | The latest release can always be found on the [releases page][github-releases]. diff --git a/Samples/EmbeddedAuthWithSDKs/EmbeddedAuth/View Controllers/ProfileTableViewController.swift b/Samples/EmbeddedAuthWithSDKs/EmbeddedAuth/View Controllers/ProfileTableViewController.swift index 366ca7ad..dda6fc52 100644 --- a/Samples/EmbeddedAuthWithSDKs/EmbeddedAuth/View Controllers/ProfileTableViewController.swift +++ b/Samples/EmbeddedAuthWithSDKs/EmbeddedAuth/View Controllers/ProfileTableViewController.swift @@ -39,11 +39,21 @@ class ProfileTableViewController: UITableViewController { var credential: Credential? { didSet { if let credential = credential { - credential.refreshIfNeeded { _ in - credential.userInfo { _ in - DispatchQueue.main.async { - self.configure(credential) + DispatchQueue.main.async { + self.configure(credential) + } + + credential.refreshIfNeeded { result in + switch result { + case .success(): + credential.userInfo { _ in + DispatchQueue.main.async { + self.configure(credential) + } } + + case .failure(let error): + self.show(error: error) } } } @@ -78,6 +88,12 @@ class ProfileTableViewController: UITableViewController { dateFormatter.timeStyle = .long guard let info = credential.userInfo else { + tableContent = [ + .actions: [ + .init(kind: .destructive, id: "signout", title: "Sign Out") + ] + ] + tableView.reloadData() return } @@ -117,7 +133,7 @@ class ProfileTableViewController: UITableViewController { DispatchQueue.main.async { let alert = UIAlertController(title: nil, message: error.localizedDescription, preferredStyle: .alert) alert.addAction(.init(title: "OK", style: .default)) - self.show(alert, sender: nil) + self.present(alert, animated: true) } } @@ -151,7 +167,7 @@ class ProfileTableViewController: UITableViewController { func refresh() { guard let credential = Credential.default else { return } - credential.refreshIfNeeded { result in + credential.refresh { result in if case let .failure(error) = result { self.show(error: error) } diff --git a/Sources/OktaIdx/Extensions/InteractionCodeFlow+DeviceIdentifier.swift b/Sources/OktaIdx/Extensions/InteractionCodeFlow+DeviceIdentifier.swift index a3f7a5c6..e2bf2788 100644 --- a/Sources/OktaIdx/Extensions/InteractionCodeFlow+DeviceIdentifier.swift +++ b/Sources/OktaIdx/Extensions/InteractionCodeFlow+DeviceIdentifier.swift @@ -80,3 +80,12 @@ extension InteractionCodeFlow { return deviceToken } } + +extension InteractionCodeFlow.Option { + var includeInInteractRequest: Bool { + switch self { + case .omitDeviceToken: return false + default: return true + } + } +} diff --git a/Sources/OktaIdx/InteractionCodeFlow.swift b/Sources/OktaIdx/InteractionCodeFlow.swift index 5502b4a5..411b9965 100644 --- a/Sources/OktaIdx/InteractionCodeFlow.swift +++ b/Sources/OktaIdx/InteractionCodeFlow.swift @@ -26,6 +26,9 @@ public final class InteractionCodeFlow: AuthenticationFlow { /// Option used when a user is authenticating using a recovery token. case recoveryToken = "recovery_token" + + /// Option indicating whether or not a device identifier should be sent on API requests, which enables the device to be remembered. + case omitDeviceToken } /// The type used for the completion handler result from any method that returns an ``Response``. @@ -69,6 +72,11 @@ public final class InteractionCodeFlow: AuthenticationFlow { /// This value is used when resuming authentication at a later date or after app launch, and to ensure the final token exchange can be completed. public internal(set) var context: Context? + /// The options used when starting an authentication flow. + /// + /// This is updated when the ``start(options:completion:)`` (or ``start(options:)``) method is invoked, and is cleared when ``reset()`` is called. + public internal(set) var options: [Option: Any]? + /// Convenience initializer to construct an authentication flow from variables. /// - Parameters: /// - issuer: The issuer URL. @@ -133,7 +141,7 @@ public final class InteractionCodeFlow: AuthenticationFlow { /// - Parameters: /// - options: Options to include within the OAuth2 transaction. /// - completion: Completion block to be invoked when the session is started. - public func start(options: [Option: String]? = nil, + public func start(options: [Option: Any]? = nil, completion: @escaping ResponseResult) { if isAuthenticating { @@ -141,7 +149,7 @@ public final class InteractionCodeFlow: AuthenticationFlow { } // Ensure we have, at minimum, a state value - let state = options?[.state] ?? UUID().uuidString + let state: String = options?[.state] as? String ?? UUID().uuidString var options = options ?? [:] options[.state] = state @@ -151,6 +159,8 @@ public final class InteractionCodeFlow: AuthenticationFlow { } self.isAuthenticating = true + self.options = options + let request = InteractRequest(baseURL: client.baseURL, clientId: client.configuration.clientId, scope: client.configuration.scopes, @@ -309,6 +319,7 @@ public final class InteractionCodeFlow: AuthenticationFlow { public func reset() { context = nil isAuthenticating = false + options = nil // Remove any previous `idx` cookies so it won't leak into other sessions. let storage = client.session.configuration.httpCookieStorage ?? HTTPCookieStorage.shared @@ -346,7 +357,7 @@ extension InteractionCodeFlow { /// - configuration: Configuration describing the app settings to contact. /// - options: Options to include within the OAuth2 transaction. /// - Returns: A ``Response``. - public func start(options: [Option: String]? = nil) async throws -> Response { + public func start(options: [Option: Any]? = nil) async throws -> Response { try await withCheckedThrowingContinuation { continuation in start(options: options) { result in continuation.resume(with: result) @@ -387,7 +398,8 @@ extension InteractionCodeFlow: UsesDelegateCollection { extension InteractionCodeFlow: OAuth2ClientDelegate { public func api(client: APIClient, willSend request: inout URLRequest) { - guard let url = request.url, + guard options?[.omitDeviceToken] as? Bool ?? false == false, + let url = request.url, let deviceTokenCookie = deviceTokenCookie else { return diff --git a/Sources/OktaIdx/Internal/Implementations/Version1/Requests/InteractRequest.swift b/Sources/OktaIdx/Internal/Implementations/Version1/Requests/InteractRequest.swift index 69970859..edd4d237 100644 --- a/Sources/OktaIdx/Internal/Implementations/Version1/Requests/InteractRequest.swift +++ b/Sources/OktaIdx/Internal/Implementations/Version1/Requests/InteractRequest.swift @@ -19,14 +19,14 @@ extension InteractionCodeFlow { let clientId: String let scope: String let redirectUri: URL - let options: [InteractionCodeFlow.Option: String]? + let options: [InteractionCodeFlow.Option: Any]? let pkce: PKCE init(baseURL: URL, clientId: String, scope: String, redirectUri: URL, - options: [InteractionCodeFlow.Option: String]?, + options: [InteractionCodeFlow.Option: Any]?, pkce: PKCE) { url = baseURL.appendingPathComponent("v1/interact") @@ -59,10 +59,12 @@ extension InteractionCodeFlow.InteractRequest: APIRequest, APIRequestBody { "code_challenge_method": pkce.method.rawValue ] - options?.forEach { (key: InteractionCodeFlow.Option, value: String) in - result[key.rawValue] = value - } - + options?.filter { $0.key.includeInInteractRequest } + .compactMapValues { $0 as? String } + .forEach { (key: InteractionCodeFlow.Option, value: String) in + result[key.rawValue] = value + } + return result } } diff --git a/Sources/OktaIdx/Version.swift b/Sources/OktaIdx/Version.swift index 0a109246..a7718185 100644 --- a/Sources/OktaIdx/Version.swift +++ b/Sources/OktaIdx/Version.swift @@ -12,4 +12,4 @@ import Foundation -public let Version = SDKVersion(sdk: "okta-idx-swift", version: "3.0.4") +public let Version = SDKVersion(sdk: "okta-idx-swift", version: "3.0.5") diff --git a/Tests/OktaIdxTests/InteractionCodeFlowTests.swift b/Tests/OktaIdxTests/InteractionCodeFlowTests.swift index b2382d20..88118938 100644 --- a/Tests/OktaIdxTests/InteractionCodeFlowTests.swift +++ b/Tests/OktaIdxTests/InteractionCodeFlowTests.swift @@ -76,6 +76,40 @@ class InteractionCodeFlowTests: XCTestCase { XCTAssertEqual(delegate.calls.count, 1) XCTAssertEqual(delegate.calls.first?.type, .response) + + if InteractionCodeFlow.deviceIdentifier != nil { + let deviceToken = try XCTUnwrap(flow.deviceTokenCookie?.value) + XCTAssertEqual(urlSession.requests.first?.allHTTPHeaderFields?["Cookie"], + "DT=\(deviceToken)") + } + } + + func testStartWithOptions() throws { + urlSession.expect("https://example.com/oauth2/default/v1/interact", + data: try data(from: .module, + for: "interact-response")) + urlSession.expect("https://example.com/idp/idx/introspect", + data: try data(from: .module, + for: "introspect-response")) + + let wait = expectation(description: "start") + flow.start(options: [ + .omitDeviceToken: true, + .state: "CustomState" + ]) { result in + defer { wait.fulfill() } + + guard case let Result.success(response) = result else { + XCTFail("Received a failure when a success was expected") + return + } + + XCTAssertNotNil(response.remediations[.identify]) + } + waitForExpectations(timeout: 1.0) + + XCTAssertNil(urlSession.requests.first?.allHTTPHeaderFields?["Cookie"]) + XCTAssertEqual(flow.context?.state, "CustomState") } func testStartFailedInteract() throws {