Implemented phase 5 for the POC prototype in the Website service.
This commit is contained in:
1 parent
89a7a7b8e5
commit
31d2d98d49
46 files changed
+4039
-162
No files matched your search
@@ -8,8 +8,8 @@ import WebAuthn
|
||||
/// Serves the passkey ceremonies behind the sign-in screen, as JSON for `js/passkey.js`.
|
||||
///
|
||||
/// One step both signs in and joins: an unknown email registers a passkey and creates its account together; anything else authenticates with a
|
||||
/// passkey registered earlier — a discoverable one when no email is given. A passkey is never added to an existing account: the prototype has
|
||||
/// no other proof of who is asking, so that would hand the account over.
|
||||
/// passkey registered earlier — a discoverable one when no email is given. A passkey is added to an existing account only from that account's
|
||||
/// own session: signed out there is no proof of who is asking, so it would hand the account over.
|
||||
///
|
||||
/// - Note: `Context` is the request context the routes are resolved against, and must match the context of the router the routes are added to.
|
||||
public struct PasskeyController<Context: RequestContext> {
|
||||
@@ -134,7 +134,35 @@ private extension PasskeyController {
|
||||
|
||||
user = joined
|
||||
|
||||
return try await signedIn(user, returningTo: answer.return)
|
||||
// A new account picks its display name before going on.
|
||||
return try await signedIn(user, returningTo: .Route.welcome(returningTo: .Route.safeReturn(answer.return)))
|
||||
case .addition(let userID):
|
||||
guard let owner = await sessions.user(for: request), owner.id == userID else {
|
||||
return Self.refusal(.unauthorized, "Sign in again to add a passkey.")
|
||||
}
|
||||
|
||||
let answer = try JSONDecoder().decode(Answer<RegistrationCredential>.self, from: body)
|
||||
let credential = try await manager.finishRegistration(
|
||||
challenge: ceremony.challenge,
|
||||
credentialCreationData: answer.credential,
|
||||
confirmCredentialIDNotRegisteredYet: { [accounts] in
|
||||
try await accounts.isUnregistered(credentialID: Self.base64URL($0))
|
||||
}
|
||||
)
|
||||
|
||||
try await accounts.addPasskey(
|
||||
to: owner,
|
||||
credentialID: Self.base64URL(credential.id),
|
||||
publicKey: credential.publicKey,
|
||||
signCount: credential.signCount
|
||||
)
|
||||
|
||||
// The bare path: from the account screen, one ending in a fragment would not reload it.
|
||||
var response = Self.json(.ok, #"{"location":"\#(String.Route.account)"}"#)
|
||||
|
||||
response.setCookie(PasskeyCeremonies.cookie("", isSecure: isSecure))
|
||||
|
||||
return response
|
||||
case .authentication:
|
||||
let answer = try JSONDecoder().decode(Answer<AuthenticationCredential>.self, from: body)
|
||||
let credentialID = Self.base64URL(answer.credential.id.asString())
|
||||
@@ -167,8 +195,23 @@ private extension PasskeyController {
|
||||
request: Request,
|
||||
context: Context
|
||||
) async throws -> Response {
|
||||
let typed = (try? await request.decode(as: Start.self, context: context))?.email?
|
||||
.trimmingCharacters(in: .whitespaces) ?? ""
|
||||
let start = try? await request.decode(as: Start.self, context: context)
|
||||
let typed = start?.email?.trimmingCharacters(in: .whitespaces) ?? ""
|
||||
|
||||
// From the account screen: another passkey for the account signed in.
|
||||
if start?.add == true {
|
||||
guard let owner = await sessions.user(for: request) else {
|
||||
return Self.refusal(.unauthorized, "Sign in again to add a passkey.")
|
||||
}
|
||||
|
||||
let options = manager.beginRegistration(user: .init(
|
||||
id: Array(owner.id.uuidString.utf8),
|
||||
name: owner.email,
|
||||
displayName: owner.displayName
|
||||
))
|
||||
|
||||
return try await begin(.addition(userID: owner.id), options: options, challenge: options.challenge, mode: "create")
|
||||
}
|
||||
|
||||
// No email: whichever passkey the browser holds for this site.
|
||||
guard !typed.isEmpty else {
|
||||
@@ -299,6 +342,7 @@ private extension PasskeyController {
|
||||
/// What the script posts to start a ceremony.
|
||||
private struct Start: Decodable {
|
||||
let email: String?
|
||||
let add: Bool?
|
||||
}
|
||||
|
||||
/// What the script posts to finish a ceremony: the browser's credential, and where to go afterwards.
|
||||
|
||||
Reference in new issue
Block a user