Skip to content

Commit c2ccdb6

Browse files
tommoorclaude
andauthored
fix: Prevent registration of duplicate passkeys on the same device (outline#11870)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 793804c commit c2ccdb6

File tree

1 file changed

+22
-2
lines changed

1 file changed

+22
-2
lines changed

plugins/passkeys/server/auth/passkeys.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,12 +95,20 @@ router.post(
9595
const { user } = ctx.state.auth;
9696
authorize(user, "createUserPasskey", user.team);
9797

98+
// Fetch existing passkeys to exclude them from registration
99+
const existingPasskeys = await UserPasskey.findAll({
100+
where: { userId: user.id },
101+
});
102+
98103
const options = await generateRegistrationOptions({
99104
rpName,
100105
rpID: getRpID(ctx),
101106
userID: isoBase64URL.toBuffer(user.id),
102107
userName: user.email || user.name,
103-
// Don't exclude credentials, so we can detect if one is already registered (optional)
108+
excludeCredentials: existingPasskeys.map((pk) => ({
109+
id: pk.credentialId,
110+
transports: pk.transports as AuthenticatorTransportFuture[],
111+
})),
104112
authenticatorSelection: {
105113
residentKey: "preferred",
106114
userVerification: "preferred",
@@ -154,6 +162,7 @@ router.post(
154162
}
155163

156164
const { verified, registrationInfo } = verification;
165+
const ZERO_AAGUID = "00000000-0000-0000-0000-000000000000";
157166

158167
if (verified && registrationInfo) {
159168
const { credential, aaguid } = registrationInfo;
@@ -166,7 +175,7 @@ router.post(
166175
const userAgent = ctx.request.get("user-agent");
167176
const transports = body.response.transports || [];
168177

169-
// Check if already exists
178+
// Check if already exists by credential ID
170179
const existing = await UserPasskey.findOne({
171180
where: { credentialId: credentialIdBase64 },
172181
});
@@ -183,6 +192,17 @@ router.post(
183192
aaguid,
184193
});
185194
} else {
195+
// Check if user already has a passkey from the same authenticator
196+
if (aaguid && aaguid !== ZERO_AAGUID) {
197+
const duplicateDevice = await UserPasskey.findOne({
198+
where: { userId: user.id, aaguid },
199+
});
200+
201+
if (duplicateDevice) {
202+
throw ValidationError("You already have a passkey on this device");
203+
}
204+
}
205+
186206
await UserPasskey.createWithCtx(ctx, {
187207
userId: user.id,
188208
credentialId: credentialIdBase64,

0 commit comments

Comments
 (0)