diff --git a/.changeset/brave-hounds-verify.md b/.changeset/brave-hounds-verify.md new file mode 100644 index 000000000..ffe5154c9 --- /dev/null +++ b/.changeset/brave-hounds-verify.md @@ -0,0 +1,5 @@ +--- +"@exactly/server": patch +--- + +🐛 fix multi-device passkey auth diff --git a/.changeset/sharp-webs-eat.md b/.changeset/sharp-webs-eat.md new file mode 100644 index 000000000..65aae4525 --- /dev/null +++ b/.changeset/sharp-webs-eat.md @@ -0,0 +1,5 @@ +--- +"@exactly/server": patch +--- + +🗃️ drop passkey counter column diff --git a/server/api/auth/authentication.ts b/server/api/auth/authentication.ts index 04a777ff0..3be05e28a 100644 --- a/server/api/auth/authentication.ts +++ b/server/api/auth/authentication.ts @@ -227,7 +227,7 @@ When called with an Ethereum address as \`credentialId\`, this endpoint creates describeRoute({ summary: "Authenticate", description: ` -Authenticates a user using a WebAuthn credential. This endpoint verifies the authentication response from the client, updates the credential counter, and sets a signed cookie for the authenticated session. +Authenticates a user using a WebAuthn credential. This endpoint verifies the authentication response from the client and sets a signed cookie for the authenticated session. **SIWE Authentication** @@ -309,7 +309,7 @@ Submit the signed SIWE message to prove ownership of an Ethereum address. The se if (!sessionId) return c.json({ code: "bad session" }, 400); const [credential, challenge] = await Promise.all([ database.query.credentials.findFirst({ - columns: { publicKey: true, account: true, factory: true, transports: true, counter: true }, + columns: { publicKey: true, account: true, factory: true, transports: true }, where: eq(credentials.id, assertion.id), }), redis.getdel(sessionId), @@ -347,7 +347,6 @@ Submit the signed SIWE message to prove ownership of an Ethereum address. The se } setUser({ id: parse(Address, credential.account) }); - let newCounter: number | undefined; try { switch (assertion.method) { case "siwe": { @@ -374,13 +373,12 @@ Submit the signed SIWE message to prove ownership of an Ethereum address. The se id: assertion.id, publicKey: credential.publicKey, transports: (credential.transports as AuthenticatorTransportFuture[] | undefined) ?? undefined, - counter: credential.counter, + counter: 0, }, }); if (!verified || authenticationInfo.credentialID !== assertion.id) { return c.json({ code: "bad authentication", legacy: "bad authentication" }, 400); } - newCounter = authenticationInfo.newCounter; } } } catch (error) { @@ -398,7 +396,6 @@ Submit the signed SIWE message to prove ownership of an Ethereum address. The se ? { sameSite: "lax", secure: false } : { domain, sameSite: "none", secure: true, partitioned: true }), }), - newCounter && database.update(credentials).set({ counter: newCounter }).where(eq(credentials.id, assertion.id)), ]); return c.json( diff --git a/server/api/card.ts b/server/api/card.ts index 82745d040..e32c836f7 100644 --- a/server/api/card.ts +++ b/server/api/card.ts @@ -677,15 +677,7 @@ async function encryptPIN(pin: string) { return mutex .runExclusive(async () => { const credential = await database.query.credentials.findFirst({ - columns: { - account: true, - counter: true, - factory: true, - pandaId: true, - publicKey: true, - source: true, - transports: true, - }, + columns: { account: true, factory: true, pandaId: true, publicKey: true, source: true, transports: true }, where: eq(credentials.id, credentialId), with: { cards: { @@ -776,7 +768,6 @@ async function encryptPIN(pin: string) { credential: { publicKey: { type: "Buffer", data: [...credential.publicKey] }, transports: credential.transports, - counter: credential.counter, }, assertion: patch.assertion, factory: credential.factory, diff --git a/server/database/schema.ts b/server/database/schema.ts index 68684f87d..38971f137 100644 --- a/server/database/schema.ts +++ b/server/database/schema.ts @@ -31,7 +31,6 @@ export const credentials = pgTable( factory: text("factory").notNull(), account: text("account").notNull(), transports: text("transports").array(), - counter: integer("counter").notNull().default(0), kycId: text("kyc_id"), pandaId: text("panda_id"), bridgeId: text("bridge_id"), diff --git a/server/test/api/auth.test.ts b/server/test/api/auth.test.ts index 210549302..656c754dd 100644 --- a/server/test/api/auth.test.ts +++ b/server/test/api/auth.test.ts @@ -37,7 +37,6 @@ describe("authentication", () => { publicKey: new Uint8Array(), account: zeroAddress, factory: parse(Address, inject("ExaAccountFactory")), - counter: 0, transports: [], }, ]); @@ -198,7 +197,7 @@ describe("authentication", () => { it("consumes challenge after unverified authentication response to prevent replay", async () => { vi.mocked(verifyAuthenticationResponse).mockResolvedValueOnce({ verified: false, - authenticationInfo: { credentialID: "dGVzdC1jcmVkLWlk", newCounter: 1 }, + authenticationInfo: { credentialID: "dGVzdC1jcmVkLWlk" }, } as Awaited>); const firstResponse = await appClient.index.$post( @@ -237,7 +236,7 @@ describe("authentication", () => { it("consumes challenge after mismatched authentication credential id to prevent replay", async () => { vi.mocked(verifyAuthenticationResponse).mockResolvedValueOnce({ verified: true, - authenticationInfo: { credentialID: "another-credential", newCounter: 1 }, + authenticationInfo: { credentialID: "another-credential" }, } as Awaited>); const firstResponse = await appClient.index.$post( @@ -429,7 +428,6 @@ describe("registration", () => { credential: { id: "another-credential", publicKey: new Uint8Array(65), - counter: 0, transports: ["internal"], }, credentialDeviceType: "multiDevice", @@ -452,7 +450,6 @@ describe("registration", () => { credential: { id: "dGVzdC1jcmVkLWlk2", publicKey: new Uint8Array(65), - counter: 0, transports: ["internal"], }, credentialDeviceType: "singleDevice", @@ -581,16 +578,16 @@ vi.mock("@simplewebauthn/server", async (importOriginal) => { return { ...actual, verifyAuthenticationResponse: vi - .fn<() => Promise<{ authenticationInfo: { credentialID: string; newCounter: number }; verified: boolean }>>() + .fn<() => Promise<{ authenticationInfo: { credentialID: string }; verified: boolean }>>() .mockResolvedValue({ verified: true, - authenticationInfo: { credentialID: "dGVzdC1jcmVkLWlk", newCounter: 1 }, + authenticationInfo: { credentialID: "dGVzdC1jcmVkLWlk" }, }), verifyRegistrationResponse: vi .fn< (options: { response: { id: string } }) => Promise<{ registrationInfo: { - credential: { counter: number; id: string; publicKey: Uint8Array; transports: string[] }; + credential: { id: string; publicKey: Uint8Array; transports: string[] }; credentialDeviceType: string; }; verified: boolean; @@ -603,7 +600,6 @@ vi.mock("@simplewebauthn/server", async (importOriginal) => { credential: { id: options.response.id, publicKey: new Uint8Array(65), - counter: 0, transports: ["internal"], }, credentialDeviceType: "multiDevice", diff --git a/server/test/api/card.test.ts b/server/test/api/card.test.ts index 18b70552a..972280c36 100644 --- a/server/test/api/card.test.ts +++ b/server/test/api/card.test.ts @@ -1477,7 +1477,6 @@ describe("authenticated", () => { factory, pandaId: "webauthn-verify-panda", transports: ["internal"], - counter: 5, }); await database.insert(cards).values({ id: "webauthn-verify-card", credentialId, lastFour: "3377" }); const verifySpy = vi.spyOn(panda, "verify").mockResolvedValueOnce({}); @@ -1496,7 +1495,6 @@ describe("authenticated", () => { credential: { publicKey: { type: "Buffer", data: [1, 2, 3] }, transports: ["internal"], - counter: 5, }, assertion, factory, @@ -1514,7 +1512,6 @@ describe("authenticated", () => { account, factory, pandaId: "webauthn-null-panda", - counter: 0, }); await database.insert(cards).values({ id: "webauthn-null-card", credentialId, lastFour: "2020" }); const verifySpy = vi.spyOn(panda, "verify").mockResolvedValueOnce({}); @@ -1530,7 +1527,7 @@ describe("authenticated", () => { await expect(response.json()).resolves.toStrictEqual({ verification: "OK" }); expect(verifySpy).toHaveBeenCalledWith("webauthn-null-panda", { authType: "webauthn", - credential: { publicKey: { type: "Buffer", data: [9, 8, 7] }, transports: null, counter: 0 }, + credential: { publicKey: { type: "Buffer", data: [9, 8, 7] }, transports: null }, assertion, factory, statement, @@ -1570,7 +1567,6 @@ describe("authenticated", () => { factory, pandaId: "webauthn-panda-401-panda", transports: ["internal"], - counter: 0, }); await database.insert(cards).values({ id: "webauthn-panda-401-card", credentialId, lastFour: "4141" }); const verifySpy = vi @@ -1587,7 +1583,7 @@ describe("authenticated", () => { await expect(response.json()).resolves.toStrictEqual({ code: "bad signature" }); expect(verifySpy).toHaveBeenCalledWith("webauthn-panda-401-panda", { authType: "webauthn", - credential: { publicKey: { type: "Buffer", data: [1, 2, 3] }, transports: ["internal"], counter: 0 }, + credential: { publicKey: { type: "Buffer", data: [1, 2, 3] }, transports: ["internal"] }, assertion, factory, statement: `I authorize the account ${checksumAddress(account)} to be linked with the card ending in 4141 for my user (webauthn-panda-401-panda)`, @@ -1605,7 +1601,6 @@ describe("authenticated", () => { factory, pandaId: "webauthn-panda-503-panda", transports: ["internal"], - counter: 0, }); await database.insert(cards).values({ id: "webauthn-panda-503-card", credentialId, lastFour: "5151" }); const verifySpy = vi @@ -1621,7 +1616,7 @@ describe("authenticated", () => { expect(response.status).toBe(500); expect(verifySpy).toHaveBeenCalledWith("webauthn-panda-503-panda", { authType: "webauthn", - credential: { publicKey: { type: "Buffer", data: [4, 5, 6] }, transports: ["internal"], counter: 0 }, + credential: { publicKey: { type: "Buffer", data: [4, 5, 6] }, transports: ["internal"] }, assertion, factory, statement: `I authorize the account ${checksumAddress(account)} to be linked with the card ending in 5151 for my user (webauthn-panda-503-panda)`, diff --git a/server/utils/createCredential.ts b/server/utils/createCredential.ts index 9d6652ebe..828ec78a4 100644 --- a/server/utils/createCredential.ts +++ b/server/utils/createCredential.ts @@ -41,7 +41,6 @@ export default async function createCredential( publicKey, factory: exaAccountFactoryAddress, transports: options?.webauthn?.transports, - counter: options?.webauthn?.counter, source: options?.source, }, ]); diff --git a/server/utils/panda.ts b/server/utils/panda.ts index 7871438a0..e1d402a5f 100644 --- a/server/utils/panda.ts +++ b/server/utils/panda.ts @@ -199,7 +199,6 @@ export function verify( }; authType: "webauthn"; credential: { - counter: number; publicKey: { data: number[]; type: "Buffer" }; transports: null | string[]; };