Skip to content

Commit 57239a8

Browse files
committed
fix: enhance passkey handling with improved error messages and backend-only field settings
1 parent f594580 commit 57239a8

File tree

1 file changed

+56
-12
lines changed

1 file changed

+56
-12
lines changed

index.ts

Lines changed: 56 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { AdminForthPlugin, Filters } from "adminforth";
1+
import { AdminForthPlugin, Filters, suggestIfTypo } from "adminforth";
22
import type { AdminForthResource, AdminUser, IAdminForth, IHttpServer, IAdminForthAuth, BeforeLoginConfirmationFunction, IAdminForthHttpResponse } from "adminforth";
33
import twofactor from 'node-2fa';
44
import { PluginOptions } from "./types.js"
@@ -173,9 +173,36 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
173173
if (!this.options.passkeys.credentialIdFieldName) {
174174
throw new Error('Passkeys credentialIdFieldName is required');
175175
}
176+
177+
const credentialResource = adminforth.config.resources.find(r => r.resourceId === this.options.passkeys.credentialResourceID);
178+
const credentialIDField = credentialResource.columns.find(c => c.name === this.options.passkeys.credentialIdFieldName);
179+
if ( !credentialIDField ) {
180+
const similar = suggestIfTypo(credentialResource.columns.map(c => c.name), this.options.passkeys.credentialIdFieldName);
181+
throw new Error(
182+
`Passkeys credentialIdFieldName '${this.options.passkeys.credentialIdFieldName}' not found in resource '${this.options.passkeys.credentialResourceID}'. ${
183+
similar ? `Did you mean '${similar}'?` : ''
184+
}`
185+
);
186+
}
187+
credentialIDField.backendOnly = true;
188+
176189
if (!this.options.passkeys.credentialMetaFieldName) {
177190
throw new Error('Passkeys credentialMetaFieldName is required');
178191
}
192+
193+
const metaResource = adminforth.config.resources.find(r => r.resourceId === this.options.passkeys.credentialMetaFieldName);
194+
const metaField = credentialResource.columns.find(c => c.name === this.options.passkeys.credentialMetaFieldName);
195+
if ( !metaField ) {
196+
const similar = suggestIfTypo(metaResource.columns.map(c => c.name), this.options.passkeys.credentialMetaFieldName);
197+
throw new Error(
198+
`Passkeys credentialMetaFieldName '${this.options.passkeys.credentialMetaFieldName}' not found in resource '${this.options.passkeys.credentialMetaFieldName}'. ${
199+
similar ? `Did you mean '${similar}'?` : ''
200+
}`
201+
);
202+
}
203+
metaField.backendOnly = true;
204+
205+
179206
if (!this.options.passkeys.credentialUserIdFieldName) {
180207
throw new Error('Passkeys credentialUserIdFieldName is required');
181208
}
@@ -455,7 +482,7 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
455482
userVerification: this.options.passkeys?.settings.authenticatorSelection.userVerification || "required"
456483
},
457484
});
458-
const value = this.adminforth.auth.issueJWT({ "challenge": options.challenge, "user_id": adminUser.pk }, 'tempPasskeyChallenge', '10m');
485+
const value = this.adminforth.auth.issueJWT({ "challenge": options.challenge, "user_id": adminUser.pk }, 'registerTempPasskeyChallenge', '10m');
459486
this.adminforth.auth.setCustomCookie({response, payload: {name: "registerPasskeyTemporaryJWT", value: value, expiry: undefined, expirySeconds: 10 * 60, httpOnly: true}});
460487
return { ok: true, data: options };
461488
}
@@ -469,7 +496,7 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
469496
if (!passkeysCookies) {
470497
return { error: 'Passkey token is required' };
471498
}
472-
const decodedPasskeysCookies = await this.adminforth.auth.verify(passkeysCookies, 'tempPasskeyChallenge', false);
499+
const decodedPasskeysCookies = await this.adminforth.auth.verify(passkeysCookies, 'registerTempPasskeyChallenge', false);
473500
if (!decodedPasskeysCookies) {
474501
return { error: 'Invalid passkey token' };
475502
}
@@ -584,12 +611,12 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
584611
path: `/plugin/passkeys/deletePasskey`,
585612
noAuth: false,
586613
handler: async ({body, adminUser }) => {
587-
const passkeyId = body.passkeyId;
588-
if (!passkeyId) {
614+
const credentialID = body.passkeyId;
615+
if (!credentialID) {
589616
return { ok: false, error: 'Passkey ID is required' };
590617
}
591618

592-
const passkeyRecord = await this.adminforth.resource(this.options.passkeys.credentialResourceID).get([Filters.EQ(this.options.passkeys.credentialIdFieldName, passkeyId), Filters.EQ(this.options.passkeys.credentialUserIdFieldName, adminUser.pk)]);
619+
const passkeyRecord = await this.adminforth.resource(this.options.passkeys.credentialResourceID).get([Filters.EQ(this.options.passkeys.credentialIdFieldName, credentialID), Filters.EQ(this.options.passkeys.credentialUserIdFieldName, adminUser.pk)]);
593620
if (!passkeyRecord) {
594621
return { ok: false, error: 'Passkey not found' };
595622
}
@@ -598,9 +625,18 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
598625
if (!credResource) {
599626
throw new Error('Credential resource not found.');
600627
}
628+
const credResourcePKColumn = credResource.columns.find(c => c.primaryKey);
629+
if (!credResourcePKColumn) {
630+
throw new Error('Credential resource primary key not found.');
631+
}
632+
const credResourcePKName = credResourcePKColumn.name;
633+
const passkeyRecord = await this.adminforth.resource(this.options.passkeys.credentialResourceID).get([Filters.EQ(this.options.passkeys.credentialIdFieldName, credentialID), Filters.EQ(this.options.passkeys.credentialUserIdFieldName, adminUser.pk)]);
634+
if (!passkeyRecord) {
635+
return { ok: false, error: 'Passkey not found' };
636+
}
601637
await this.adminforth.deleteResourceRecord({
602638
resource: credResource,
603-
recordId: passkeyId,
639+
recordId: passkeyRecord[credResourcePKName],
604640
record: passkeyRecord,
605641
adminUser: adminUser,
606642
});
@@ -616,16 +652,24 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
616652
path: `/plugin/passkeys/renamePasskey`,
617653
noAuth: false,
618654
handler: async ({body, adminUser }) => {
619-
const passkeyId = body.passkeyId;
655+
const credentialID = body.passkeyId;
620656
const newName = body.newName;
621-
if (!passkeyId) {
657+
if (!credentialID) {
622658
return { ok: false, error: 'Passkey ID is required' };
623659
}
624660
if (!newName) {
625661
return { ok: false, error: 'New name is required' };
626662
}
627-
628-
const passkeyRecord = await this.adminforth.resource(this.options.passkeys.credentialResourceID).get([Filters.EQ(this.options.passkeys.credentialIdFieldName, passkeyId), Filters.EQ(this.options.passkeys.credentialUserIdFieldName, adminUser.pk)]);
663+
const credResource = this.adminforth.config.resources.find(r => r.resourceId === this.options.passkeys.credentialResourceID);
664+
if (!credResource) {
665+
throw new Error('Credential resource not found.');
666+
}
667+
const credResourcePKColumn = credResource.columns.find(c => c.primaryKey);
668+
if (!credResourcePKColumn) {
669+
throw new Error('Credential resource primary key not found.');
670+
}
671+
const credResourcePKName = credResourcePKColumn.name;
672+
const passkeyRecord = await this.adminforth.resource(this.options.passkeys.credentialResourceID).get([Filters.EQ(this.options.passkeys.credentialIdFieldName, credentialID), Filters.EQ(this.options.passkeys.credentialUserIdFieldName, adminUser.pk)]);
629673
if (!passkeyRecord) {
630674
return { ok: false, error: 'Passkey not found' };
631675
}
@@ -639,7 +683,7 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
639683
}
640684
await this.adminforth.updateResourceRecord({
641685
resource: credResource,
642-
recordId: passkeyId,
686+
recordId: passkeyRecord[credResourcePKName],
643687
oldRecord: passkeyRecord,
644688
record: newRecord,
645689
adminUser: adminUser

0 commit comments

Comments
 (0)