diff --git a/Sources/WebAuthn/Ceremonies/Registration/AuthenticatorSelection.swift b/Sources/WebAuthn/Ceremonies/Registration/AuthenticatorSelection.swift new file mode 100644 index 0000000..27ba7a0 --- /dev/null +++ b/Sources/WebAuthn/Ceremonies/Registration/AuthenticatorSelection.swift @@ -0,0 +1,78 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift WebAuthn open source project +// +// Copyright (c) 2022 the Swift WebAuthn project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation + +/// A dictionary describing the Relying Party's requirements regarding authenticator attributes. +/// +/// - SeeAlso: [WebAuthn Level 3 Working Draft §5.4.4. Authenticator Selection Criteria](https://www.w3.org/TR/webauthn-3/#dictionary-authenticatorSelection) +public struct AuthenticatorSelection: Sendable, Hashable { + /// If present, indicates the Relying Party's preference for authenticator attachment. + /// - SeeAlso: [WebAuthn Level 3 Working Draft §5.4.4. Authenticator Selection Criteria](https://www.w3.org/TR/webauthn-3/#dom-authenticatorselectioncriteria-authenticatorattachment) + public var authenticatorAttachment: AuthenticatorAttachment? + + /// Describes the Relying Party's requirements regarding whether the authenticator should create a client-side-resident public key credential source. + /// - SeeAlso: [WebAuthn Level 3 Working Draft §5.4.4. Authenticator Selection Criteria](https://www.w3.org/TR/webauthn-3/#dom-authenticatorselectioncriteria-residentkey) + public var residentKey: ResidentKeyRequirement? + + /// Describes the Relying Party's requirements regarding user verification. + /// - SeeAlso: [WebAuthn Level 3 Working Draft §5.4.4. Authenticator Selection Criteria](https://www.w3.org/TR/webauthn-3/#dom-authenticatorselectioncriteria-userverification) + public var userVerification: UserVerificationRequirement? + + public init( + authenticatorAttachment: AuthenticatorAttachment? = nil, + residentKey: ResidentKeyRequirement? = nil, + userVerification: UserVerificationRequirement? = nil + ) { + self.authenticatorAttachment = authenticatorAttachment + self.residentKey = residentKey + self.userVerification = userVerification + } +} + +extension AuthenticatorSelection: Codable { + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + self.authenticatorAttachment = try container.decodeIfPresent( + AuthenticatorAttachment.self, forKey: .authenticatorAttachment) + self.residentKey = try container.decodeIfPresent( + ResidentKeyRequirement.self, forKey: .residentKey) + self.userVerification = try container.decodeIfPresent( + UserVerificationRequirement.self, forKey: .userVerification) + + // requireResidentKey is ignored during decoding as it's derived from residentKey + // It's only included in encoding for backwards compatibility with WebAuthn Level 1 + } + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encodeIfPresent(authenticatorAttachment, forKey: .authenticatorAttachment) + try container.encodeIfPresent(residentKey, forKey: .residentKey) + try container.encodeIfPresent(userVerification, forKey: .userVerification) + + // requireResidentKey is included for backwards compatibility with WebAuthn Level 1 + // It should be true if and only if residentKey is set to .required + let requireResidentKey = residentKey == .required + try container.encode(requireResidentKey, forKey: .requireResidentKey) + } + + private enum CodingKeys: String, CodingKey { + case authenticatorAttachment + case residentKey + case userVerification + case requireResidentKey + } +} + diff --git a/Sources/WebAuthn/Ceremonies/Registration/PublicKeyCredentialCreationOptions.swift b/Sources/WebAuthn/Ceremonies/Registration/PublicKeyCredentialCreationOptions.swift index 539afcc..15d138e 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/PublicKeyCredentialCreationOptions.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/PublicKeyCredentialCreationOptions.swift @@ -53,6 +53,9 @@ public struct PublicKeyCredentialCreationOptions: Sendable { /// Sets the Relying Party's preference for attestation conveyance. At the time of writing only ``AttestationConveyancePreference/none`` is supported. public var attestation: AttestationConveyancePreference + /// A dictionary describing the Relying Party's requirements regarding authenticator attributes. + public var authenticatorSelection: AuthenticatorSelection? + /// Initialize a credential creation options dictionary directly. /// /// - Warning: Manually initializing options dictionaries can easily lead to insecure implementations of the WebAuthn protocol. Whenever possible, create an options dictionary using ``WebAuthnManager/beginRegistration(user:timeout:attestation:publicKeyCredentialParameters:)`` instead. @@ -64,13 +67,15 @@ public struct PublicKeyCredentialCreationOptions: Sendable { /// - publicKeyCredentialParameters: A list of key types and signature algorithms the Relying Party supports. Ordered from most preferred to least preferred. /// - timeout: A time, in seconds, that the caller is willing to wait for the call to complete. This is treated as a hint, and may be overridden by the client. /// - attestation: Sets the Relying Party's preference for attestation conveyance. At the time of writing only `none` is supported. + /// - authenticatorSelection: A dictionary describing the Relying Party's requirements regarding authenticator attributes. public init( challenge: [UInt8], user: PublicKeyCredentialUserEntity, relyingParty: PublicKeyCredentialRelyingPartyEntity, publicKeyCredentialParameters: [PublicKeyCredentialParameters], timeout: Duration?, - attestation: AttestationConveyancePreference + attestation: AttestationConveyancePreference, + authenticatorSelection: AuthenticatorSelection? = nil ) { self.challenge = challenge self.user = user @@ -78,6 +83,7 @@ public struct PublicKeyCredentialCreationOptions: Sendable { self.publicKeyCredentialParameters = publicKeyCredentialParameters self.timeout = timeout self.attestation = attestation + self.authenticatorSelection = authenticatorSelection } } @@ -93,6 +99,8 @@ extension PublicKeyCredentialCreationOptions: Codable { self.timeout = .milliseconds(timeout) } self.attestation = try values.decode(AttestationConveyancePreference.self, forKey: .attestation) + self.authenticatorSelection = try values.decodeIfPresent( + AuthenticatorSelection.self, forKey: .authenticatorSelection) } public func encode(to encoder: any Encoder) throws { @@ -104,6 +112,7 @@ extension PublicKeyCredentialCreationOptions: Codable { try container.encode(publicKeyCredentialParameters, forKey: .publicKeyCredentialParameters) try container.encodeIfPresent(timeout?.milliseconds, forKey: .timeout) try container.encode(attestation, forKey: .attestation) + try container.encodeIfPresent(authenticatorSelection, forKey: .authenticatorSelection) } private enum CodingKeys: String, CodingKey { @@ -113,6 +122,7 @@ extension PublicKeyCredentialCreationOptions: Codable { case publicKeyCredentialParameters = "pubKeyCredParams" case timeout case attestation + case authenticatorSelection } } diff --git a/Sources/WebAuthn/Ceremonies/Registration/ResidentKeyRequirement.swift b/Sources/WebAuthn/Ceremonies/Registration/ResidentKeyRequirement.swift new file mode 100644 index 0000000..f4d0c75 --- /dev/null +++ b/Sources/WebAuthn/Ceremonies/Registration/ResidentKeyRequirement.swift @@ -0,0 +1,40 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift WebAuthn open source project +// +// Copyright (c) 2022 the Swift WebAuthn project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation + +/// The Relying Party's requirements regarding whether the authenticator should create a client-side-resident public key credential source. +/// +/// - SeeAlso: [WebAuthn Level 3 Working Draft §5.4.6. Resident Key Requirement Enumeration](https://www.w3.org/TR/webauthn-3/#enum-residentKeyRequirement) +public struct ResidentKeyRequirement: UnreferencedStringEnumeration, Sendable { + public var rawValue: String + + public init(_ rawValue: String) { + self.rawValue = rawValue + } + + /// This value indicates the Relying Party requires a client-side-resident credential (i.e., a discoverable credential). + /// + /// If the authenticator cannot create a client-side-resident credential, it will return an error. + /// - SeeAlso: [WebAuthn Level 3 Working Draft §5.4.6. Resident Key Requirement Enumeration](https://www.w3.org/TR/webauthn-3/#dom-residentkeyrequirement-required) + public static let required: Self = "required" + + /// This value indicates the Relying Party strongly prefers a client-side-resident credential, but will accept a server-side credential. + /// - SeeAlso: [WebAuthn Level 3 Working Draft §5.4.6. Resident Key Requirement Enumeration](https://www.w3.org/TR/webauthn-3/#dom-residentkeyrequirement-preferred) + public static let preferred: Self = "preferred" + + /// This value indicates the Relying Party strongly prefers a server-side credential, but will accept a client-side-resident credential. + /// - SeeAlso: [WebAuthn Level 3 Working Draft §5.4.6. Resident Key Requirement Enumeration](https://www.w3.org/TR/webauthn-3/#dom-residentkeyrequirement-discouraged) + public static let discouraged: Self = "discouraged" +} + diff --git a/Sources/WebAuthn/WebAuthnManager.swift b/Sources/WebAuthn/WebAuthnManager.swift index feebb20..d1e05e3 100644 --- a/Sources/WebAuthn/WebAuthnManager.swift +++ b/Sources/WebAuthn/WebAuthnManager.swift @@ -57,12 +57,15 @@ public struct WebAuthnManager: Sendable { /// - attestation: The Relying Party's preference regarding attestation. Defaults to `.none`. /// - publicKeyCredentialParameters: A list of public key algorithms the Relying Party chooses to restrict /// support to. Defaults to all supported algorithms. + /// - authenticatorSelection: The Relying Party's authenticator selection criteria that should be communicated to the client when choosing an authenticator to use. + /// Defaults to `nil` (no requirements). /// - Returns: Registration options ready for the browser. public func beginRegistration( user: PublicKeyCredentialUserEntity, timeout: Duration? = .seconds(5*60), attestation: AttestationConveyancePreference = .none, - publicKeyCredentialParameters: [PublicKeyCredentialParameters] = .supported + publicKeyCredentialParameters: [PublicKeyCredentialParameters] = .supported, + authenticatorSelection: AuthenticatorSelection? = nil ) -> PublicKeyCredentialCreationOptions { let challenge = challengeGenerator.generate() @@ -72,7 +75,8 @@ public struct WebAuthnManager: Sendable { relyingParty: .init(id: configuration.relyingPartyID, name: configuration.relyingPartyName), publicKeyCredentialParameters: publicKeyCredentialParameters, timeout: timeout, - attestation: attestation + attestation: attestation, + authenticatorSelection: authenticatorSelection ) } diff --git a/Tests/WebAuthnTests/AuthenticatorSelectionTests.swift b/Tests/WebAuthnTests/AuthenticatorSelectionTests.swift new file mode 100644 index 0000000..0e5b7ce --- /dev/null +++ b/Tests/WebAuthnTests/AuthenticatorSelectionTests.swift @@ -0,0 +1,460 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift WebAuthn open source project +// +// Copyright (c) 2022 the Swift WebAuthn project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation +import Testing +@testable import WebAuthn + +struct AuthenticatorSelectionTests { + + // MARK: - ResidentKeyRequirement Tests + + @Test + func residentKeyRequirementValues() { + #expect(ResidentKeyRequirement.required.rawValue == "required") + #expect(ResidentKeyRequirement.preferred.rawValue == "preferred") + #expect(ResidentKeyRequirement.discouraged.rawValue == "discouraged") + } + + @Test + func residentKeyRequirementEncoding() throws { + let required = ResidentKeyRequirement.required + let preferred = ResidentKeyRequirement.preferred + let discouraged = ResidentKeyRequirement.discouraged + + let requiredJSON = try JSONEncoder().encode(required) + let preferredJSON = try JSONEncoder().encode(preferred) + let discouragedJSON = try JSONEncoder().encode(discouraged) + + let requiredString = String(data: requiredJSON, encoding: .utf8) + let preferredString = String(data: preferredJSON, encoding: .utf8) + let discouragedString = String(data: discouragedJSON, encoding: .utf8) + + #expect(requiredString == "\"required\"") + #expect(preferredString == "\"preferred\"") + #expect(discouragedString == "\"discouraged\"") + } + + @Test + func residentKeyRequirementDecoding() throws { + let requiredJSON = "\"required\"".data(using: .utf8)! + let preferredJSON = "\"preferred\"".data(using: .utf8)! + let discouragedJSON = "\"discouraged\"".data(using: .utf8)! + + let required = try JSONDecoder().decode(ResidentKeyRequirement.self, from: requiredJSON) + let preferred = try JSONDecoder().decode(ResidentKeyRequirement.self, from: preferredJSON) + let discouraged = try JSONDecoder().decode(ResidentKeyRequirement.self, from: discouragedJSON) + + #expect(required == .required) + #expect(preferred == .preferred) + #expect(discouraged == .discouraged) + } + + @Test + func residentKeyRequirementRoundTrip() throws { + let original = ResidentKeyRequirement.required + let json = try JSONEncoder().encode(original) + let decoded = try JSONDecoder().decode(ResidentKeyRequirement.self, from: json) + #expect(original == decoded) + } + + // MARK: - AuthenticatorSelection Tests + + @Test + func authenticatorSelectionInitialization() { + let selection = AuthenticatorSelection( + authenticatorAttachment: .platform, + residentKey: .required, + userVerification: .preferred + ) + + #expect(selection.authenticatorAttachment == .platform) + #expect(selection.residentKey == .required) + #expect(selection.userVerification == .preferred) + } + + @Test + func authenticatorSelectionWithNilValues() { + let selection = AuthenticatorSelection() + + #expect(selection.authenticatorAttachment == nil) + #expect(selection.residentKey == nil) + #expect(selection.userVerification == nil) + } + + @Test + func authenticatorSelectionEncoding() throws { + let selection = AuthenticatorSelection( + authenticatorAttachment: .platform, + residentKey: .required, + userVerification: .preferred + ) + + let encoder = JSONEncoder() + encoder.outputFormatting = .sortedKeys + let json = try encoder.encode(selection) + let jsonString = String(decoding: json, as: UTF8.self) + #expect( + jsonString + == #"{"authenticatorAttachment":"platform","requireResidentKey":true,"residentKey":"required","userVerification":"preferred"}"#) + } + + @Test + func authenticatorSelectionEncodingWithRequireResidentKeyTrue() throws { + let selection = AuthenticatorSelection( + residentKey: .required + ) + + let json = try JSONEncoder().encode(selection) + let jsonObject = try JSONSerialization.jsonObject(with: json) as! [String: Any] + + // When residentKey is .required, requireResidentKey should be true + #expect(jsonObject["residentKey"] as? String == "required") + #expect(jsonObject["requireResidentKey"] as? Bool == true) + } + + @Test + func authenticatorSelectionEncodingWithRequireResidentKeyFalse() throws { + let selection = AuthenticatorSelection( + residentKey: .preferred + ) + + let json = try JSONEncoder().encode(selection) + let jsonObject = try JSONSerialization.jsonObject(with: json) as! [String: Any] + + // When residentKey is not .required, requireResidentKey should be false + #expect(jsonObject["residentKey"] as? String == "preferred") + #expect(jsonObject["requireResidentKey"] as? Bool == false) + } + + @Test + func authenticatorSelectionEncodingWithRequireResidentKeyFalseWhenNil() throws { + let selection = AuthenticatorSelection() + + let json = try JSONEncoder().encode(selection) + let jsonObject = try JSONSerialization.jsonObject(with: json) as! [String: Any] + + // When residentKey is nil, requireResidentKey should be false + #expect(jsonObject["residentKey"] == nil) + #expect(jsonObject["requireResidentKey"] as? Bool == false) + } + + @Test + func authenticatorSelectionDecoding() throws { + let jsonString = """ + { + "authenticatorAttachment": "platform", + "residentKey": "required", + "userVerification": "preferred" + } + """ + let json = jsonString.data(using: .utf8)! + + let selection = try JSONDecoder().decode(AuthenticatorSelection.self, from: json) + + #expect(selection.authenticatorAttachment == .platform) + #expect(selection.residentKey == .required) + #expect(selection.userVerification == .preferred) + } + + @Test + func authenticatorSelectionDecodingWithRequireResidentKey() throws { + // requireResidentKey should be ignored during decoding - value comes from residentKey + let jsonString = """ + { + "authenticatorAttachment": "platform", + "residentKey": "required", + "userVerification": "preferred", + "requireResidentKey": true + } + """ + let json = jsonString.data(using: .utf8)! + + let selection = try JSONDecoder().decode(AuthenticatorSelection.self, from: json) + + #expect(selection.authenticatorAttachment == .platform) + #expect(selection.residentKey == .required) + #expect(selection.userVerification == .preferred) + } + + @Test + func authenticatorSelectionDecodingWithRequireResidentKeyMismatch() throws { + // requireResidentKey should be ignored even if it doesn't match residentKey + // The value should come from residentKey, not requireResidentKey + let jsonString = """ + { + "residentKey": "preferred", + "requireResidentKey": true + } + """ + let json = jsonString.data(using: .utf8)! + + let selection = try JSONDecoder().decode(AuthenticatorSelection.self, from: json) + + // Should decode based on residentKey, ignoring requireResidentKey + #expect(selection.residentKey == .preferred) + } + + @Test + func authenticatorSelectionDecodingWithoutRequireResidentKey() throws { + // Decoding should work fine without requireResidentKey present + let jsonString = """ + { + "authenticatorAttachment": "cross-platform", + "residentKey": "discouraged", + "userVerification": "required" + } + """ + let json = jsonString.data(using: .utf8)! + + let selection = try JSONDecoder().decode(AuthenticatorSelection.self, from: json) + + #expect(selection.authenticatorAttachment == .crossPlatform) + #expect(selection.residentKey == .discouraged) + #expect(selection.userVerification == .required) + } + + @Test + func authenticatorSelectionDecodingWithPartialFields() throws { + let jsonString = """ + { + "residentKey": "required" + } + """ + let json = jsonString.data(using: .utf8)! + + let selection = try JSONDecoder().decode(AuthenticatorSelection.self, from: json) + + #expect(selection.authenticatorAttachment == nil) + #expect(selection.residentKey == .required) + #expect(selection.userVerification == nil) + } + + @Test + func authenticatorSelectionRoundTrip() throws { + let original = AuthenticatorSelection( + authenticatorAttachment: .crossPlatform, + residentKey: .preferred, + userVerification: .required + ) + + let json = try JSONEncoder().encode(original) + let decoded = try JSONDecoder().decode(AuthenticatorSelection.self, from: json) + + #expect(decoded.authenticatorAttachment == original.authenticatorAttachment) + #expect(decoded.residentKey == original.residentKey) + #expect(decoded.userVerification == original.userVerification) + } + + // MARK: - PublicKeyCredentialCreationOptions with AuthenticatorSelection Tests + + @Test + func publicKeyCredentialCreationOptionsWithAuthenticatorSelection() { + let user = PublicKeyCredentialUserEntity.mock + let authenticatorSelection = AuthenticatorSelection( + residentKey: .required, + userVerification: .preferred + ) + + let options = PublicKeyCredentialCreationOptions( + challenge: [1, 2, 3], + user: user, + relyingParty: PublicKeyCredentialRelyingPartyEntity( + id: "example.com", + name: "Example" + ), + publicKeyCredentialParameters: [PublicKeyCredentialParameters(type: .publicKey, alg: .algES256)], + timeout: .seconds(60), + attestation: .none, + authenticatorSelection: authenticatorSelection + ) + + #expect(options.authenticatorSelection != nil) + #expect(options.authenticatorSelection?.residentKey == .required) + #expect(options.authenticatorSelection?.userVerification == .preferred) + } + + @Test + func publicKeyCredentialCreationOptionsWithoutAuthenticatorSelection() { + let user = PublicKeyCredentialUserEntity.mock + + let options = PublicKeyCredentialCreationOptions( + challenge: [1, 2, 3], + user: user, + relyingParty: PublicKeyCredentialRelyingPartyEntity( + id: "example.com", + name: "Example" + ), + publicKeyCredentialParameters: [PublicKeyCredentialParameters(type: .publicKey, alg: .algES256)], + timeout: .seconds(60), + attestation: .none + ) + + #expect(options.authenticatorSelection == nil) + } + + @Test + func publicKeyCredentialCreationOptionsEncodingWithAuthenticatorSelection() throws { + let user = PublicKeyCredentialUserEntity.mock + let authenticatorSelection = AuthenticatorSelection( + residentKey: .required, + userVerification: .preferred + ) + + let options = PublicKeyCredentialCreationOptions( + challenge: [1, 2, 3], + user: user, + relyingParty: PublicKeyCredentialRelyingPartyEntity( + id: "example.com", + name: "Example" + ), + publicKeyCredentialParameters: [PublicKeyCredentialParameters(type: .publicKey, alg: .algES256)], + timeout: .seconds(60), + attestation: .none, + authenticatorSelection: authenticatorSelection + ) + + let json = try JSONEncoder().encode(options) + let jsonString = String(data: json, encoding: .utf8)! + + // Verify authenticatorSelection is included + #expect(jsonString.contains("authenticatorSelection")) + #expect(jsonString.contains("residentKey")) + #expect(jsonString.contains("required")) + #expect(jsonString.contains("userVerification")) + #expect(jsonString.contains("preferred")) + // When residentKey is .required, requireResidentKey should be present and true + #expect(jsonString.contains("requireResidentKey")) + #expect(jsonString.contains("true")) + } + + @Test + func publicKeyCredentialCreationOptionsDecodingWithAuthenticatorSelection() throws { + let jsonString = """ + { + "challenge": "AQID", + "rp": { + "id": "example.com", + "name": "Example" + }, + "user": { + "id": "AQID", + "name": "John", + "displayName": "Johnny" + }, + "pubKeyCredParams": [{"type": "public-key", "alg": -7}], + "attestation": "none", + "authenticatorSelection": { + "residentKey": "required", + "userVerification": "preferred" + } + } + """ + let json = jsonString.data(using: .utf8)! + + let options = try JSONDecoder().decode(PublicKeyCredentialCreationOptions.self, from: json) + + #expect(options.authenticatorSelection != nil) + #expect(options.authenticatorSelection?.residentKey == .required) + #expect(options.authenticatorSelection?.userVerification == .preferred) + } + + @Test + func publicKeyCredentialCreationOptionsDecodingWithRequireResidentKey() throws { + // Should decode successfully with requireResidentKey present (backwards compatibility) + let jsonString = """ + { + "challenge": "AQID", + "rp": { + "id": "example.com", + "name": "Example" + }, + "user": { + "id": "AQID", + "name": "John", + "displayName": "Johnny" + }, + "pubKeyCredParams": [{"type": "public-key", "alg": -7}], + "attestation": "none", + "authenticatorSelection": { + "residentKey": "required", + "userVerification": "preferred", + "requireResidentKey": true + } + } + """ + let json = jsonString.data(using: .utf8)! + + let options = try JSONDecoder().decode(PublicKeyCredentialCreationOptions.self, from: json) + + #expect(options.authenticatorSelection != nil) + #expect(options.authenticatorSelection?.residentKey == .required) + #expect(options.authenticatorSelection?.userVerification == .preferred) + } + + // MARK: - WebAuthnManager.beginRegistration with AuthenticatorSelection Tests + + @Test + func beginRegistrationWithAuthenticatorSelection() { + let configuration = WebAuthnManager.Configuration( + relyingPartyID: "example.com", + relyingPartyName: "Example", + relyingPartyOrigin: "https://example.com" + ) + let challenge: [UInt8] = [1, 2, 3] + let webAuthnManager = WebAuthnManager( + configuration: configuration, + challengeGenerator: .mock(generate: challenge) + ) + + let user = PublicKeyCredentialUserEntity.mock + let authenticatorSelection = AuthenticatorSelection( + residentKey: .required, + userVerification: .preferred + ) + + let options = webAuthnManager.beginRegistration( + user: user, + authenticatorSelection: authenticatorSelection + ) + + #expect(options.authenticatorSelection != nil) + #expect(options.authenticatorSelection?.residentKey == .required) + #expect(options.authenticatorSelection?.userVerification == .preferred) + #expect(options.challenge == challenge) + #expect(options.user.id == user.id) + } + + @Test + func beginRegistrationWithoutAuthenticatorSelection() { + let configuration = WebAuthnManager.Configuration( + relyingPartyID: "example.com", + relyingPartyName: "Example", + relyingPartyOrigin: "https://example.com" + ) + let challenge: [UInt8] = [1, 2, 3] + let webAuthnManager = WebAuthnManager( + configuration: configuration, + challengeGenerator: .mock(generate: challenge) + ) + + let user = PublicKeyCredentialUserEntity.mock + + let options = webAuthnManager.beginRegistration(user: user) + + #expect(options.authenticatorSelection == nil) + #expect(options.challenge == challenge) + #expect(options.user.id == user.id) + } +} +