Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implementing support for handling binding update for OOB factors #156

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions Sources/OktaDirectAuth/DirectAuthFlow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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.
///
Expand All @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)):
Expand Down
7 changes: 6 additions & 1 deletion Sources/OktaDirectAuth/Extensions/ErrorExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -46,7 +50,8 @@ extension DirectAuthenticationFlow.PrimaryFactor: AuthenticationFactor {
loginHint: loginHint,
mfaToken: currentStatus?.mfaToken,
channel: channel,
factor: factor)
factor: factor,
bindingContext: bindingContext)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -35,7 +39,8 @@ extension DirectAuthenticationFlow.SecondaryFactor: AuthenticationFactor {
loginHint: loginHint,
mfaToken: currentStatus?.mfaToken,
channel: channel,
factor: factor)
factor: factor,
bindingContext: bindingContext)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -62,7 +63,8 @@ struct ChallengeRequest {
expiresIn: expiresIn,
interval: interval,
channel: channel,
bindingMethod: bindingMethod)
bindingMethod: bindingMethod,
bindingCode: bindingCode)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -51,6 +61,7 @@ struct OOBAuthenticateRequest {

enum BindingMethod: String, Codable {
case none
case transfer
}

extension OOBAuthenticateRequest: APIRequest, APIRequestBody {
Expand Down
123 changes: 73 additions & 50 deletions Sources/OktaDirectAuth/Internal/Step Handlers/OOBStepHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,76 +20,49 @@ class OOBStepHandler<Factor: AuthenticationFactor>: StepHandler {
let mfaToken: String?
let channel: DirectAuthenticationFlow.OOBChannel
let factor: Factor
private let bindingContext: DirectAuthenticationFlow.BindingUpdateContext?
private var poll: PollingHandler<TokenRequest>?

init(flow: DirectAuthenticationFlow,
openIdConfiguration: OpenIdConfiguration,
loginHint: String?,
mfaToken: String?,
channel: DirectAuthenticationFlow.OOBChannel,
factor: Factor) throws
factor: Factor,
bindingContext: DirectAuthenticationFlow.BindingUpdateContext? = nil) throws
{
self.flow = flow
self.openIdConfiguration = openIdConfiguration
self.loginHint = loginHint
self.mfaToken = mfaToken
self.channel = channel
self.factor = factor
self.bindingContext = bindingContext
}

func process(completion: @escaping (Result<DirectAuthenticationFlow.Status, DirectAuthenticationFlowError>) -> 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
}
}
}
Expand Down Expand Up @@ -160,4 +133,54 @@ class OOBStepHandler<Factor: AuthenticationFactor>: StepHandler {
completion(.failure(.validation(error: error)))
}
}

private func requestToken(using response: OOBResponse, completion: @escaping (Result<DirectAuthenticationFlow.Status, DirectAuthenticationFlowError>) -> 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
}
}
}
Original file line number Diff line number Diff line change
@@ -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";
6 changes: 6 additions & 0 deletions Tests/OktaDirectAuthTests/DirectAuth1FATests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
}

Expand All @@ -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")
}
}

Expand All @@ -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
Expand Down
4 changes: 4 additions & 0 deletions Tests/OktaDirectAuthTests/DirectAuth2FATests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
Loading
Loading