diff --git a/OktaMobileSDK.xcworkspace/xcshareddata/IDETemplateMacros.plist b/OktaMobileSDK.xcworkspace/xcshareddata/IDETemplateMacros.plist index aab448c93..da634eac1 100644 --- a/OktaMobileSDK.xcworkspace/xcshareddata/IDETemplateMacros.plist +++ b/OktaMobileSDK.xcworkspace/xcshareddata/IDETemplateMacros.plist @@ -4,7 +4,7 @@ FILEHEADER -// Copyright (c) 2023-Present, Okta, Inc. and/or its affiliates. All rights reserved. +// Copyright (c) 2024-Present, Okta, Inc. and/or its affiliates. All rights reserved. // The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") // // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. diff --git a/Sources/AuthFoundation/Responses/GrantType.swift b/Sources/AuthFoundation/Responses/GrantType.swift index e013336fc..7b4d34415 100644 --- a/Sources/AuthFoundation/Responses/GrantType.swift +++ b/Sources/AuthFoundation/Responses/GrantType.swift @@ -23,6 +23,7 @@ public enum GrantType: Codable, Hashable { case oob case otpMFA case oobMFA + case webAuthn case other(_ type: String) } @@ -36,7 +37,8 @@ private let grantTypeMapping: [String: GrantType] = [ "urn:okta:params:oauth:grant-type:otp": .otp, "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 + "http://auth0.com/oauth/grant-type/mfa-oob": .oobMFA, + "urn:okta:params:oauth:grant-type:webauthn": .webAuthn ] @@ -75,6 +77,8 @@ extension GrantType: RawRepresentable { return "http://auth0.com/oauth/grant-type/mfa-otp" case .oobMFA: return "http://auth0.com/oauth/grant-type/mfa-oob" + case .webAuthn: + return "urn:okta:params:oauth:grant-type:webauthn" } } } diff --git a/Sources/AuthFoundation/Utilities/JSONValue.swift b/Sources/AuthFoundation/Utilities/JSONValue.swift new file mode 100644 index 000000000..70bce3971 --- /dev/null +++ b/Sources/AuthFoundation/Utilities/JSONValue.swift @@ -0,0 +1,172 @@ +// +// Copyright (c) 2023-Present, Okta, Inc. and/or its affiliates. All rights reserved. +// The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") +// +// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and limitations under the License. +// + +import Foundation + +public enum JSONValueError: Error { + case cannotDecode(value: Any) +} + +/// Represent mixed JSON values as instances of `Any`. This is used to expose API response values to Swift native types +/// where Swift enums are not supported. +public enum JSONValue: Equatable { + case string(String) + case number(Double) + case bool(Bool) + case dictionary([String: JSONValue]) + case array([JSONValue]) + case object(Any) + case null + + public init(_ value: Any?) throws { + if let value = value as? String { + self = .string(value) + } else if let value = value as? NSNumber { + self = .number(value.doubleValue) + } else if let value = value as? Bool { + self = .bool(value) + } else if let value = value as? [String: Any] { + self = .dictionary(try value.mapValues({ try JSONValue($0) })) + } else if let value = value as? [Any] { + self = .array(try value.map({ try JSONValue($0) })) + } else if value == nil { + self = .null + } else { + throw JSONValueError.cannotDecode(value: value as Any) + } + } + + /// Returns the value as an instance of `Any`. + public var anyValue: Any? { + switch self { + case let .string(value): + return value + case let .number(value): + return value + case let .bool(value): + return value + case let .dictionary(value): + return value.reduce(into: [String: Any?]()) { + $0[$1.key] = $1.value.anyValue + } + case let .array(value): + return value.map { $0.anyValue } + case let .object(value): + return value + case .null: + return nil + } + } + + public static func == (lhs: JSONValue, rhs: JSONValue) -> Bool { + switch (lhs, rhs) { + case (.string(let lhsValue), .string(let rhsValue)): + return lhsValue == rhsValue + case (.number(let lhsValue), .number(let rhsValue)): + return lhsValue == rhsValue + case (.bool(let lhsValue), .bool(let rhsValue)): + return lhsValue == rhsValue + case (.dictionary(let lhsValue), .dictionary(let rhsValue)): + return lhsValue == rhsValue + case (.array(let lhsValue), .array(let rhsValue)): + return lhsValue == rhsValue + case (.object(let lhsValue), .object(let rhsValue)): + if let lhsValue = lhsValue as? AnyHashable, + let rhsValue = rhsValue as? AnyHashable + { + return lhsValue == rhsValue + } else { + return false + } + case (.null, .null): + return true + default: + return false + } + } +} + +extension JSONValue: Codable { + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + if let value = try? container.decode(String.self) { + self = .string(value) + } else if let value = try? container.decode(Double.self) { + self = .number(value) + } else if let value = try? container.decode(Bool.self) { + self = .bool(value) + } else if let value = try? container.decode([String: JSONValue].self) { + self = .dictionary(value) + } else if let value = try? container.decode([JSONValue].self) { + self = .array(value) + } else if container.decodeNil() { + self = .null + } else { + throw DecodingError.dataCorrupted(.init(codingPath: decoder.codingPath, + debugDescription: "Invalid JSON value \(decoder.codingPath)")) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + switch self { + case let .string(value): + try container.encode(value) + case let .number(value): + try container.encode(value) + case let .bool(value): + try container.encode(value) + case let .dictionary(value): + try container.encode(value) + case let .array(value): + try container.encode(value) + case let .object(value): + if let value = value as? Codable { + try container.encode(value) + } else { + throw EncodingError.invalidValue(value, .init(codingPath: encoder.codingPath, + debugDescription: "Value is not encodable at \(encoder.codingPath)")) + } + case .null: + try container.encodeNil() + } + } +} + +extension JSONValue: CustomDebugStringConvertible { + public var debugDescription: String { + switch self { + case .string(let str): + return str.debugDescription + case .number(let num): + return num.debugDescription + case .bool(let bool): + return bool ? "true" : "false" + case .null: + return "null" + case .object(let obj): + if let obj = obj as? CustomDebugStringConvertible { + return obj.debugDescription + } else { + return "Custom object \(String(describing: obj))" + } + default: + let encoder = JSONEncoder() + encoder.outputFormatting = [.prettyPrinted] + // swiftlint:disable force_unwrapping + // swiftlint:disable force_try + return try! String(data: encoder.encode(self), encoding: .utf8)! + // swiftlint:enable force_try + // swiftlint:enable force_unwrapping + } + } +} diff --git a/Sources/OktaDirectAuth/DirectAuthFlow.swift b/Sources/OktaDirectAuth/DirectAuthFlow.swift index ab268be49..193b7c765 100644 --- a/Sources/OktaDirectAuth/DirectAuthFlow.swift +++ b/Sources/OktaDirectAuth/DirectAuthFlow.swift @@ -39,7 +39,7 @@ public enum DirectAuthenticationFlowError: Error { /// This enables developers to build native sign-in workflows into their applications, while leveraging MFA to securely authenticate users, without the need to present a browser. Furthermore, this enables passwordless authentication scenarios by giving developers the power to choose which primary and secondary authentication factors to use when challenging a user for their credentials. public class DirectAuthenticationFlow: AuthenticationFlow { /// Enumeration defining the list of possible primary authentication factors. - /// + /// /// These values are used by the ``DirectAuthenticationFlow/start(_:with:)`` function. public enum PrimaryFactor: Equatable { /// Authenticate the user with the given password. @@ -70,6 +70,15 @@ public class DirectAuthenticationFlow: AuthenticationFlow { /// /// > Note: While `.oob` accepts a `channel` argument, at this time only the `push` option is available. case oob(channel: OOBChannel = .push) + + /// Authenticate the user using WebAuthn. + /// + /// This requests that a new WebAuthn challenge is generated and returned to the client, which can subsequently be used to sign the attestation for return back to the server. + /// + /// ```swift + /// let status = try await flow.start("jane.doe@example.com", with: .webAuthn) + /// ``` + case webAuthn } /// Enumeration defining the list of possible secondary authentication factors. @@ -93,6 +102,20 @@ public class DirectAuthenticationFlow: AuthenticationFlow { /// /// > Note: While `.oob` accepts a `channel` argument, at this time only the `push` option is available. case oob(channel: OOBChannel) + + /// Authenticate the user using WebAuthn. + /// + /// This requests that a new WebAuthn challenge is generated and returned to the client, which can subsequently be used to sign the attestation for return back to the server. + /// + /// ```swift + /// let status = try await flow.start("jane.doe@example.com", with: .webAuthn) + /// ``` + case webAuthn + + /// Respond to a WebAuthn challenge with an authenticator assertion. + /// + /// This uses a previously supplied WebAuthn challenge (using ``DirectAuthenticationFlow/PrimaryFactor/webAuthn`` or ``webAuthn``) to respond to the server with the signed attestation from the local authenticator. + case webAuthnAssertion(_ response: WebAuthn.AuthenticatorAssertionResponse) } /// Channel used when authenticating an out-of-band factor using Okta Verify. @@ -140,6 +163,9 @@ public class DirectAuthenticationFlow: AuthenticationFlow { /// /// When this status is returned, the developer should use the ``DirectAuthenticationFlow/resume(_:with:)`` function to supply a secondary factor to verify the user. case mfaRequired(_ context: MFAContext) + + /// Indicates the user is being prompted with a WebAuthn challenge request. + case webAuthn(request: WebAuthn.CredentialRequestOptions) } /// The OAuth2Client this authentication flow will use. diff --git a/Sources/OktaDirectAuth/Internal/Authentication Factors/AuthenticationFactor.swift b/Sources/OktaDirectAuth/Internal/Authentication Factors/AuthenticationFactor.swift index 1863fde69..6c4667cca 100644 --- a/Sources/OktaDirectAuth/Internal/Authentication Factors/AuthenticationFactor.swift +++ b/Sources/OktaDirectAuth/Internal/Authentication Factors/AuthenticationFactor.swift @@ -13,14 +13,17 @@ import Foundation 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 } +} + /// Defines the common properties and functions shared between factor types. -protocol AuthenticationFactor { +protocol AuthenticationFactor: HasTokenParameters { /// The grant type supported by this factor. var grantType: GrantType { get } - /// Parameters to include in the API request. - var tokenParameters: [String: Any]? { get } - /// Returns a step handler capable of handling this authentication factor. /// - Parameters: /// - flow: The current flow for this authentication step. diff --git a/Sources/OktaDirectAuth/Internal/Authentication Factors/PrimaryFactor.swift b/Sources/OktaDirectAuth/Internal/Authentication Factors/PrimaryFactor.swift index 98934c1d9..409c99e97 100644 --- a/Sources/OktaDirectAuth/Internal/Authentication Factors/PrimaryFactor.swift +++ b/Sources/OktaDirectAuth/Internal/Authentication Factors/PrimaryFactor.swift @@ -31,10 +31,6 @@ 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: @@ -45,6 +41,10 @@ extension DirectAuthenticationFlow.PrimaryFactor: AuthenticationFactor { grantTypesSupported: flow.supportedGrantTypes) return TokenStepHandler(flow: flow, request: request) case .oob(channel: let channel): + var bindingContext: DirectAuthenticationFlow.BindingUpdateContext? + if case .bindingUpdate(let context) = currentStatus { + bindingContext = context + } return try OOBStepHandler(flow: flow, openIdConfiguration: openIdConfiguration, loginHint: loginHint, @@ -52,6 +52,14 @@ extension DirectAuthenticationFlow.PrimaryFactor: AuthenticationFactor { channel: channel, factor: factor, bindingContext: bindingContext) + case .webAuthn: + let request = try WebAuthnChallengeRequest(openIdConfiguration: openIdConfiguration, + clientConfiguration: flow.client.configuration, + loginHint: loginHint, + mfaToken: currentStatus?.mfaToken) + return ChallengeStepHandler(flow: flow, request: request) { + .webAuthn(request: $0) + } } } @@ -67,7 +75,7 @@ extension DirectAuthenticationFlow.PrimaryFactor: AuthenticationFactor { "grant_type": grantType.rawValue, "password": password ] - case .oob: + case .oob, .webAuthn: return nil } @@ -81,6 +89,8 @@ extension DirectAuthenticationFlow.PrimaryFactor: AuthenticationFactor { return .password case .oob: return .oob + case .webAuthn: + return .webAuthn } } } diff --git a/Sources/OktaDirectAuth/Internal/Authentication Factors/SecondaryFactor.swift b/Sources/OktaDirectAuth/Internal/Authentication Factors/SecondaryFactor.swift index 575e41490..e14591bc5 100644 --- a/Sources/OktaDirectAuth/Internal/Authentication Factors/SecondaryFactor.swift +++ b/Sources/OktaDirectAuth/Internal/Authentication Factors/SecondaryFactor.swift @@ -41,6 +41,23 @@ extension DirectAuthenticationFlow.SecondaryFactor: AuthenticationFactor { channel: channel, factor: factor, bindingContext: bindingContext) + case .webAuthn: + let request = try WebAuthnChallengeRequest(openIdConfiguration: openIdConfiguration, + clientConfiguration: flow.client.configuration, + loginHint: loginHint, + mfaToken: currentStatus?.mfaToken) + return ChallengeStepHandler(flow: flow, request: request) { + .webAuthn(request: $0) + } + case .webAuthnAssertion(let response): + let request = TokenRequest(openIdConfiguration: openIdConfiguration, + clientConfiguration: flow.client.configuration, + loginHint: loginHint, + factor: factor, + mfaToken: currentStatus?.mfaToken, + parameters: response, + grantTypesSupported: flow.supportedGrantTypes) + return TokenStepHandler(flow: flow, request: request) } } @@ -51,8 +68,12 @@ extension DirectAuthenticationFlow.SecondaryFactor: AuthenticationFactor { "grant_type": grantType.rawValue, "otp": code ] - case .oob: + case .oob, .webAuthn: return nil + case .webAuthnAssertion(_): + return [ + "grant_type": grantType.rawValue + ] } } @@ -63,6 +84,8 @@ extension DirectAuthenticationFlow.SecondaryFactor: AuthenticationFactor { return .otpMFA case .oob: return .oobMFA + case .webAuthn, .webAuthnAssertion(_): + return .webAuthn } } } diff --git a/Sources/OktaDirectAuth/Internal/Requests/OOBAuthenticateRequest.swift b/Sources/OktaDirectAuth/Internal/Requests/OOBAuthenticateRequest.swift index cbea63bea..7a017f304 100644 --- a/Sources/OktaDirectAuth/Internal/Requests/OOBAuthenticateRequest.swift +++ b/Sources/OktaDirectAuth/Internal/Requests/OOBAuthenticateRequest.swift @@ -14,12 +14,12 @@ import Foundation import AuthFoundation extension OpenIdConfiguration { - var oobAuthenticateEndpoint: URL? { - tokenEndpoint.url(replacing: "/token", with: "/oob-authenticate") + var primaryAuthenticateEndpoint: URL? { + tokenEndpoint.url(replacing: "/token", with: "/primary-authenticate") } } -struct OOBResponse: Codable { +struct OOBResponse: Codable, HasTokenParameters { let oobCode: String let expiresIn: TimeInterval let interval: TimeInterval @@ -35,6 +35,10 @@ struct OOBResponse: Codable { self.bindingMethod = bindingMethod self.bindingCode = bindingCode } + + var tokenParameters: [String : Any]? { + ["oob_code": oobCode] + } } struct OOBAuthenticateRequest { @@ -48,7 +52,7 @@ struct OOBAuthenticateRequest { loginHint: String, channelHint: DirectAuthenticationFlow.OOBChannel) throws { - guard let url = openIdConfiguration.oobAuthenticateEndpoint else { + guard let url = openIdConfiguration.primaryAuthenticateEndpoint else { throw OAuth2Error.cannotComposeUrl } diff --git a/Sources/OktaDirectAuth/Internal/Requests/TokenRequest.swift b/Sources/OktaDirectAuth/Internal/Requests/TokenRequest.swift index fb60abb04..df5ded182 100644 --- a/Sources/OktaDirectAuth/Internal/Requests/TokenRequest.swift +++ b/Sources/OktaDirectAuth/Internal/Requests/TokenRequest.swift @@ -19,7 +19,7 @@ struct TokenRequest { let loginHint: String? let factor: any AuthenticationFactor let mfaToken: String? - let oobCode: String? + let parameters: (any HasTokenParameters)? let grantTypesSupported: [GrantType]? init(openIdConfiguration: OpenIdConfiguration, @@ -27,7 +27,7 @@ struct TokenRequest { loginHint: String? = nil, factor: any AuthenticationFactor, mfaToken: String? = nil, - oobCode: String? = nil, + parameters: (any HasTokenParameters)? = nil, grantTypesSupported: [GrantType]? = nil) { self.openIdConfiguration = openIdConfiguration @@ -35,7 +35,7 @@ struct TokenRequest { self.loginHint = loginHint self.factor = factor self.mfaToken = mfaToken - self.oobCode = oobCode + self.parameters = parameters self.grantTypesSupported = grantTypesSupported } } @@ -57,8 +57,8 @@ extension TokenRequest: OAuth2TokenRequest, OAuth2APIRequest, APIRequestBody { result["mfa_token"] = mfaToken } - if let oobCode = oobCode { - result["oob_code"] = oobCode + if let tokenParameters = parameters?.tokenParameters { + result.merge(tokenParameters, uniquingKeysWith: { $1 }) } if let loginHint = loginHint { diff --git a/Sources/OktaDirectAuth/Internal/Requests/WebAuthnRequest.swift b/Sources/OktaDirectAuth/Internal/Requests/WebAuthnRequest.swift new file mode 100644 index 000000000..539507820 --- /dev/null +++ b/Sources/OktaDirectAuth/Internal/Requests/WebAuthnRequest.swift @@ -0,0 +1,80 @@ +// +// Copyright (c) 2024-Present, Okta, Inc. and/or its affiliates. All rights reserved. +// The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") +// +// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and limitations under the License. +// + +import Foundation +import AuthFoundation + +struct WebAuthnChallengeRequest { + let url: URL + let clientConfiguration: OAuth2Client.Configuration + let loginHint: String? + let mfaToken: String? + + init(openIdConfiguration: OpenIdConfiguration, + clientConfiguration: OAuth2Client.Configuration, + loginHint: String? = nil, + mfaToken: String? = nil) throws + { + guard let url = openIdConfiguration.primaryAuthenticateEndpoint else { + throw OAuth2Error.cannotComposeUrl + } + + self.url = url + self.clientConfiguration = clientConfiguration + self.loginHint = loginHint + self.mfaToken = mfaToken + } +} + +extension WebAuthnChallengeRequest: APIRequest, APIRequestBody { + typealias ResponseType = WebAuthn.CredentialRequestOptions + + var httpMethod: APIRequestMethod { .post } + var contentType: APIContentType? { .formEncoded } + var acceptsType: APIContentType? { .json } + var bodyParameters: [String: Any]? { + var result: [String: Any] = [ + "client_id": clientConfiguration.clientId, + "challenge_hint": GrantType.webAuthn.rawValue + ] + + if let loginHint = loginHint { + result["login_hint"] = loginHint + } + + if let mfaToken = mfaToken { + result["mfa_token"] = mfaToken + } + + if let parameters = clientConfiguration.authentication.additionalParameters { + result.merge(parameters, uniquingKeysWith: { $1 }) + } + + return result + } +} + +extension WebAuthn.AuthenticatorAssertionResponse: HasTokenParameters { + var tokenParameters: [String : Any]? { + var result = [ + "clientDataJSON": clientDataJSON, + "authenticatorData": authenticatorData, + "signature": signature, + ] + + if let userHandle = userHandle { + result["userHandle"] = userHandle + } + + return result + } +} diff --git a/Sources/OktaDirectAuth/Internal/Step Handlers/ChallengeStepHandler.swift b/Sources/OktaDirectAuth/Internal/Step Handlers/ChallengeStepHandler.swift new file mode 100644 index 000000000..e33d4b284 --- /dev/null +++ b/Sources/OktaDirectAuth/Internal/Step Handlers/ChallengeStepHandler.swift @@ -0,0 +1,45 @@ +// +// Copyright (c) 2024-Present, Okta, Inc. and/or its affiliates. All rights reserved. +// The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") +// +// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and limitations under the License. +// + +import Foundation +import AuthFoundation + +class ChallengeStepHandler: StepHandler { + let flow: DirectAuthenticationFlow + let request: Request + private let statusBlock: (_ response: Request.ResponseType) throws -> DirectAuthenticationFlow.Status + + init(flow: DirectAuthenticationFlow, + request: Request, + statusBlock: @escaping (_ response: Request.ResponseType) throws -> DirectAuthenticationFlow.Status) + { + self.flow = flow + self.request = request + self.statusBlock = statusBlock + } + + func process(completion: @escaping (Result) -> Void) { + request.send(to: flow.client) { result in + switch result { + case .failure(let error): + self.flow.process(error, completion: completion) + case .success(let response): + do { + let status = try self.statusBlock(response.result) + completion(.success(status)) + } catch { + completion(.failure(.init(error))) + } + } + } + } +} diff --git a/Sources/OktaDirectAuth/Internal/Step Handlers/OOBStepHandler.swift b/Sources/OktaDirectAuth/Internal/Step Handlers/OOBStepHandler.swift index f2b57f8f3..d8ed2c768 100644 --- a/Sources/OktaDirectAuth/Internal/Step Handlers/OOBStepHandler.swift +++ b/Sources/OktaDirectAuth/Internal/Step Handlers/OOBStepHandler.swift @@ -139,7 +139,7 @@ class OOBStepHandler: StepHandler { clientConfiguration: self.flow.client.configuration, factor: self.factor, mfaToken: self.mfaToken, - oobCode: response.oobCode, + parameters: response, grantTypesSupported: self.flow.supportedGrantTypes) self.poll = PollingHandler(client: self.flow.client, request: request, diff --git a/Sources/OktaDirectAuth/WebAuthn/PublicKeyCredentialDescriptor.swift b/Sources/OktaDirectAuth/WebAuthn/PublicKeyCredentialDescriptor.swift new file mode 100644 index 000000000..52f1f4843 --- /dev/null +++ b/Sources/OktaDirectAuth/WebAuthn/PublicKeyCredentialDescriptor.swift @@ -0,0 +1,30 @@ +// Copyright (c) 2023-Present, Okta, Inc. and/or its affiliates. All rights reserved. +// The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") +// +// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and limitations under the License. +// + +import Foundation + +extension WebAuthn { + /** + This dictionary contains the attributes that are specified by a caller when referring to a public key credential as an input parameter to the create() or get() methods. It mirrors the fields of the PublicKeyCredential object returned by the latter methods. + + - Note: [W3C Reccomendation](https://www.w3.org/TR/webauthn/#dictionary-credential-descriptor) + */ + public struct PublicKeyCredentialDescriptor: Codable { + /// This member contains the credential ID of the public key credential the caller is referring to. + public let id: Data + + /// This member contains the type of the public key credential the caller is referring to. + public let type: PublicKeyCredentialType + + /// This OPTIONAL member contains a hint as to how the client might communicate with the managing authenticator of the public key credential the caller is referring to. The values SHOULD be members of AuthenticatorTransport but client platforms MUST ignore unknown values. + public let transports: [AuthenticatorTransport] + } +} diff --git a/Sources/OktaDirectAuth/WebAuthn/PublicKeyCredentialRequestOptions.swift b/Sources/OktaDirectAuth/WebAuthn/PublicKeyCredentialRequestOptions.swift new file mode 100644 index 000000000..da77a6c7a --- /dev/null +++ b/Sources/OktaDirectAuth/WebAuthn/PublicKeyCredentialRequestOptions.swift @@ -0,0 +1,90 @@ +// Copyright (c) 2023-Present, Okta, Inc. and/or its affiliates. All rights reserved. +// The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") +// +// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and limitations under the License. +// + +import Foundation + +extension WebAuthn { + /** + The PublicKeyCredentialRequestOptions dictionary supplies get() with the data it needs to generate an assertion. Its challenge member MUST be present, while its other members are OPTIONAL. + + - Note: [W3C Reccomendation](https://www.w3.org/TR/webauthn/#dictionary-assertion-options) + */ + public struct PublicKeyCredentialRequestOptions: Codable { + /// This member specifies a challenge that the authenticator signs, along with other data, when producing an authentication assertion. See the § 13.4.3 Cryptographic Challenges security consideration. + public let challenge: Data + + /// Specifies the RP ID claimed by the Relying Party. The client MUST verify that the Relying Party's origin matches the scope of this RP ID. The authenticator MUST verify that this RP ID exactly equals the rpId of the credential to be used for the authentication ceremony. + public internal(set) var rpID: String? + + /// Used by the client to find authenticators eligible for this authentication ceremony. + public let allowCredentials: [PublicKeyCredentialDescriptor]? + + /// Specifies a time that the Relying Party is willing to wait for the call to complete. + public let timeout: TimeInterval? + + /// Specifies the Relying Party's requirements regarding user verification. Eligible authenticators are filtered to only those capable of satisfying this requirement. + public let userVerification: UserVerificationRequirement? + + /// Guides the user agent in interacting with the user. + public let hints: [PublicKeyCredentialHints]? + + /// The Relying Party MAY use this to provide client extension inputs requesting additional processing by the client and authenticator. + public let extensions: [String: Any?]? + + enum CodingKeys: String, CodingKey { + case allowCredentials + case challenge + case extensions + case rpID + case timeout + case hints + case userVerification + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + allowCredentials = try container.decodeIfPresent([PublicKeyCredentialDescriptor].self, forKey: .allowCredentials) + challenge = try container.decode(Data.self, forKey: .challenge) + rpID = try container.decodeIfPresent(String.self, forKey: .rpID) + hints = try container.decodeIfPresent([PublicKeyCredentialHints].self, forKey: .hints) + userVerification = try container.decodeIfPresent(UserVerificationRequirement.self, forKey: .userVerification) + + if let interval = try container.decodeIfPresent(UInt64.self, forKey: .timeout) { + timeout = Double(interval) / 1000.0 + } else { + timeout = nil + } + + if let jsonValues = try container.decodeIfPresent([String: JSONValue].self, forKey: .extensions) { + extensions = jsonValues.mapValues({ $0.anyValue }) + } else { + extensions = nil + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(challenge, forKey: .challenge) + try container.encodeIfPresent(allowCredentials, forKey: .allowCredentials) + try container.encodeIfPresent(rpID, forKey: .rpID) + try container.encodeIfPresent(hints, forKey: .hints) + + if let timeout = timeout { + try container.encode(UInt64(timeout * 1000), forKey: .timeout) + } + + if let extensions = extensions { + try container.encode(try extensions.mapValues({ try JSONValue($0) }), forKey: .extensions) + } + } + } +} diff --git a/Sources/OktaDirectAuth/WebAuthn/Type/AuthenticatorTransport.swift b/Sources/OktaDirectAuth/WebAuthn/Type/AuthenticatorTransport.swift new file mode 100644 index 000000000..9b4a91d6a --- /dev/null +++ b/Sources/OktaDirectAuth/WebAuthn/Type/AuthenticatorTransport.swift @@ -0,0 +1,39 @@ +// Copyright (c) 2023-Present, Okta, Inc. and/or its affiliates. All rights reserved. +// The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") +// +// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and limitations under the License. +// + +import Foundation + +extension WebAuthn { + /** + This member contains the type of the public key credential the caller is referring to. + + - Note: [W3C Reccomendation](https://www.w3.org/TR/webauthn/#dom-publickeycredentialdescriptor-type) + */ + public enum AuthenticatorTransport: String, Codable { + /// Indicates the respective authenticator can be contacted over Bluetooth Smart (Bluetooth Low Energy / BLE). + case ble + + /// Indicates the respective authenticator can be contacted over Near Field Communication (NFC). + case nfc + + /// Indicates the respective authenticator is contacted using a client device-specific transport, i.e., it is a platform authenticator. These authenticators are not removable from the client device. + case platform = "internal" + + /// Indicates the respective authenticator can be contacted over removable USB + case usb + + /// Indicates the respective authenticator can be contacted over ISO/IEC 7816 smart card with contacts. + case smartCard = "smart-card" + + /// Indicates the respective authenticator can be contacted using a combination of (often separate) data-transport and proximity mechanisms. This supports, for example, authentication on a desktop computer using a smartphone. + case hybrid + } +} diff --git a/Sources/OktaDirectAuth/WebAuthn/Type/PublicKeyCredentialHints.swift b/Sources/OktaDirectAuth/WebAuthn/Type/PublicKeyCredentialHints.swift new file mode 100644 index 000000000..3ecb695e6 --- /dev/null +++ b/Sources/OktaDirectAuth/WebAuthn/Type/PublicKeyCredentialHints.swift @@ -0,0 +1,31 @@ +// +// Copyright (c) 2024-Present, Okta, Inc. and/or its affiliates. All rights reserved. +// The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") +// +// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and limitations under the License. +// + +import Foundation + +extension WebAuthn { + /** + WebAuthn Relying Parties may use this enumeration to communicate hints to the user-agent about how a request may be best completed. + + - Note: [W3C Reccomendation](https://w3c.github.io/webauthn/#enumdef-publickeycredentialhints) + */ + public enum PublicKeyCredentialHints: String, Codable { + /// Indicates that the Relying Party believes that users will satisfy this request with a physical security key. + case securityKey = "security-key" + + /// Indicates that the Relying Party believes that users will satisfy this request with a platform authenticator attached to the client device. + case clientDevice = "client-device" + + /// Indicates that the Relying Party believes that users will satisfy this request with general-purpose authenticators such as smartphones. + case hybrid + } +} diff --git a/Sources/OktaDirectAuth/WebAuthn/Type/PublicKeyCredentialType.swift b/Sources/OktaDirectAuth/WebAuthn/Type/PublicKeyCredentialType.swift new file mode 100644 index 000000000..2bf023594 --- /dev/null +++ b/Sources/OktaDirectAuth/WebAuthn/Type/PublicKeyCredentialType.swift @@ -0,0 +1,24 @@ +// Copyright (c) 2023-Present, Okta, Inc. and/or its affiliates. All rights reserved. +// The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") +// +// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and limitations under the License. +// + +import Foundation + +extension WebAuthn { + /** + This member contains the type of the public key credential the caller is referring to. + + - Note: [W3C Reccomendation](https://www.w3.org/TR/webauthn/#dom-publickeycredentialdescriptor-type) + */ + public enum PublicKeyCredentialType: String, Codable { + /// Descripes a public key credential type. + case publicKey = "public-key" + } +} diff --git a/Sources/OktaDirectAuth/WebAuthn/Type/UserVerificationRequirement.swift b/Sources/OktaDirectAuth/WebAuthn/Type/UserVerificationRequirement.swift new file mode 100644 index 000000000..c51a40a1b --- /dev/null +++ b/Sources/OktaDirectAuth/WebAuthn/Type/UserVerificationRequirement.swift @@ -0,0 +1,30 @@ +// Copyright (c) 2023-Present, Okta, Inc. and/or its affiliates. All rights reserved. +// The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") +// +// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and limitations under the License. +// + +import Foundation + +extension WebAuthn { + /** + A WebAuthn Relying Party may require user verification for some of its operations but not for others, and may use this type to express its needs. + + - Note: [W3C Reccomendation](https://www.w3.org/TR/webauthn/#enum-userVerificationRequirement) + */ + public enum UserVerificationRequirement: String, Codable { + /// This value indicates that the Relying Party requires user verification for the operation and will fail the operation if the response does not have the UV flag set. + case required + + /// This value indicates that the Relying Party prefers user verification for the operation if possible, but will not fail the operation if the response does not have the UV flag set. + case preferred + + /// This value indicates that the Relying Party does not want user verification employed during the operation. + case discouraged + } +} diff --git a/Sources/OktaDirectAuth/WebAuthn/WebAuthn.swift b/Sources/OktaDirectAuth/WebAuthn/WebAuthn.swift new file mode 100644 index 000000000..1a4ea1ddd --- /dev/null +++ b/Sources/OktaDirectAuth/WebAuthn/WebAuthn.swift @@ -0,0 +1,74 @@ +// +// Copyright (c) 2024-Present, Okta, Inc. and/or its affiliates. All rights reserved. +// The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") +// +// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and limitations under the License. +// + +import Foundation + +/// Exposes the types and classes used to authenticate using WebAuthn. +/// +/// Example: +/// +/// ```swift +/// let challengeStatus = try await flow.start("user@example.com", with: .webAuthn) +/// guard case let .webAuthn(let request) = challengeStatus else { return } +/// +/// // Supply challenge request values to your authenticator +/// let responseStatus = try await flow.resume( +/// challengeStatus, +/// with: .webAuthnAssertion(.init( +/// clientDataJSON: authJson, +/// authenticatorData: authData, +/// signature: authSignature, +/// userHandle: nil) +/// ) +/// ) +/// ``` +public struct WebAuthn { + /// Represents the credential challenge returned from the server when a WebAuthn authentication is initiated. + public struct CredentialRequestOptions: Codable { + /// The public key request options supplied to the client from the server. + public let publicKey: WebAuthn.PublicKeyCredentialRequestOptions + + /// Defines additional authenticator enrollment information supplied by the server. + public let authenticatorEnrollments: [AuthenticatorEnrollment]? + + /// Defines additional authenticator enrollment information supplied by the server. + public struct AuthenticatorEnrollment: Codable { + /// The ID supplied from the server representing this credential. + /// + /// **Note:** This should be identical to the ``WebAuthn/PublicKeyCredentialRequestOptions/rpID`` value. + public let credentialId: String + + /// The human-readable display name for this authenticator. + public let displayName: String + + /// Additional profile information related to this authenticator. + public let profile: [String: String] + } + } + + /// Defines the set of data expected from the client in response to an authenticator challenge. + /// + /// This value should be supplied to the ``DirectAuthenticationFlow/SecondaryFactor/webAuthnAssertion`` type. + public struct AuthenticatorAssertionResponse: Codable, Equatable { + /// The client data JSON response, represented as a string. + public let clientDataJSON: String + + /// The authenticator data for the response. + public let authenticatorData: String + + /// The signature generated from the authenticator. + public let signature: String + + /// The optional user handle to supply to the server, typically if the resident key is enabled. + public let userHandle: String? + } +} diff --git a/Tests/AuthFoundationTests/JSONValueTests.swift b/Tests/AuthFoundationTests/JSONValueTests.swift new file mode 100644 index 000000000..22e2f20b7 --- /dev/null +++ b/Tests/AuthFoundationTests/JSONValueTests.swift @@ -0,0 +1,153 @@ +/* + * Copyright (c) 2021-Present, Okta, Inc. and/or its affiliates. All rights reserved. + * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") + * + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + * See the License for the specific language governing permissions and limitations under the License. + */ + +import XCTest +@testable import AuthFoundation + +class JSONValueTests: XCTestCase { + let decoder = JSONDecoder() + let encoder = JSONEncoder() + + func testString() throws { + let value = JSONValue.string("Test String") + XCTAssertNotNil(value) + XCTAssertEqual(value.debugDescription, "\"Test String\"") + + if let stringValue = value.anyValue as? String { + XCTAssertEqual(stringValue, "Test String") + } else { + XCTFail("Object not a string") + } + + let encoded = try encoder.encode(value) + XCTAssertNotNil(encoded) + let decoded = try decoder.decode(JSONValue.self, from: encoded) + XCTAssertEqual(decoded, value) + } + + func testNumber() throws { + let value = JSONValue.number(1) + XCTAssertNotNil(value) + XCTAssertEqual(value.debugDescription, "1.0") + + if let numberValue = value.anyValue as? NSNumber { + XCTAssertEqual(numberValue, NSNumber(integerLiteral: 1)) + } else { + XCTFail("Object not a number") + } + + let encoded = try encoder.encode(value) + XCTAssertNotNil(encoded) + let decoded = try decoder.decode(JSONValue.self, from: encoded) + XCTAssertEqual(decoded, value) + } + + func testBool() throws { + let value = JSONValue.bool(true) + XCTAssertNotNil(value) + XCTAssertEqual(value.debugDescription, "true") + XCTAssertEqual(JSONValue.bool(false).debugDescription, "false") + + if let boolValue = value.anyValue as? NSNumber { + XCTAssertEqual(boolValue, NSNumber(booleanLiteral: true)) + } else { + XCTFail("Object not a bool") + } + + let encoded = try encoder.encode(value) + XCTAssertNotNil(encoded) + let decoded = try decoder.decode(JSONValue.self, from: encoded) + XCTAssertEqual(decoded, value) + } + + func testNull() throws { + let value = JSONValue.null + XCTAssertNotNil(value) + XCTAssertEqual(value.debugDescription, "null") + + XCTAssertNil(value.anyValue) + + let encoded = try encoder.encode(value) + XCTAssertNotNil(encoded) + let decoded = try decoder.decode(JSONValue.self, from: encoded) + XCTAssertEqual(decoded, value) + } + + func testArray() throws { + let value = JSONValue.array([JSONValue.string("foo"), JSONValue.string("bar")]) + XCTAssertNotNil(value) + XCTAssertEqual(value.debugDescription, """ + [ + "foo", + "bar" + ] + """) + + if let arrayValue = value.anyValue as? NSArray { + XCTAssertEqual(arrayValue, ["foo", "bar"]) + } else { + XCTFail("Object not a array") + } + + let encoded = try encoder.encode(value) + XCTAssertNotNil(encoded) + let decoded = try decoder.decode(JSONValue.self, from: encoded) + XCTAssertEqual(decoded, value) + } + + func testDictionary() throws { + let value = JSONValue.dictionary( + ["foo": JSONValue.dictionary( + ["bar": JSONValue.array( + [JSONValue.string("woof")]) + ]) + ]) + XCTAssertNotNil(value) + XCTAssertEqual(value.debugDescription, """ + { + "foo" : { + "bar" : [ + "woof" + ] + } + } + """) + if let dictValue = value.anyValue as? NSDictionary { + XCTAssertEqual(dictValue, ["foo": ["bar": ["woof"]]]) + } else { + XCTFail("Object not a dictionary") + } + + let encoded = try encoder.encode(value) + XCTAssertNotNil(encoded) + let decoded = try decoder.decode(JSONValue.self, from: encoded) + XCTAssertEqual(decoded, value) + } + + func testObject() throws { + let object = URL(string: "https://example.com")! + let value = JSONValue.object(object) + XCTAssertNotNil(value) + XCTAssertEqual(value.debugDescription, "https://example.com") + + if let urlValue = value.anyValue as? URL { + XCTAssertEqual(urlValue, URL(string: "https://example.com")) + } else { + XCTFail("Object not a URL") + } + + XCTAssertEqual(value, JSONValue.object(URL(string: "https://example.com")!)) + let encoded = try encoder.encode(value) + let decoded = try decoder.decode(JSONValue.self, from: encoded) + XCTAssertEqual(decoded.anyValue as? String, "https://example.com") + } +} diff --git a/Tests/OktaDirectAuthTests/DirectAuth1FATests.swift b/Tests/OktaDirectAuthTests/DirectAuth1FATests.swift index b68af549d..03ffbd598 100644 --- a/Tests/OktaDirectAuthTests/DirectAuth1FATests.swift +++ b/Tests/OktaDirectAuthTests/DirectAuth1FATests.swift @@ -59,6 +59,8 @@ final class DirectAuth1FATests: XCTestCase { XCTFail("Not expecting MFA Required") case .bindingUpdate(_): XCTFail("Not expecting binding update") + case .webAuthn(request: _): + XCTFail("Not expecting webauthn request") } } @@ -79,6 +81,8 @@ final class DirectAuth1FATests: XCTestCase { XCTFail("Not expecting MFA Required") case .bindingUpdate(_): XCTFail("Not expecting binding update") + case .webAuthn(request: _): + XCTFail("Not expecting webauthn request") } } @@ -99,6 +103,8 @@ final class DirectAuth1FATests: XCTestCase { XCTFail("Not expecting MFA Required") case .bindingUpdate(_): XCTFail("Not expecting binding update") + case .webAuthn(request: _): + XCTFail("Not expecting webauthn request") } } #endif diff --git a/Tests/OktaDirectAuthTests/DirectAuth2FATests.swift b/Tests/OktaDirectAuthTests/DirectAuth2FATests.swift index 590308874..cadc95e26 100644 --- a/Tests/OktaDirectAuthTests/DirectAuth2FATests.swift +++ b/Tests/OktaDirectAuthTests/DirectAuth2FATests.swift @@ -66,9 +66,13 @@ final class DirectAuth2FATests: XCTestCase { XCTFail("Not expecting MFA Required") case .bindingUpdate(_): XCTFail("Not expecting binding update") + case .webAuthn(request: _): + XCTFail("Not expecting webauthn request") } case .bindingUpdate(_): XCTFail("Not expecting binding update") + case .webAuthn(request: _): + XCTFail("Not expecting webauthn request") } XCTAssertFalse(flow.isAuthenticating) } diff --git a/Tests/OktaDirectAuthTests/FactorPropertyTests.swift b/Tests/OktaDirectAuthTests/FactorPropertyTests.swift index 02f1f066f..5402fbbf8 100644 --- a/Tests/OktaDirectAuthTests/FactorPropertyTests.swift +++ b/Tests/OktaDirectAuthTests/FactorPropertyTests.swift @@ -21,15 +21,22 @@ final class FactorPropertyTests: XCTestCase { XCTAssertEqual(PrimaryFactor.password("foo").loginHintKey, "username") XCTAssertEqual(PrimaryFactor.otp(code: "123456").loginHintKey, "login_hint") XCTAssertEqual(PrimaryFactor.oob(channel: .push).loginHintKey, "login_hint") + XCTAssertEqual(PrimaryFactor.webAuthn.loginHintKey, "login_hint") } func testGrantTypes() throws { XCTAssertEqual(PrimaryFactor.password("foo").grantType, .password) XCTAssertEqual(PrimaryFactor.otp(code: "123456").grantType, .otp) XCTAssertEqual(PrimaryFactor.oob(channel: .push).grantType, .oob) + XCTAssertEqual(PrimaryFactor.webAuthn.grantType, .webAuthn) XCTAssertEqual(SecondaryFactor.otp(code: "123456").grantType, .otpMFA) XCTAssertEqual(SecondaryFactor.oob(channel: .push).grantType, .oobMFA) + XCTAssertEqual(SecondaryFactor.webAuthn.grantType, .webAuthn) + XCTAssertEqual(SecondaryFactor.webAuthnAssertion(.init(clientDataJSON: "", + authenticatorData: "", + signature: "", + userHandle: nil)).grantType, .webAuthn) } func testTokenParameters() throws { @@ -42,11 +49,20 @@ final class FactorPropertyTests: XCTestCase { "otp": "123456" ]) XCTAssertNil(PrimaryFactor.oob(channel: .push).tokenParameters) + XCTAssertNil(PrimaryFactor.webAuthn.tokenParameters) XCTAssertEqual(SecondaryFactor.otp(code: "123456").tokenParameters as? [String: String], [ "grant_type": "http://auth0.com/oauth/grant-type/mfa-otp", "otp": "123456" ]) XCTAssertNil(SecondaryFactor.oob(channel: .push).tokenParameters) + XCTAssertNil(SecondaryFactor.webAuthn.tokenParameters) + XCTAssertEqual(SecondaryFactor.webAuthnAssertion(.init(clientDataJSON: "", + authenticatorData: "", + signature: "", + userHandle: nil)).tokenParameters as? [String: String], + [ + "grant_type": "urn:okta:params:oauth:grant-type:webauthn", + ]) } } diff --git a/Tests/OktaDirectAuthTests/FactorStepHandlerTests.swift b/Tests/OktaDirectAuthTests/FactorStepHandlerTests.swift index c9bb3145a..a07a0c146 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(_), .bindingUpdate(_): + case .mfaRequired(_), .bindingUpdate(_), .webAuthn(request: _): 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(_), .bindingUpdate(_): + case .success(_), .bindingUpdate(_), .webAuthn(request: _): XCTFail("Did not receive a mfa_required response") case .mfaRequired(let context): XCTAssertEqual(context.mfaToken, "abcd1234") @@ -215,8 +215,8 @@ final class FactorStepHandlerTests: XCTestCase { 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", in: "MockResponses")) + urlSession.expect("https://example.okta.com/oauth2/v1/primary-authenticate", + data: try data(from: .module, for: "primary-authenticate", in: "MockResponses")) urlSession.expect("https://example.okta.com/oauth2/v1/token", data: try data(from: .module, for: "token", in: "MockResponses")) @@ -232,7 +232,7 @@ final class FactorStepHandlerTests: XCTestCase { case .success(let status): switch status { case .success(_): break - case .mfaRequired(_), .bindingUpdate(_): + case .mfaRequired(_), .bindingUpdate(_), .webAuthn(request: _): XCTFail("Did not receive a success response") } case .failure(let error): @@ -250,8 +250,8 @@ final class FactorStepHandlerTests: XCTestCase { 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/primary-authenticate", + data: try data(from: .module, for: "primary-authenticate-binding-transfer", in: "MockResponses")) urlSession.expect("https://example.okta.com/oauth2/v1/token", data: try data(from: .module, for: "token", in: "MockResponses")) @@ -294,8 +294,8 @@ final class FactorStepHandlerTests: XCTestCase { 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/primary-authenticate", + data: try data(from: .module, for: "primary-authenticate-binding-transfer-missingCode", in: "MockResponses")) urlSession.expect("https://example.okta.com/oauth2/v1/token", data: try data(from: .module, for: "token", in: "MockResponses")) @@ -325,8 +325,8 @@ final class FactorStepHandlerTests: XCTestCase { 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", in: "MockResponses")) + urlSession.expect("https://example.okta.com/oauth2/v1/primary-authenticate", + data: try data(from: .module, for: "primary-authenticate", in: "MockResponses")) urlSession.expect("https://example.okta.com/oauth2/v1/token", data: try data(from: .module, for: "token-mfa_required", in: "MockResponses"), statusCode: 400) @@ -342,7 +342,7 @@ final class FactorStepHandlerTests: XCTestCase { switch result { case .success(let status): switch status { - case .success(_), .bindingUpdate(_): + case .success(_), .bindingUpdate(_), .webAuthn(request: _): XCTFail("Did not receive a mfa_required response") case .mfaRequired(let context): XCTAssertEqual(context.mfaToken, "abcd1234") @@ -381,7 +381,7 @@ final class FactorStepHandlerTests: XCTestCase { case .success(let status): switch status { case .success(_): break - case .mfaRequired(_), .bindingUpdate(_): + case .mfaRequired(_), .bindingUpdate(_), .webAuthn(request: _): XCTFail("Did not receive a success response") } case .failure(let error): diff --git a/Tests/OktaDirectAuthTests/MockResponses/oob-authenticate-binding-transfer-missingCode.json b/Tests/OktaDirectAuthTests/MockResponses/primary-authenticate-binding-transfer-missingCode.json similarity index 100% rename from Tests/OktaDirectAuthTests/MockResponses/oob-authenticate-binding-transfer-missingCode.json rename to Tests/OktaDirectAuthTests/MockResponses/primary-authenticate-binding-transfer-missingCode.json diff --git a/Tests/OktaDirectAuthTests/MockResponses/oob-authenticate-binding-transfer.json b/Tests/OktaDirectAuthTests/MockResponses/primary-authenticate-binding-transfer.json similarity index 100% rename from Tests/OktaDirectAuthTests/MockResponses/oob-authenticate-binding-transfer.json rename to Tests/OktaDirectAuthTests/MockResponses/primary-authenticate-binding-transfer.json diff --git a/Tests/OktaDirectAuthTests/MockResponses/oob-authenticate.json b/Tests/OktaDirectAuthTests/MockResponses/primary-authenticate.json similarity index 100% rename from Tests/OktaDirectAuthTests/MockResponses/oob-authenticate.json rename to Tests/OktaDirectAuthTests/MockResponses/primary-authenticate.json