Skip to content

Commit 52f5a3e

Browse files
Added basic outline for client authentication
1 parent 233870a commit 52f5a3e

File tree

3 files changed

+113
-6
lines changed

3 files changed

+113
-6
lines changed

Sources/WebAuthn/Authenticators/Protocol/AssertionAuthenticationRequest.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,17 @@
1212
//
1313
//===----------------------------------------------------------------------===//
1414

15-
import Crypto
15+
@preconcurrency import Crypto
1616

17-
public struct AssertionAuthenticationRequest {
17+
public struct AssertionAuthenticationRequest: Sendable {
1818
public var options: PublicKeyCredentialRequestOptions
1919
public var clientDataHash: SHA256Digest
2020
public var attemptAuthentication: Callback
2121

2222
init(
2323
options: PublicKeyCredentialRequestOptions,
2424
clientDataHash: SHA256Digest,
25-
attemptAuthentication: @escaping (_ assertionResults: Results) async throws -> ()
25+
attemptAuthentication: @Sendable @escaping (_ assertionResults: Results) async throws -> ()
2626
) {
2727
self.options = options
2828
self.clientDataHash = clientDataHash
@@ -31,9 +31,9 @@ public struct AssertionAuthenticationRequest {
3131
}
3232

3333
extension AssertionAuthenticationRequest {
34-
public struct Callback {
34+
public struct Callback: Sendable {
3535
/// The internal callback the attestation should call.
36-
var callback: (_ assertionResults: Results) async throws -> ()
36+
var callback: @Sendable (_ assertionResults: Results) async throws -> ()
3737

3838
/// Submit the results of asserting a user's authentication request.
3939
///

Sources/WebAuthn/Authenticators/Protocol/AuthenticatorCredentialSourceIdentifier.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
import Foundation
1616

17-
public protocol AuthenticatorCredentialSourceIdentifier: Hashable {
17+
public protocol AuthenticatorCredentialSourceIdentifier: Hashable, Sendable {
1818
init?(bytes: some BidirectionalCollection<UInt8>)
1919
var bytes: [UInt8] { get }
2020
}

Sources/WebAuthn/WebAuthnClient.swift

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import Crypto
2020
/// - Important: Unless you specifically need to implement a custom WebAuthn client, it is vastly preferable to reach for the built-in [AuthenticationServices](https://developer.apple.com/documentation/authenticationservices) framework instead, which provides out-of-the-box support for a user's [Passkey](https://developer.apple.com/documentation/authenticationservices/public-private_key_authentication/supporting_passkeys). However, this is not always possible or preferrable to use this credential, especially when you want to implement silent account creation, and wish to build it off of WebAuthn. For those cases, `WebAuthnClient` is available.
2121
///
2222
/// Registration: To create a registration credential, first ask the relying party (aka the server) for ``PublicKeyCredentialCreationOptions``, then pass those to ``createRegistrationCredential(options:minTimeout:maxTimeout:origin:supportedPublicKeyCredentialParameters:attestRegistration:)`` along with a closure that can generate credentials from configured ``AuthenticatorProtocol`` types such as ``KeyPairAuthenticator`` by passing the provided ``AttestationRegistration`` to ``AuthenticatorProtocol/makeCredentials(with:)``, making sure to persist the resulting ``AuthenticatorProtocol/CredentialSource`` in some way. Finally, pass the resulting ``RegistrationCredential`` back to the relying party to finish registration.
23+
/// Authentication: To retrieve an authentication credential, first ask the relying party (aka the server) for ``PublicKeyCredentialRequestOptions``, then pass those to ``getAuthenticationCredential(options:minTimeout:maxTimeout:origin:assertAuthentication:)`` along with a closure that can validate credentials from configured ``AuthenticatorProtocol`` types such as ``KeyPairAuthenticator`` by passing the provided ``AssertionAuthentication`` to ``AuthenticatorProtocol/validateCredentials(with:)``, making sure to persist the resulting ``AuthenticatorProtocol/CredentialSource`` in some way. Finally, pass the resulting ``AuthenticationCredential`` back to the relying party to finish registration.
24+
///
2325
public struct WebAuthnClient {
2426
public init() {}
2527

@@ -287,6 +289,24 @@ public struct WebAuthnClient {
287289
throw error
288290
}
289291
}
292+
293+
public func assertAuthenticationCredential(
294+
options: PublicKeyCredentialRequestOptions,
295+
/// Recommended Range: https://w3c.github.io/webauthn/#recommended-range-and-default-for-a-webauthn-ceremony-timeout
296+
minTimeout: Duration = .seconds(300),
297+
maxTimeout: Duration = .seconds(600),
298+
origin: String,
299+
// mediation: ,
300+
assertAuthentication: (_ authentication: AssertionAuthenticationRequest) async throws -> ()
301+
) async throws -> AuthenticationCredential {
302+
/*
303+
1. Perform setup and massage inputs
304+
2. Prepare callback for assertion that an authenticator can call
305+
3. Have authenticator validate and sign a provided credential that matches it, calling the authentication callback
306+
4. Prepare final deliverable and cancel in-progress authenticators
307+
*/
308+
throw WebAuthnError.unsupported
309+
}
290310
}
291311

292312
// MARK: Convenience Registration and Authentication
@@ -365,4 +385,91 @@ extension WebAuthnClient {
365385

366386
return (registrationCredential, credentialSources)
367387
}
388+
389+
@inlinable
390+
public func assertAuthenticationCredential<Authenticator: AuthenticatorProtocol>(
391+
options: PublicKeyCredentialRequestOptions,
392+
/// Recommended Range: https://w3c.github.io/webauthn/#recommended-range-and-default-for-a-webauthn-ceremony-timeout
393+
minTimeout: Duration = .seconds(300),
394+
maxTimeout: Duration = .seconds(600),
395+
origin: String,
396+
// mediation: ,
397+
authenticator: Authenticator,
398+
credentialStore: CredentialStore<Authenticator>
399+
) async throws -> (
400+
authenticationCredential: AuthenticationCredential,
401+
updatedCredentialSource: Authenticator.CredentialSource
402+
) {
403+
var credentialSource: Authenticator.CredentialSource?
404+
let authenticationCredential = try await assertAuthenticationCredential(
405+
options: options,
406+
minTimeout: minTimeout,
407+
maxTimeout: maxTimeout,
408+
origin: origin
409+
) { authentication in
410+
credentialSource = try await authenticator.assertCredentials(
411+
authenticationRequest: authentication,
412+
credentials: credentialStore
413+
)
414+
}
415+
416+
guard let credentialSource
417+
else { throw WebAuthnError.missingCredentialSourceDespiteSuccess }
418+
419+
return (authenticationCredential, credentialSource)
420+
}
421+
422+
@inlinable
423+
public func assertAuthenticationCredential<each Authenticator: AuthenticatorProtocol & Sendable>(
424+
options: PublicKeyCredentialRequestOptions,
425+
/// Recommended Range: https://w3c.github.io/webauthn/#recommended-range-and-default-for-a-webauthn-ceremony-timeout
426+
minTimeout: Duration = .seconds(300),
427+
maxTimeout: Duration = .seconds(600),
428+
origin: String,
429+
// mediation: ,
430+
authenticators: repeat each Authenticator,
431+
credentialStores: repeat CredentialStore<(each Authenticator)>
432+
) async throws -> (
433+
authenticationCredential: AuthenticationCredential,
434+
updatedCredentialSources: (repeat Result<(each Authenticator).CredentialSource, Error>)
435+
) {
436+
/// Wrapper function since `repeat` doesn't currently support complex expressions
437+
@Sendable func authenticate<LocalAuthenticator: AuthenticatorProtocol & Sendable>(
438+
authenticator: LocalAuthenticator,
439+
authentication: AssertionAuthenticationRequest,
440+
credentials: CredentialStore<LocalAuthenticator>
441+
) -> Task<LocalAuthenticator.CredentialSource, Error> {
442+
Task {
443+
try await authenticator.assertCredentials(
444+
authenticationRequest: authentication,
445+
credentials: credentials
446+
)
447+
}
448+
}
449+
450+
var credentialSources: (repeat Result<(each Authenticator).CredentialSource, Error>)?
451+
let authenticationCredential = try await assertAuthenticationCredential(
452+
options: options,
453+
minTimeout: minTimeout,
454+
maxTimeout: maxTimeout,
455+
origin: origin
456+
) { authentication in
457+
/// Run each authenticator in parallel as child tasks, so we can automatically propagate cancellation to each of them should it occur.
458+
let tasks = (repeat authenticate(
459+
authenticator: each authenticators,
460+
authentication: authentication,
461+
credentials: each credentialStores
462+
))
463+
await withTaskCancellationHandler {
464+
credentialSources = (repeat await (each tasks).result)
465+
} onCancel: {
466+
repeat (each tasks).cancel()
467+
}
468+
}
469+
470+
guard let credentialSources
471+
else { throw WebAuthnError.missingCredentialSourceDespiteSuccess }
472+
473+
return (authenticationCredential, credentialSources)
474+
}
368475
}

0 commit comments

Comments
 (0)