Implemented phase 6 for the POC prototype in the Website service.
This commit is contained in:
1 parent
256d22df40
commit
f2f445827b
40 files changed
+1906
-37
No files matched your search
@@ -28,6 +28,9 @@ public struct PasskeyController<Context: RequestContext> {
|
||||
/// The origin the sign-in screen is served at, which passkeys are bound to.
|
||||
private let origin: String
|
||||
|
||||
/// The one-time recovery links a recovery ceremony answers to.
|
||||
private let recoveryLinks: RecoveryLinks
|
||||
|
||||
/// The store sessions are started in.
|
||||
private let sessions: SessionStore
|
||||
|
||||
@@ -40,12 +43,15 @@ public struct PasskeyController<Context: RequestContext> {
|
||||
/// - accounts: the repository accounts and their passkeys are read and created through.
|
||||
/// - sessions: the store sessions are started in.
|
||||
/// - ceremonies: the ceremonies under way; a set of its own by default.
|
||||
/// - recoveryLinks: the one-time recovery links a recovery ceremony answers to; a set of its own by default.
|
||||
public init(
|
||||
origin: String,
|
||||
accounts: AccountRepository,
|
||||
sessions: SessionStore,
|
||||
ceremonies: PasskeyCeremonies = .init()
|
||||
ceremonies: PasskeyCeremonies = .init(),
|
||||
recoveryLinks: RecoveryLinks = .init()
|
||||
) {
|
||||
self.recoveryLinks = recoveryLinks
|
||||
self.accounts = accounts
|
||||
self.ceremonies = ceremonies
|
||||
self.isSecure = origin.hasPrefix("https://")
|
||||
@@ -136,6 +142,29 @@ private extension PasskeyController {
|
||||
|
||||
// A new account picks its display name before going on.
|
||||
return try await signedIn(user, returningTo: .Route.welcome(returningTo: .Route.safeReturn(answer.return)))
|
||||
case .recovery(let token):
|
||||
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))
|
||||
}
|
||||
)
|
||||
|
||||
// Used up only now: a prompt cancelled leaves the link good for another try.
|
||||
guard let owner = await recoveryLinks.redeem(token) else {
|
||||
return Self.refusal(.gone, "That recovery link has run out. Ask for a new one.")
|
||||
}
|
||||
|
||||
try await accounts.replacePasskeys(
|
||||
of: owner,
|
||||
credentialID: Self.base64URL(credential.id),
|
||||
publicKey: credential.publicKey,
|
||||
signCount: credential.signCount
|
||||
)
|
||||
|
||||
return try await signedIn(owner, returningTo: .Route.account)
|
||||
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.")
|
||||
@@ -198,6 +227,21 @@ private extension PasskeyController {
|
||||
let start = try? await request.decode(as: Start.self, context: context)
|
||||
let typed = start?.email?.trimmingCharacters(in: .whitespaces) ?? ""
|
||||
|
||||
// From a recovery link: a new passkey, replacing the account's others.
|
||||
if let token = start?.recover {
|
||||
guard let owner = await recoveryLinks.user(for: token) else {
|
||||
return Self.refusal(.gone, "That recovery link has run out. Ask for a new one.")
|
||||
}
|
||||
|
||||
let options = manager.beginRegistration(user: .init(
|
||||
id: Array(UUID().uuidString.utf8),
|
||||
name: owner.email,
|
||||
displayName: owner.displayName
|
||||
))
|
||||
|
||||
return try await begin(.recovery(token: token), options: options, challenge: options.challenge, mode: "create")
|
||||
}
|
||||
|
||||
// From the account screen: another passkey for the account signed in.
|
||||
if start?.add == true {
|
||||
guard let owner = await sessions.user(for: request) else {
|
||||
@@ -343,6 +387,7 @@ private extension PasskeyController {
|
||||
private struct Start: Decodable {
|
||||
let email: String?
|
||||
let add: Bool?
|
||||
let recover: String?
|
||||
}
|
||||
|
||||
/// What the script posts to finish a ceremony: the browser's credential, and where to go afterwards.
|
||||
|
||||
Reference in new issue
Block a user