Skip to content

Commit

Permalink
Handle MFA scenarios in WebAuthn properly
Browse files Browse the repository at this point in the history
  • Loading branch information
mikenachbaur-okta committed Feb 22, 2024
1 parent 77308f8 commit 1d2d6d8
Show file tree
Hide file tree
Showing 17 changed files with 200 additions and 113 deletions.
7 changes: 5 additions & 2 deletions Sources/AuthFoundation/Responses/GrantType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ public enum GrantType: Codable, Hashable {
case otpMFA
case oobMFA
case webAuthn
case webAuthnMFA
case other(_ type: String)
}

Expand All @@ -38,8 +39,8 @@ private let grantTypeMapping: [String: GrantType] = [
"urn:okta:params:oauth:grant-type:oob": .oob,
"http://auth0.com/oauth/grant-type/mfa-otp": .otpMFA,
"http://auth0.com/oauth/grant-type/mfa-oob": .oobMFA,
"urn:okta:params:oauth:grant-type:webauthn": .webAuthn

"urn:okta:params:oauth:grant-type:webauthn": .webAuthn,
"urn:okta:params:oauth:grant-type:mfa-webauthn": .webAuthnMFA,
]

extension GrantType: RawRepresentable {
Expand Down Expand Up @@ -79,6 +80,8 @@ extension GrantType: RawRepresentable {
return "http://auth0.com/oauth/grant-type/mfa-oob"
case .webAuthn:
return "urn:okta:params:oauth:grant-type:webauthn"
case .webAuthnMFA:
return "urn:okta:params:oauth:grant-type:mfa-webauthn"
}
}
}
10 changes: 9 additions & 1 deletion Sources/OktaDirectAuth/DirectAuthFlow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,14 @@ public class DirectAuthenticationFlow: AuthenticationFlow {
let oobResponse: OOBResponse
}

/// Holds information about a challenge request when initiating a WebAuthn authentication.
public struct WebAuthnContext {
/// The credential request returned from the server.
public let request: WebAuthn.CredentialRequestOptions

let mfaContext: MFAContext?
}

/// The current status of the authentication flow.
///
/// This value is returned from ``DirectAuthenticationFlow/start(_:with:)`` and ``DirectAuthenticationFlow/resume(_:with:)`` to indicate the result of an individual authentication step. This can be used to drive your application's sign-in workflow.
Expand All @@ -165,7 +173,7 @@ public class DirectAuthenticationFlow: AuthenticationFlow {
case mfaRequired(_ context: MFAContext)

/// Indicates the user is being prompted with a WebAuthn challenge request.
case webAuthn(request: WebAuthn.CredentialRequestOptions)
case webAuthn(_ context: WebAuthnContext)
}

/// The OAuth2Client this authentication flow will use.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,20 @@ import AuthFoundation
/// Defines the additional token parameters that can be introduced through input arguments.
protocol HasTokenParameters {
/// Parameters to include in the API request.
var tokenParameters: [String: Any]? { get }
func tokenParameters(currentStatus: DirectAuthenticationFlow.Status?) -> [String: String]
}

/// Defines the common properties and functions shared between factor types.
protocol AuthenticationFactor: HasTokenParameters {
/// The grant type supported by this factor.
var grantType: GrantType { get }
func grantType(currentStatus: DirectAuthenticationFlow.Status?) -> GrantType

/// Returns a step handler capable of handling this authentication factor.
/// - Parameters:
/// - flow: The current flow for this authentication step.
/// - openIdConfiguration: OpenID configuration for this org.
/// - loginHint: The login hint for this session.
/// - currentStatus: The current status this step is being created from, if applicable.
/// - factor: The factor for the step to process.
/// - Returns: A step handler capable of processing this authentication factor.
func stepHandler(flow: DirectAuthenticationFlow,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ extension DirectAuthenticationFlow.PrimaryFactor {

extension DirectAuthenticationFlow.PrimaryFactor: AuthenticationFactor {
func stepHandler(flow: DirectAuthenticationFlow,
openIdConfiguration: AuthFoundation.OpenIdConfiguration,
openIdConfiguration: OpenIdConfiguration,
loginHint: String? = nil,
currentStatus: DirectAuthenticationFlow.Status? = nil,
factor: DirectAuthenticationFlow.PrimaryFactor) throws -> StepHandler
Expand All @@ -36,6 +36,7 @@ extension DirectAuthenticationFlow.PrimaryFactor: AuthenticationFactor {
case .password:
let request = TokenRequest(openIdConfiguration: openIdConfiguration,
clientConfiguration: flow.client.configuration,
currentStatus: currentStatus,
loginHint: loginHint,
factor: factor,
grantTypesSupported: flow.supportedGrantTypes)
Expand All @@ -47,41 +48,42 @@ extension DirectAuthenticationFlow.PrimaryFactor: AuthenticationFactor {
}
return try OOBStepHandler(flow: flow,
openIdConfiguration: openIdConfiguration,
currentStatus: currentStatus,
loginHint: loginHint,
mfaToken: currentStatus?.mfaToken,
channel: channel,
factor: factor,
bindingContext: bindingContext)
case .webAuthn:
let mfaContext = currentStatus?.mfaContext
let request = try WebAuthnChallengeRequest(openIdConfiguration: openIdConfiguration,
clientConfiguration: flow.client.configuration,
loginHint: loginHint,
mfaToken: currentStatus?.mfaToken)
mfaToken: mfaContext?.mfaToken)
return ChallengeStepHandler(flow: flow, request: request) {
.webAuthn(request: $0)
.webAuthn(.init(request: $0,
mfaContext: mfaContext))
}
}
}

var tokenParameters: [String: Any]? {
func tokenParameters(currentStatus: DirectAuthenticationFlow.Status?) -> [String: String] {
var result: [String: String] = [
"grant_type": grantType(currentStatus: currentStatus).rawValue,
]

switch self {
case .otp(code: let code):
return [
"grant_type": grantType.rawValue,
"otp": code
]
result["otp"] = code
case .password(let password):
return [
"grant_type": grantType.rawValue,
"password": password
]
case .oob, .webAuthn:
return nil
result["password"] = password
case .oob: break
case .webAuthn: break
}


return result
}

var grantType: GrantType {
func grantType(currentStatus: DirectAuthenticationFlow.Status?) -> GrantType {
switch self {
case .otp:
return .otp
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,64 +28,73 @@ extension DirectAuthenticationFlow.SecondaryFactor: AuthenticationFactor {
case .otp:
let request = TokenRequest(openIdConfiguration: openIdConfiguration,
clientConfiguration: flow.client.configuration,
currentStatus: currentStatus,
loginHint: loginHint,
factor: factor,
mfaToken: currentStatus?.mfaToken,
grantTypesSupported: flow.supportedGrantTypes)
return TokenStepHandler(flow: flow, request: request)
case .oob(channel: let channel):
return try OOBStepHandler(flow: flow,
openIdConfiguration: openIdConfiguration,
currentStatus: currentStatus,
loginHint: loginHint,
mfaToken: currentStatus?.mfaToken,
channel: channel,
factor: factor,
bindingContext: bindingContext)
case .webAuthn:
let mfaContext = currentStatus?.mfaContext
let request = try WebAuthnChallengeRequest(openIdConfiguration: openIdConfiguration,
clientConfiguration: flow.client.configuration,
loginHint: loginHint,
mfaToken: currentStatus?.mfaToken)
mfaToken: mfaContext?.mfaToken)
return ChallengeStepHandler(flow: flow, request: request) {
.webAuthn(request: $0)
.webAuthn(.init(request: $0,
mfaContext: mfaContext))
}
case .webAuthnAssertion(let response):
let request = TokenRequest(openIdConfiguration: openIdConfiguration,
clientConfiguration: flow.client.configuration,
currentStatus: currentStatus,
loginHint: loginHint,
factor: factor,
mfaToken: currentStatus?.mfaToken,
parameters: response,
grantTypesSupported: flow.supportedGrantTypes)
return TokenStepHandler(flow: flow, request: request)
}
}

var tokenParameters: [String: Any]? {
func tokenParameters(currentStatus: DirectAuthenticationFlow.Status?) -> [String: String] {
var result: [String: String] = [
"grant_type": grantType(currentStatus: currentStatus).rawValue,
]

if let context = currentStatus?.mfaContext {
result["mfa_token"] = context.mfaToken
}

switch self {
case .otp(code: let code):
return [
"grant_type": grantType.rawValue,
"otp": code
]
case .oob, .webAuthn:
return nil
case .webAuthnAssertion(_):
return [
"grant_type": grantType.rawValue
]
result["otp"] = code
case .webAuthnAssertion(_): break
case .oob(channel: _): break
case .webAuthn: break
}

return result
}

var grantType: GrantType {
func grantType(currentStatus: DirectAuthenticationFlow.Status?) -> GrantType {
switch self {
case .otp:
return .otpMFA
case .oob:
return .oobMFA
case .webAuthn, .webAuthnAssertion(_):
return .webAuthn
if currentStatus?.mfaContext?.mfaToken != nil {
return .webAuthnMFA
} else {
return .webAuthn
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ struct OOBResponse: Codable, HasTokenParameters {
self.bindingCode = bindingCode
}

var tokenParameters: [String: Any]? {
func tokenParameters(currentStatus: DirectAuthenticationFlow.Status?) -> [String: String] {
["oob_code": oobCode]
}
}
Expand Down
24 changes: 7 additions & 17 deletions Sources/OktaDirectAuth/Internal/Requests/TokenRequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,25 +16,25 @@ import AuthFoundation
struct TokenRequest {
let openIdConfiguration: OpenIdConfiguration
let clientConfiguration: OAuth2Client.Configuration
let currentStatus: DirectAuthenticationFlow.Status?
let loginHint: String?
let factor: any AuthenticationFactor
let mfaToken: String?
let parameters: (any HasTokenParameters)?
let grantTypesSupported: [GrantType]?

init(openIdConfiguration: OpenIdConfiguration,
clientConfiguration: OAuth2Client.Configuration,
currentStatus: DirectAuthenticationFlow.Status?,
loginHint: String? = nil,
factor: any AuthenticationFactor,
mfaToken: String? = nil,
parameters: (any HasTokenParameters)? = nil,
grantTypesSupported: [GrantType]? = nil)
{
self.openIdConfiguration = openIdConfiguration
self.clientConfiguration = clientConfiguration
self.currentStatus = currentStatus
self.loginHint = loginHint
self.factor = factor
self.mfaToken = mfaToken
self.parameters = parameters
self.grantTypesSupported = grantTypesSupported
}
Expand All @@ -47,17 +47,11 @@ extension TokenRequest: OAuth2TokenRequest, OAuth2APIRequest, APIRequestBody {
var contentType: APIContentType? { .formEncoded }
var acceptsType: APIContentType? { .json }
var bodyParameters: [String: Any]? {
var result: [String: Any] = [
"client_id": clientConfiguration.clientId,
"grant_type": factor.grantType.rawValue,
"scope": clientConfiguration.scopes
]

if let mfaToken = mfaToken {
result["mfa_token"] = mfaToken
}
var result = factor.tokenParameters(currentStatus: currentStatus)
result["client_id"] = clientConfiguration.clientId
result["scope"] = clientConfiguration.scopes

if let tokenParameters = parameters?.tokenParameters {
if let tokenParameters = parameters?.tokenParameters(currentStatus: currentStatus) {
result.merge(tokenParameters, uniquingKeysWith: { $1 })
}

Expand All @@ -75,10 +69,6 @@ extension TokenRequest: OAuth2TokenRequest, OAuth2APIRequest, APIRequestBody {
result["grant_types_supported"] = grantTypesSupported.joined(separator: " ")
}

if let parameters = factor.tokenParameters {
result.merge(parameters, uniquingKeysWith: { $1 })
}

if let parameters = clientConfiguration.authentication.additionalParameters {
result.merge(parameters, uniquingKeysWith: { $1 })
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ extension WebAuthnChallengeRequest: APIRequest, APIRequestBody {
}

extension WebAuthn.AuthenticatorAssertionResponse: HasTokenParameters {
var tokenParameters: [String: Any]? {
func tokenParameters(currentStatus: DirectAuthenticationFlow.Status?) -> [String: String] {
var result = [
"clientDataJSON": clientDataJSON,
"authenticatorData": authenticatorData,
Expand Down
Loading

0 comments on commit 1d2d6d8

Please sign in to comment.