-
Notifications
You must be signed in to change notification settings - Fork 29
/
Copy pathWebAuthnManager.swift
198 lines (182 loc) · 9.65 KB
/
WebAuthnManager.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
//===----------------------------------------------------------------------===//
//
// 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
/// Main entrypoint for WebAuthn operations.
///
/// Use this struct to perform registration and authentication ceremonies.
///
/// Registration: To generate registration options, call `beginRegistration()`. Pass the resulting
/// ``PublicKeyCredentialCreationOptions`` to the client.
/// When the client has received the response from the authenticator, pass the response to
/// `finishRegistration()`.
///
/// Authentication: To generate authentication options, call `beginAuthentication()`. Pass the resulting
/// ``PublicKeyCredentialRequestOptions`` to the client.
/// When the client has received the response from the authenticator, pass the response to
/// `finishAuthentication()`.
public struct WebAuthnManager: Sendable {
private let configuration: Configuration
private let challengeGenerator: ChallengeGenerator
/// Create a new WebAuthnManager using the given configuration.
///
/// - Parameters:
/// - configuration: The configuration to use for this manager.
public init(configuration: Configuration) {
self.init(configuration: configuration, challengeGenerator: .live)
}
package init(configuration: Configuration, challengeGenerator: ChallengeGenerator) {
self.configuration = configuration
self.challengeGenerator = challengeGenerator
}
/// Generate a new set of registration data to be sent to the client.
///
/// This method will use the Relying Party information from the WebAuthnManager's configuration to create ``PublicKeyCredentialCreationOptions``
/// - Parameters:
/// - user: The user to register.
/// - timeout: How long the browser should give the user to choose an authenticator. This value
/// is a *hint* and may be ignored by the browser. Defaults to 300000 milliseconds (5 minutes).
/// - 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.
/// - Returns: Registration options ready for the browser.
public func beginRegistration(
user: PublicKeyCredentialUserEntity,
timeout: Duration? = .seconds(5*60),
attestation: AttestationConveyancePreference = .none,
publicKeyCredentialParameters: [PublicKeyCredentialParameters] = .supported
) -> PublicKeyCredentialCreationOptions {
let challenge = challengeGenerator.generate()
return PublicKeyCredentialCreationOptions(
challenge: challenge,
user: user,
relyingParty: .init(id: configuration.relyingPartyID, name: configuration.relyingPartyName),
publicKeyCredentialParameters: publicKeyCredentialParameters,
timeout: timeout,
attestation: attestation,
hints: [],
extensions: .init(credProps: true),
excludeCredentials: [],
authenticatorSelection: .init(residentKey: .preferred,
requireResidentKey: false,
userVerification: .preferred)
)
}
/// Take response from authenticator and client and verify credential against the user's credentials and
/// session data.
///
/// - Parameters:
/// - challenge: The challenge passed to the authenticator within the preceding registration options.
/// - credentialCreationData: The value returned from `navigator.credentials.create()`
/// - requireUserVerification: Whether or not to require that the authenticator verified the user.
/// - supportedPublicKeyAlgorithms: A list of public key algorithms the Relying Party chooses to restrict
/// support to. Defaults to all supported algorithms.
/// - pemRootCertificatesByFormat: A list of root certificates used for attestation verification.
/// If attestation verification is not required (default behavior) this parameter does nothing.
/// - confirmCredentialIDNotRegisteredYet: For a successful registration ceremony we need to verify that the
/// `credentialId`, generated by the authenticator, is not yet registered for any user. This is a good place to
/// handle that.
/// - Returns: A new `Credential` with information about the authenticator and registration
public func finishRegistration(
challenge: [UInt8],
credentialCreationData: RegistrationCredential,
requireUserVerification: Bool = false,
supportedPublicKeyAlgorithms: [PublicKeyCredentialParameters] = .supported,
pemRootCertificatesByFormat: [AttestationFormat: [Data]] = [:],
confirmCredentialIDNotRegisteredYet: (String) async throws -> Bool
) async throws -> Credential {
let parsedData = try ParsedCredentialCreationResponse(from: credentialCreationData)
let attestedCredentialData = try await parsedData.verify(
storedChallenge: challenge,
verifyUser: requireUserVerification,
relyingPartyID: configuration.relyingPartyID,
relyingPartyOrigin: configuration.relyingPartyOrigin,
supportedPublicKeyAlgorithms: supportedPublicKeyAlgorithms,
pemRootCertificatesByFormat: pemRootCertificatesByFormat
)
// TODO: Step 18. -> Verify client extensions
// Step 24.
guard try await confirmCredentialIDNotRegisteredYet(parsedData.id.asString()) else {
throw WebAuthnError.credentialIDAlreadyExists
}
// Step 25.
return Credential(
type: parsedData.type,
id: parsedData.id.urlDecoded.asString(),
publicKey: attestedCredentialData.publicKey,
signCount: parsedData.response.attestationObject.authenticatorData.counter,
backupEligible: parsedData.response.attestationObject.authenticatorData.flags.isBackupEligible,
isBackedUp: parsedData.response.attestationObject.authenticatorData.flags.isCurrentlyBackedUp,
attestationObject: parsedData.response.attestationObject,
attestationClientDataJSON: parsedData.response.clientData
)
}
/// Generate options for retrieving a credential via navigator.credentials.get()
///
/// - Parameters:
/// - timeout: How long the browser should give the user to choose an authenticator. This value
/// is a *hint* and may be ignored by the browser. Defaults to 60 seconds.
/// - allowCredentials: A list of credentials registered to the user.
/// - userVerification: The Relying Party's preference for the authenticator's enforcement of the
/// "user verified" flag.
/// - Returns: Authentication options ready for the browser.
public func beginAuthentication(
timeout: Duration? = .seconds(60),
allowCredentials: [PublicKeyCredentialDescriptor]? = nil,
userVerification: UserVerificationRequirement = .preferred
) -> PublicKeyCredentialRequestOptions {
let challenge = challengeGenerator.generate()
return PublicKeyCredentialRequestOptions(
challenge: challenge,
timeout: timeout,
relyingPartyID: configuration.relyingPartyID,
allowCredentials: allowCredentials,
userVerification: userVerification
)
}
/// Verify a response from navigator.credentials.get()
///
/// - Parameters:
/// - credential: The value returned from `navigator.credentials.get()`.
/// - expectedChallenge: The challenge passed to the authenticator within the preceding authentication options.
/// - credentialPublicKey: The public key for the credential's ID as provided in a preceding authenticator
/// registration ceremony.
/// - credentialCurrentSignCount: The current known number of times the authenticator was used.
/// - requireUserVerification: Whether or not to require that the authenticator verified the user.
/// - Returns: Information about the authenticator
public func finishAuthentication(
credential: AuthenticationCredential,
// clientExtensionResults: ,
expectedChallenge: [UInt8],
credentialPublicKey: [UInt8],
credentialCurrentSignCount: UInt32,
requireUserVerification: Bool = false
) throws -> VerifiedAuthentication {
guard credential.type == .publicKey
else { throw WebAuthnError.invalidAssertionCredentialType }
let parsedAssertion = try ParsedAuthenticatorAssertionResponse(from: credential.response)
try parsedAssertion.verify(
expectedChallenge: expectedChallenge,
relyingPartyOrigin: configuration.relyingPartyOrigin,
relyingPartyID: configuration.relyingPartyID,
requireUserVerification: requireUserVerification,
credentialPublicKey: credentialPublicKey,
credentialCurrentSignCount: credentialCurrentSignCount
)
return VerifiedAuthentication(
credentialID: credential.id,
newSignCount: parsedAssertion.authenticatorData.counter,
credentialDeviceType: parsedAssertion.authenticatorData.flags.deviceType,
credentialBackedUp: parsedAssertion.authenticatorData.flags.isCurrentlyBackedUp
)
}
}