From d0752b40906a91b4756b4aaaa4b0804e2f8eda56 Mon Sep 17 00:00:00 2001 From: Santhosh Ramaraju Date: Wed, 15 Nov 2023 16:51:37 -0600 Subject: [PATCH] Implementing support for handling binding update for OOB factors --- Sources/OktaDirectAuth/DirectAuthFlow.swift | 17 ++ ...enticationFlowError+PublicExtensions.swift | 1 + .../Extensions/ErrorExtensions.swift | 7 +- .../PrimaryFactor.swift | 7 +- .../SecondaryFactor.swift | 7 +- .../Internal/Requests/ChallengeRequest.swift | 4 +- .../Requests/OOBAuthenticateRequest.swift | 11 ++ .../Step Handlers/OOBStepHandler.swift | 123 +++++++----- .../Resources/en.lproj/OktaDirectAuth.strings | 1 + .../DirectAuth1FATests.swift | 6 + .../DirectAuth2FATests.swift | 4 + .../FactorStepHandlerTests.swift | 177 +++++++++++++++++- ...enge-oob-binding-transfer-missingCode.json | 8 + .../challenge-oob-binding-transfer.json | 9 + ...enticate-binding-transfer-missingCode.json | 7 + .../oob-authenticate-binding-transfer.json | 8 + 16 files changed, 337 insertions(+), 60 deletions(-) create mode 100644 Tests/OktaDirectAuthTests/MockResponses/challenge-oob-binding-transfer-missingCode.json create mode 100644 Tests/OktaDirectAuthTests/MockResponses/challenge-oob-binding-transfer.json create mode 100644 Tests/OktaDirectAuthTests/MockResponses/oob-authenticate-binding-transfer-missingCode.json create mode 100644 Tests/OktaDirectAuthTests/MockResponses/oob-authenticate-binding-transfer.json diff --git a/Sources/OktaDirectAuth/DirectAuthFlow.swift b/Sources/OktaDirectAuth/DirectAuthFlow.swift index 2649526c1..ab268be49 100644 --- a/Sources/OktaDirectAuth/DirectAuthFlow.swift +++ b/Sources/OktaDirectAuth/DirectAuthFlow.swift @@ -26,6 +26,7 @@ public protocol DirectAuthenticationFlowDelegate: AuthenticationDelegate { /// Errors that may be generated while authenticating using ``DirectAuthenticationFlow``. public enum DirectAuthenticationFlowError: Error { case pollingTimeoutExceeded + case bindingCodeMissing case missingArguments(_ names: [String]) case network(error: APIClientError) case oauth2(error: OAuth2Error) @@ -111,6 +112,19 @@ public class DirectAuthenticationFlow: AuthenticationFlow { self.mfaToken = mfaToken } } + + /// Represents the different types of binding updates that can be received + public enum BindingUpdateType { + /// Binding requires transfer of a code from one channel to another + case transfer(_ code: String) + } + + /// Holds information about the binding update received when verifying OOB factors + public struct BindingUpdateContext { + /// Holds the type of binding update received from the server + public let update: BindingUpdateType + let oobResponse: OOBResponse + } /// The current status of the authentication flow. /// @@ -119,6 +133,9 @@ public class DirectAuthenticationFlow: AuthenticationFlow { /// Authentication was successful, returning the given token. case success(_ token: Token) + /// Indicates that there is an update about binding authentication channels when verifying OOB factors + case bindingUpdate(_ context: BindingUpdateContext) + /// Indicates the user should be challenged with some other secondary factor. /// /// When this status is returned, the developer should use the ``DirectAuthenticationFlow/resume(_:with:)`` function to supply a secondary factor to verify the user. diff --git a/Sources/OktaDirectAuth/Extensions/DirectAuthenticationFlowError+PublicExtensions.swift b/Sources/OktaDirectAuth/Extensions/DirectAuthenticationFlowError+PublicExtensions.swift index e4b1c1292..3f96162d9 100644 --- a/Sources/OktaDirectAuth/Extensions/DirectAuthenticationFlowError+PublicExtensions.swift +++ b/Sources/OktaDirectAuth/Extensions/DirectAuthenticationFlowError+PublicExtensions.swift @@ -21,6 +21,7 @@ extension DirectAuthenticationFlowError: Equatable { public static func == (lhs: DirectAuthenticationFlowError, rhs: DirectAuthenticationFlowError) -> Bool { switch (lhs, rhs) { case (.pollingTimeoutExceeded, .pollingTimeoutExceeded): return true + case (.bindingCodeMissing, .bindingCodeMissing): return true case (.missingArguments(let lhsNames), .missingArguments(let rhsNames)): return lhsNames.sorted() == rhsNames.sorted() case (.network(error: let lhsError), .network(error: let rhsError)): diff --git a/Sources/OktaDirectAuth/Extensions/ErrorExtensions.swift b/Sources/OktaDirectAuth/Extensions/ErrorExtensions.swift index e4d05fced..bbfea3cf4 100644 --- a/Sources/OktaDirectAuth/Extensions/ErrorExtensions.swift +++ b/Sources/OktaDirectAuth/Extensions/ErrorExtensions.swift @@ -17,9 +17,14 @@ extension DirectAuthenticationFlowError: LocalizedError { switch self { case .pollingTimeoutExceeded: return NSLocalizedString("polling_timeout_exceeded", - tableName: "OktaOAuth2", + tableName: "OktaDirectAuth", bundle: .oktaDirectAuth, comment: "Polling timeout exceeded") + case .bindingCodeMissing: + return NSLocalizedString("binding_code_missing", + tableName: "OktaDirectAuth", + bundle: .oktaDirectAuth, + comment: "Binding Code is missing") case .missingArguments(let arguments): return String.localizedStringWithFormat( NSLocalizedString("missing_arguments", diff --git a/Sources/OktaDirectAuth/Internal/Authentication Factors/PrimaryFactor.swift b/Sources/OktaDirectAuth/Internal/Authentication Factors/PrimaryFactor.swift index 8eb1f809f..98934c1d9 100644 --- a/Sources/OktaDirectAuth/Internal/Authentication Factors/PrimaryFactor.swift +++ b/Sources/OktaDirectAuth/Internal/Authentication Factors/PrimaryFactor.swift @@ -31,6 +31,10 @@ extension DirectAuthenticationFlow.PrimaryFactor: AuthenticationFactor { currentStatus: DirectAuthenticationFlow.Status? = nil, factor: DirectAuthenticationFlow.PrimaryFactor) throws -> StepHandler { + var bindingContext: DirectAuthenticationFlow.BindingUpdateContext? + if case .bindingUpdate(let context) = currentStatus { + bindingContext = context + } switch self { case .otp: fallthrough case .password: @@ -46,7 +50,8 @@ extension DirectAuthenticationFlow.PrimaryFactor: AuthenticationFactor { loginHint: loginHint, mfaToken: currentStatus?.mfaToken, channel: channel, - factor: factor) + factor: factor, + bindingContext: bindingContext) } } diff --git a/Sources/OktaDirectAuth/Internal/Authentication Factors/SecondaryFactor.swift b/Sources/OktaDirectAuth/Internal/Authentication Factors/SecondaryFactor.swift index 90c1867a1..575e41490 100644 --- a/Sources/OktaDirectAuth/Internal/Authentication Factors/SecondaryFactor.swift +++ b/Sources/OktaDirectAuth/Internal/Authentication Factors/SecondaryFactor.swift @@ -20,6 +20,10 @@ extension DirectAuthenticationFlow.SecondaryFactor: AuthenticationFactor { currentStatus: DirectAuthenticationFlow.Status?, factor: DirectAuthenticationFlow.SecondaryFactor) throws -> StepHandler { + var bindingContext: DirectAuthenticationFlow.BindingUpdateContext? + if case .bindingUpdate(let context) = currentStatus { + bindingContext = context + } switch self { case .otp: let request = TokenRequest(openIdConfiguration: openIdConfiguration, @@ -35,7 +39,8 @@ extension DirectAuthenticationFlow.SecondaryFactor: AuthenticationFactor { loginHint: loginHint, mfaToken: currentStatus?.mfaToken, channel: channel, - factor: factor) + factor: factor, + bindingContext: bindingContext) } } diff --git a/Sources/OktaDirectAuth/Internal/Requests/ChallengeRequest.swift b/Sources/OktaDirectAuth/Internal/Requests/ChallengeRequest.swift index 8b9a28b07..fb8ae0e29 100644 --- a/Sources/OktaDirectAuth/Internal/Requests/ChallengeRequest.swift +++ b/Sources/OktaDirectAuth/Internal/Requests/ChallengeRequest.swift @@ -47,6 +47,7 @@ struct ChallengeRequest { let interval: TimeInterval? let channel: DirectAuthenticationFlow.OOBChannel? let bindingMethod: BindingMethod? + let bindingCode: String? var oobResponse: OOBResponse? { guard let oobCode = oobCode, @@ -62,7 +63,8 @@ struct ChallengeRequest { expiresIn: expiresIn, interval: interval, channel: channel, - bindingMethod: bindingMethod) + bindingMethod: bindingMethod, + bindingCode: bindingCode) } } } diff --git a/Sources/OktaDirectAuth/Internal/Requests/OOBAuthenticateRequest.swift b/Sources/OktaDirectAuth/Internal/Requests/OOBAuthenticateRequest.swift index 1c1153464..cbea63bea 100644 --- a/Sources/OktaDirectAuth/Internal/Requests/OOBAuthenticateRequest.swift +++ b/Sources/OktaDirectAuth/Internal/Requests/OOBAuthenticateRequest.swift @@ -25,6 +25,16 @@ struct OOBResponse: Codable { let interval: TimeInterval let channel: DirectAuthenticationFlow.OOBChannel let bindingMethod: BindingMethod + let bindingCode: String? + + init(oobCode: String, expiresIn: TimeInterval, interval: TimeInterval, channel: DirectAuthenticationFlow.OOBChannel, bindingMethod: BindingMethod, bindingCode: String? = nil) { + self.oobCode = oobCode + self.expiresIn = expiresIn + self.interval = interval + self.channel = channel + self.bindingMethod = bindingMethod + self.bindingCode = bindingCode + } } struct OOBAuthenticateRequest { @@ -51,6 +61,7 @@ struct OOBAuthenticateRequest { enum BindingMethod: String, Codable { case none + case transfer } extension OOBAuthenticateRequest: APIRequest, APIRequestBody { diff --git a/Sources/OktaDirectAuth/Internal/Step Handlers/OOBStepHandler.swift b/Sources/OktaDirectAuth/Internal/Step Handlers/OOBStepHandler.swift index 746890bd7..f2b57f8f3 100644 --- a/Sources/OktaDirectAuth/Internal/Step Handlers/OOBStepHandler.swift +++ b/Sources/OktaDirectAuth/Internal/Step Handlers/OOBStepHandler.swift @@ -20,6 +20,7 @@ class OOBStepHandler: StepHandler { let mfaToken: String? let channel: DirectAuthenticationFlow.OOBChannel let factor: Factor + private let bindingContext: DirectAuthenticationFlow.BindingUpdateContext? private var poll: PollingHandler? init(flow: DirectAuthenticationFlow, @@ -27,7 +28,8 @@ class OOBStepHandler: StepHandler { loginHint: String?, mfaToken: String?, channel: DirectAuthenticationFlow.OOBChannel, - factor: Factor) throws + factor: Factor, + bindingContext: DirectAuthenticationFlow.BindingUpdateContext? = nil) throws { self.flow = flow self.openIdConfiguration = openIdConfiguration @@ -35,61 +37,32 @@ class OOBStepHandler: StepHandler { self.mfaToken = mfaToken self.channel = channel self.factor = factor + self.bindingContext = bindingContext } func process(completion: @escaping (Result) -> Void) { - requestOOBCode { result in - switch result { - case .failure(let error): - self.flow.process(error, completion: completion) - - case .success(let response): - let request = TokenRequest(openIdConfiguration: self.openIdConfiguration, - clientConfiguration: self.flow.client.configuration, - factor: self.factor, - mfaToken: self.mfaToken, - oobCode: response.oobCode, - grantTypesSupported: self.flow.supportedGrantTypes) - self.poll = PollingHandler(client: self.flow.client, - request: request, - expiresIn: response.expiresIn, - interval: response.interval) { pollHandler, result in - switch result { - case .success(let response): - return .success(response.result) - case .failure(let error): - guard case let .serverError(serverError) = error, - let oauthError = serverError as? OAuth2ServerError - else { - return .failure(error) - } - - switch oauthError.code { - case .slowDown: - pollHandler.interval += 5 - fallthrough - case .authorizationPending: fallthrough - case .directAuthAuthorizationPending: - return .continuePolling - default: - return .failure(error) - } - } + if let bindingContext { + self.requestToken(using: bindingContext.oobResponse, completion: completion) + } else { + requestOOBCode { [weak self] result in + guard let self else { + return } - - self.poll?.start { result in - switch result { - case .success(let token): - completion(.success(.success(token))) - case .failure(let error): - switch error { - case .apiClientError(let error): - self.flow.process(error, completion: completion) - case .timeout: - completion(.failure(.pollingTimeoutExceeded)) + switch result { + case .failure(let error): + self.flow.process(error, completion: completion) + case .success(let response): + switch response.bindingMethod { + case .none: + self.requestToken(using: response, completion: completion) + case .transfer: + guard let bindingCode = response.bindingCode, bindingCode.isEmpty == false else { + completion(.failure(.bindingCodeMissing)) + return } + let context = DirectAuthenticationFlow.BindingUpdateContext(update: .transfer(bindingCode), oobResponse: response) + completion(.success(.bindingUpdate(context))) } - self.poll = nil } } } @@ -160,4 +133,54 @@ class OOBStepHandler: StepHandler { completion(.failure(.validation(error: error))) } } + + private func requestToken(using response: OOBResponse, completion: @escaping (Result) -> Void) { + let request = TokenRequest(openIdConfiguration: self.openIdConfiguration, + clientConfiguration: self.flow.client.configuration, + factor: self.factor, + mfaToken: self.mfaToken, + oobCode: response.oobCode, + grantTypesSupported: self.flow.supportedGrantTypes) + self.poll = PollingHandler(client: self.flow.client, + request: request, + expiresIn: response.expiresIn, + interval: response.interval) { pollHandler, result in + switch result { + case .success(let response): + return .success(response.result) + case .failure(let error): + guard case let .serverError(serverError) = error, + let oauthError = serverError as? OAuth2ServerError + else { + return .failure(error) + } + + switch oauthError.code { + case .slowDown: + pollHandler.interval += 5 + fallthrough + case .authorizationPending: fallthrough + case .directAuthAuthorizationPending: + return .continuePolling + default: + return .failure(error) + } + } + } + + self.poll?.start { result in + switch result { + case .success(let token): + completion(.success(.success(token))) + case .failure(let error): + switch error { + case .apiClientError(let apiClientError): + self.flow.process(apiClientError, completion: completion) + case .timeout: + completion(.failure(.pollingTimeoutExceeded)) + } + } + self.poll = nil + } + } } diff --git a/Sources/OktaDirectAuth/Resources/en.lproj/OktaDirectAuth.strings b/Sources/OktaDirectAuth/Resources/en.lproj/OktaDirectAuth.strings index 67daf9510..8ad067de6 100644 --- a/Sources/OktaDirectAuth/Resources/en.lproj/OktaDirectAuth.strings +++ b/Sources/OktaDirectAuth/Resources/en.lproj/OktaDirectAuth.strings @@ -1,3 +1,4 @@ /* DirectAuthenticationFlowError */ "polling_timeout_exceeded" = "Authentication timed out while polling the server."; "missing_arguments" = "Could not authenticate since some expected arguments were missing. [%@]"; +"binding_code_missing" = "Did not receive a binding code from server"; diff --git a/Tests/OktaDirectAuthTests/DirectAuth1FATests.swift b/Tests/OktaDirectAuthTests/DirectAuth1FATests.swift index fed160e93..b68af549d 100644 --- a/Tests/OktaDirectAuthTests/DirectAuth1FATests.swift +++ b/Tests/OktaDirectAuthTests/DirectAuth1FATests.swift @@ -57,6 +57,8 @@ final class DirectAuth1FATests: XCTestCase { XCTAssertNotNil(token.refreshToken) case .mfaRequired(_): XCTFail("Not expecting MFA Required") + case .bindingUpdate(_): + XCTFail("Not expecting binding update") } } @@ -75,6 +77,8 @@ final class DirectAuth1FATests: XCTestCase { XCTAssertNotNil(token.refreshToken) case .mfaRequired(_): XCTFail("Not expecting MFA Required") + case .bindingUpdate(_): + XCTFail("Not expecting binding update") } } @@ -93,6 +97,8 @@ final class DirectAuth1FATests: XCTestCase { XCTAssertNotNil(token.refreshToken) case .mfaRequired(_): XCTFail("Not expecting MFA Required") + case .bindingUpdate(_): + XCTFail("Not expecting binding update") } } #endif diff --git a/Tests/OktaDirectAuthTests/DirectAuth2FATests.swift b/Tests/OktaDirectAuthTests/DirectAuth2FATests.swift index 034ca5356..590308874 100644 --- a/Tests/OktaDirectAuthTests/DirectAuth2FATests.swift +++ b/Tests/OktaDirectAuthTests/DirectAuth2FATests.swift @@ -64,7 +64,11 @@ final class DirectAuth2FATests: XCTestCase { break case .mfaRequired(_): XCTFail("Not expecting MFA Required") + case .bindingUpdate(_): + XCTFail("Not expecting binding update") } + case .bindingUpdate(_): + XCTFail("Not expecting binding update") } XCTAssertFalse(flow.isAuthenticating) } diff --git a/Tests/OktaDirectAuthTests/FactorStepHandlerTests.swift b/Tests/OktaDirectAuthTests/FactorStepHandlerTests.swift index 75a6f0f3d..bcb596f30 100644 --- a/Tests/OktaDirectAuthTests/FactorStepHandlerTests.swift +++ b/Tests/OktaDirectAuthTests/FactorStepHandlerTests.swift @@ -160,7 +160,7 @@ final class FactorStepHandlerTests: XCTestCase { case .success(let status): switch status { case .success(_): break - case .mfaRequired(_): + case .mfaRequired(_), .bindingUpdate(_): XCTFail("Did not receive a success response") } case .failure(let error): @@ -193,7 +193,7 @@ final class FactorStepHandlerTests: XCTestCase { switch result { case .success(let status): switch status { - case .success(_): + case .success(_), .bindingUpdate(_): XCTFail("Did not receive a mfa_required response") case .mfaRequired(let context): XCTAssertEqual(context.mfaToken, "abcd1234") @@ -232,7 +232,7 @@ final class FactorStepHandlerTests: XCTestCase { case .success(let status): switch status { case .success(_): break - case .mfaRequired(_): + case .mfaRequired(_), .bindingUpdate(_): XCTFail("Did not receive a success response") } case .failure(let error): @@ -242,6 +242,81 @@ final class FactorStepHandlerTests: XCTestCase { } waitForExpectations(timeout: 5) } + + func testPrimaryOOBBindingTransferSuccess() throws { + urlSession.expect("https://example.okta.com/.well-known/openid-configuration", + data: try data(from: .module, for: "openid-configuration", in: "MockResponses"), + contentType: "application/json") + urlSession.expect("https://example.okta.com/oauth2/v1/keys?client_id=clientId", + data: try data(from: .module, for: "keys", in: "MockResponses"), + contentType: "application/json") + urlSession.expect("https://example.okta.com/oauth2/v1/oob-authenticate", + data: try data(from: .module, for: "oob-authenticate-binding-transfer", in: "MockResponses")) + urlSession.expect("https://example.okta.com/oauth2/v1/token", + data: try data(from: .module, for: "token", in: "MockResponses")) + + let factor = PrimaryFactor.oob(channel: .push) + let handler = try factor.stepHandler(flow: flow, + openIdConfiguration: openIdConfiguration, + loginHint: "jane.doe@example.com", + factor: factor) + + let wait = expectation(description: "process") + handler.process { result in + guard case .success(let status) = result, + case .bindingUpdate(let context) = status else { + XCTFail("Did not receive binding update in result: \(result)") + return + } + switch context.update { + case .transfer(let code): + XCTAssertEqual(code, "12") + do { + let resumeHandler = try factor.stepHandler(flow: self.flow, + openIdConfiguration: self.openIdConfiguration, + currentStatus: status, + factor: factor) + self.assertGettingTokenAfterBindingTransfer(using: resumeHandler) + } catch { + XCTFail("Did not expect error creating step handler: \(error)") + } + } + XCTAssertEqual(context.oobResponse.oobCode, "1c266114-a1be-4252-8ad1-04986c5b9ac1") + wait.fulfill() + } + waitForExpectations(timeout: 5) + } + + func testPrimaryOOBBindingTransferFail() throws { + urlSession.expect("https://example.okta.com/.well-known/openid-configuration", + data: try data(from: .module, for: "openid-configuration", in: "MockResponses"), + contentType: "application/json") + urlSession.expect("https://example.okta.com/oauth2/v1/keys?client_id=clientId", + data: try data(from: .module, for: "keys", in: "MockResponses"), + contentType: "application/json") + urlSession.expect("https://example.okta.com/oauth2/v1/oob-authenticate", + data: try data(from: .module, for: "oob-authenticate-binding-transfer-missingCode", in: "MockResponses")) + urlSession.expect("https://example.okta.com/oauth2/v1/token", + data: try data(from: .module, for: "token", in: "MockResponses")) + + let factor = PrimaryFactor.oob(channel: .push) + let handler = try factor.stepHandler(flow: flow, + openIdConfiguration: openIdConfiguration, + loginHint: "jane.doe@example.com", + factor: factor) + + let wait = expectation(description: "process") + handler.process { result in + switch result { + case .success(_): + XCTFail("Not expecting success") + case .failure(let error): + XCTAssertEqual(error, .bindingCodeMissing) + } + wait.fulfill() + } + waitForExpectations(timeout: 5) + } func testPrimaryOOBMFARequired() throws { urlSession.expect("https://example.okta.com/.well-known/openid-configuration", @@ -267,7 +342,7 @@ final class FactorStepHandlerTests: XCTestCase { switch result { case .success(let status): switch status { - case .success(_): + case .success(_), .bindingUpdate(_): XCTFail("Did not receive a mfa_required response") case .mfaRequired(let context): XCTAssertEqual(context.mfaToken, "abcd1234") @@ -293,7 +368,7 @@ final class FactorStepHandlerTests: XCTestCase { urlSession.expect("https://example.okta.com/oauth2/v1/token", data: try data(from: .module, for: "token", in: "MockResponses")) - let factor = PrimaryFactor.oob(channel: .push) + let factor = SecondaryFactor.oob(channel: .push) let handler = try factor.stepHandler(flow: flow, openIdConfiguration: openIdConfiguration, currentStatus: .mfaRequired(.init(supportedChallengeTypes: nil, @@ -306,7 +381,7 @@ final class FactorStepHandlerTests: XCTestCase { case .success(let status): switch status { case .success(_): break - case .mfaRequired(_): + case .mfaRequired(_), .bindingUpdate(_): XCTFail("Did not receive a success response") } case .failure(let error): @@ -316,4 +391,94 @@ final class FactorStepHandlerTests: XCTestCase { } waitForExpectations(timeout: 5) } + + func testSecondaryOOBBindingTransferSuccess() throws { + urlSession.expect("https://example.okta.com/.well-known/openid-configuration", + data: try data(from: .module, for: "openid-configuration", in: "MockResponses"), + contentType: "application/json") + urlSession.expect("https://example.okta.com/oauth2/v1/keys?client_id=clientId", + data: try data(from: .module, for: "keys", in: "MockResponses"), + contentType: "application/json") + urlSession.expect("https://example.okta.com/oauth2/v1/challenge", + data: try data(from: .module, for: "challenge-oob-binding-transfer", in: "MockResponses")) + urlSession.expect("https://example.okta.com/oauth2/v1/token", + data: try data(from: .module, for: "token", in: "MockResponses")) + + let factor = SecondaryFactor.oob(channel: .push) + let handler = try factor.stepHandler(flow: flow, + openIdConfiguration: openIdConfiguration, + currentStatus: .mfaRequired(.init(supportedChallengeTypes: nil, + mfaToken: "abcd1234")), + factor: factor) + + let wait = expectation(description: "process") + handler.process { result in + guard case .success(let status) = result, + case .bindingUpdate(let context) = status else { + XCTFail("Did not receive binding update in result: \(result)") + return + } + switch context.update { + case .transfer(let code): + XCTAssertEqual(code, "12") + do { + let resumeHandler = try factor.stepHandler(flow: self.flow, + openIdConfiguration: self.openIdConfiguration, + currentStatus: status, + factor: factor) + self.assertGettingTokenAfterBindingTransfer(using: resumeHandler) + } catch { + XCTFail("Did not expect error creating step handler: \(error)") + } + } + XCTAssertEqual(context.oobResponse.oobCode, "1c266114-a1be-4252-8ad1-04986c5b9ac1") + wait.fulfill() + } + waitForExpectations(timeout: 5) + } + + func testSecondaryOOBBindingTransferFail() throws { + urlSession.expect("https://example.okta.com/.well-known/openid-configuration", + data: try data(from: .module, for: "openid-configuration", in: "MockResponses"), + contentType: "application/json") + urlSession.expect("https://example.okta.com/oauth2/v1/keys?client_id=clientId", + data: try data(from: .module, for: "keys", in: "MockResponses"), + contentType: "application/json") + urlSession.expect("https://example.okta.com/oauth2/v1/challenge", + data: try data(from: .module, for: "challenge-oob-binding-transfer-missingCode", in: "MockResponses")) + urlSession.expect("https://example.okta.com/oauth2/v1/token", + data: try data(from: .module, for: "token", in: "MockResponses")) + + let factor = SecondaryFactor.oob(channel: .push) + let handler = try factor.stepHandler(flow: flow, + openIdConfiguration: openIdConfiguration, + currentStatus: .mfaRequired(.init(supportedChallengeTypes: nil, + mfaToken: "abcd1234")), + factor: factor) + + let wait = expectation(description: "process") + handler.process { result in + switch result { + case .success(_): + XCTFail("Not expecting success") + case .failure(let error): + XCTAssertEqual(error.errorDescription, DirectAuthenticationFlowError.bindingCodeMissing.errorDescription) + } + wait.fulfill() + } + waitForExpectations(timeout: 5) + } + + private func assertGettingTokenAfterBindingTransfer(using handler: StepHandler) { + let tokenExpectation = expectation(description: "get token") + handler.process { result in + guard case .success(let status) = result, + case .success(_) = status else { + XCTFail("Did not receive token") + return + } + tokenExpectation.fulfill() + } + wait(for: [tokenExpectation], timeout: 2.0) + } } diff --git a/Tests/OktaDirectAuthTests/MockResponses/challenge-oob-binding-transfer-missingCode.json b/Tests/OktaDirectAuthTests/MockResponses/challenge-oob-binding-transfer-missingCode.json new file mode 100644 index 000000000..2d026cabf --- /dev/null +++ b/Tests/OktaDirectAuthTests/MockResponses/challenge-oob-binding-transfer-missingCode.json @@ -0,0 +1,8 @@ +{ + "challenge_type": "http://auth0.com/oauth/grant-type/mfa-oob", + "oob_code": "1c266114-a1be-4252-8ad1-04986c5b9ac1", + "expires_in": 120, + "interval": 1, + "channel": "push", + "binding_method": "transfer" +} diff --git a/Tests/OktaDirectAuthTests/MockResponses/challenge-oob-binding-transfer.json b/Tests/OktaDirectAuthTests/MockResponses/challenge-oob-binding-transfer.json new file mode 100644 index 000000000..746e4d12e --- /dev/null +++ b/Tests/OktaDirectAuthTests/MockResponses/challenge-oob-binding-transfer.json @@ -0,0 +1,9 @@ +{ + "challenge_type": "http://auth0.com/oauth/grant-type/mfa-oob", + "oob_code": "1c266114-a1be-4252-8ad1-04986c5b9ac1", + "expires_in": 120, + "interval": 1, + "channel": "push", + "binding_method": "transfer", + "binding_code": "12" +} diff --git a/Tests/OktaDirectAuthTests/MockResponses/oob-authenticate-binding-transfer-missingCode.json b/Tests/OktaDirectAuthTests/MockResponses/oob-authenticate-binding-transfer-missingCode.json new file mode 100644 index 000000000..fabb1cecc --- /dev/null +++ b/Tests/OktaDirectAuthTests/MockResponses/oob-authenticate-binding-transfer-missingCode.json @@ -0,0 +1,7 @@ +{ + "oob_code": "1c266114-a1be-4252-8ad1-04986c5b9ac1", + "expires_in": 120, + "interval": 1, + "channel": "push", + "binding_method": "transfer" +} diff --git a/Tests/OktaDirectAuthTests/MockResponses/oob-authenticate-binding-transfer.json b/Tests/OktaDirectAuthTests/MockResponses/oob-authenticate-binding-transfer.json new file mode 100644 index 000000000..1d3e2c9d2 --- /dev/null +++ b/Tests/OktaDirectAuthTests/MockResponses/oob-authenticate-binding-transfer.json @@ -0,0 +1,8 @@ +{ + "oob_code": "1c266114-a1be-4252-8ad1-04986c5b9ac1", + "expires_in": 120, + "interval": 1, + "channel": "push", + "binding_method": "transfer", + "binding_code": "12" +}