Skip to content

Commit 5e55331

Browse files
fix(core): update WebAuthn authenticator schemas and types (#10861)
Co-authored-by: Julius Marminge <[email protected]>
1 parent 4bec046 commit 5e55331

File tree

14 files changed

+112
-136
lines changed

14 files changed

+112
-136
lines changed

docs/pages/getting-started/adapters/prisma.mdx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,6 @@ model VerificationToken {
222222
223223
// Optional for WebAuthn support
224224
model Authenticator {
225-
id String @id @default(cuid())
226225
credentialID String @unique
227226
userId String
228227
providerAccountId String
@@ -233,6 +232,8 @@ model Authenticator {
233232
transports String?
234233
235234
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
235+
236+
@@id([userId, credentialID])
236237
}
237238
```
238239

@@ -311,7 +312,6 @@ model VerificationToken {
311312
312313
// Optional for WebAuthn support
313314
model Authenticator {
314-
id String @id @default(cuid())
315315
credentialID String @unique
316316
userId String
317317
providerAccountId String
@@ -322,6 +322,8 @@ model Authenticator {
322322
transports String?
323323
324324
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
325+
326+
@@id([userId, credentialID])
325327
}
326328
```
327329

@@ -405,7 +407,6 @@ model VerificationToken {
405407
406408
// Optional for WebAuthn support
407409
model Authenticator {
408-
id String @id @default(cuid())
409410
credentialID String @unique
410411
userId String
411412
providerAccountId String
@@ -416,6 +417,8 @@ model Authenticator {
416417
transports String?
417418
418419
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
420+
421+
@@id([userId, credentialID])
419422
}
420423
```
421424

@@ -491,7 +494,6 @@ model VerificationToken {
491494
492495
// Optional for WebAuthn support
493496
model Authenticator {
494-
id String @id @default(auto()) @map("_id") @db.ObjectId
495497
credentialID String @unique
496498
userId String @db.ObjectId
497499
providerAccountId String
@@ -502,6 +504,8 @@ model Authenticator {
502504
transports String?
503505
504506
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
507+
508+
@@id([userId, credentialID])
505509
}
506510
```
507511

docs/pages/getting-started/authentication/webauthn.mdx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@ In short, the Passkeys provider requires an additional table called `Authenticat
3939
```sql filename="./migration/add-webauthn-authenticator-table.sql"
4040
-- CreateTable
4141
CREATE TABLE "Authenticator" (
42-
"id" TEXT NOT NULL PRIMARY KEY,
4342
"credentialID" TEXT NOT NULL,
4443
"userId" TEXT NOT NULL,
4544
"providerAccountId" TEXT NOT NULL,
@@ -48,9 +47,11 @@ CREATE TABLE "Authenticator" (
4847
"credentialDeviceType" TEXT NOT NULL,
4948
"credentialBackedUp" BOOLEAN NOT NULL,
5049
"transports" TEXT,
50+
PRIMARY KEY ("userId", "credentialID"),
5151
CONSTRAINT "Authenticator_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE
5252
);
5353

54+
5455
-- CreateIndex
5556
CREATE UNIQUE INDEX "Authenticator_credentialID_key" ON "Authenticator"("credentialID");
5657
```

packages/adapter-prisma/prisma/schema.prisma

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,6 @@ model VerificationToken {
5252
}
5353

5454
model Authenticator {
55-
id String @id @default(cuid())
5655
credentialID String @unique
5756
userId String
5857
providerAccountId String
@@ -63,4 +62,6 @@ model Authenticator {
6362
transports String?
6463
6564
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
65+
66+
@@id([userId, credentialID])
6667
}

packages/adapter-prisma/src/index.ts

Lines changed: 9 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import type { PrismaClient, Prisma } from "@prisma/client"
1919
import type {
2020
Adapter,
2121
AdapterAccount,
22-
AdapterAuthenticator,
2322
AdapterSession,
2423
AdapterUser,
2524
} from "@auth/core/adapters"
@@ -94,49 +93,25 @@ export function PrismaAdapter(
9493
}) as Promise<AdapterAccount | null>
9594
},
9695
async createAuthenticator(authenticator) {
97-
return p.authenticator
98-
.create({
99-
data: authenticator,
100-
})
101-
.then(fromDBAuthenticator)
96+
return p.authenticator.create({
97+
data: authenticator,
98+
})
10299
},
103100
async getAuthenticator(credentialID) {
104-
const authenticator = await p.authenticator.findUnique({
101+
return p.authenticator.findUnique({
105102
where: { credentialID },
106103
})
107-
return authenticator ? fromDBAuthenticator(authenticator) : null
108104
},
109105
async listAuthenticatorsByUserId(userId) {
110-
const authenticators = await p.authenticator.findMany({
106+
return p.authenticator.findMany({
111107
where: { userId },
112108
})
113-
114-
return authenticators.map(fromDBAuthenticator)
115109
},
116110
async updateAuthenticatorCounter(credentialID, counter) {
117-
return p.authenticator
118-
.update({
119-
where: { credentialID: credentialID },
120-
data: { counter },
121-
})
122-
.then(fromDBAuthenticator)
111+
return p.authenticator.update({
112+
where: { credentialID },
113+
data: { counter },
114+
})
123115
},
124116
}
125117
}
126-
127-
type BasePrismaAuthenticator = Parameters<
128-
PrismaClient["authenticator"]["create"]
129-
>[0]["data"]
130-
type PrismaAuthenticator = BasePrismaAuthenticator &
131-
Required<Pick<BasePrismaAuthenticator, "userId">>
132-
133-
function fromDBAuthenticator(
134-
authenticator: PrismaAuthenticator
135-
): AdapterAuthenticator {
136-
const { transports, id, user, ...other } = authenticator
137-
138-
return {
139-
...other,
140-
transports: transports || undefined,
141-
}
142-
}

packages/adapter-unstorage/src/index.ts

Lines changed: 28 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ export const defaultOptions = {
8383
sessionByUserIdKeyPrefix: "user:session:by-user-id:",
8484
userKeyPrefix: "user:",
8585
verificationTokenKeyPrefix: "user:token:",
86-
authenticatorKeyPrefix: "authenticator:id:",
86+
authenticatorKeyPrefix: "authenticator:",
8787
authenticatorUserKeyPrefix: "authenticator:by-user-id:",
8888
useItemRaw: false,
8989
}
@@ -94,7 +94,7 @@ function isDate(value: any) {
9494
return value && isoDateRE.test(value) && !isNaN(Date.parse(value))
9595
}
9696

97-
export function hydrateDates(json: object) {
97+
export function hydrateDates(json: Record<string, any>) {
9898
return Object.entries(json).reduce((acc, [key, val]) => {
9999
acc[key] = isDate(val) ? new Date(val as string) : val
100100
return acc
@@ -134,15 +134,6 @@ export function UnstorageAdapter(
134134
}
135135
}
136136

137-
async function getItems(key: string[]) {
138-
if (mergedOptions.useItemRaw) {
139-
// Unstorage missing method to get multiple items raw, i.e. `getItemsRaw`
140-
return JSON.stringify(await storage.getItems(key))
141-
} else {
142-
return await storage.getItems(key)
143-
}
144-
}
145-
146137
async function setItem(key: string, value: string) {
147138
if (mergedOptions.useItemRaw) {
148139
return await storage.setItemRaw(key, value)
@@ -151,7 +142,7 @@ export function UnstorageAdapter(
151142
}
152143
}
153144

154-
const setObjectAsJson = async (key: string, obj: any) => {
145+
const setObjectAsJson = async (key: string, obj: Record<string, any>) => {
155146
if (mergedOptions.useItemRaw) {
156147
await storage.setItemRaw(key, obj)
157148
} else {
@@ -213,11 +204,21 @@ export function UnstorageAdapter(
213204
credentialId: string,
214205
authenticator: AdapterAuthenticator
215206
): Promise<AdapterAuthenticator> => {
207+
let newCredsToSet = [credentialId]
208+
209+
const getItemReturn = await getItem<string[]>(
210+
`${authenticatorUserKeyPrefix}${authenticator.userId}`
211+
)
212+
213+
if (getItemReturn && getItemReturn[0] !== newCredsToSet[0]) {
214+
newCredsToSet.push(...getItemReturn)
215+
}
216+
216217
await Promise.all([
217218
setObjectAsJson(authenticatorKeyPrefix + credentialId, authenticator),
218219
setItem(
219220
`${authenticatorUserKeyPrefix}${authenticator.userId}`,
220-
credentialId
221+
JSON.stringify(newCredsToSet)
221222
),
222223
])
223224
return authenticator
@@ -231,24 +232,19 @@ export function UnstorageAdapter(
231232
return hydrateDates(authenticator)
232233
}
233234

234-
// TODO: This one doesn't really work with KV storage, as we can't set the same
235-
// key multiple times, they'll just overwrite one another. Maybe with some
236-
// additional logic to write an array as the value instead of overwriting
237-
// the pre-existing value. Probably in `setItems` implementation.
238235
const getAuthenticatorByUserId = async (
239236
userId: string
240237
): Promise<AdapterAuthenticator[] | []> => {
241-
const credentialIds = await getItems([
242-
`${authenticatorUserKeyPrefix}${userId}`,
243-
])
244-
if (!credentialIds.length) return []
238+
const credentialIds = await getItem<string[]>(
239+
`${authenticatorUserKeyPrefix}${userId}`
240+
)
245241

246-
const authenticators = []
247-
for (const credentialId of credentialIds) {
248-
const credentialValue =
249-
typeof credentialId === "string" ? credentialId : credentialId.value
242+
if (!credentialIds) return []
250243

251-
const authenticator = await getAuthenticator(credentialValue as string)
244+
const authenticators: AdapterAuthenticator[] = []
245+
246+
for (const credentialId of credentialIds) {
247+
const authenticator = await getAuthenticator(credentialId)
252248

253249
if (authenticator) {
254250
hydrateDates(authenticator)
@@ -362,36 +358,22 @@ export function UnstorageAdapter(
362358
])
363359
},
364360
async createAuthenticator(authenticator) {
365-
setAuthenticator(authenticator.credentialID, authenticator)
366-
return fromDBAuthenticator(authenticator)!
361+
await setAuthenticator(authenticator.credentialID, authenticator)
362+
return authenticator
367363
},
368364
async getAuthenticator(credentialID) {
369-
const authenticator = await getAuthenticator(credentialID)
370-
return fromDBAuthenticator(authenticator)
365+
return getAuthenticator(credentialID)
371366
},
372367
async listAuthenticatorsByUserId(userId) {
373368
const user = await getUser(userId)
374369
if (!user) return []
375-
const authenticators = await getAuthenticatorByUserId(user.id)
376-
return authenticators
370+
return getAuthenticatorByUserId(user.id)
377371
},
378372
async updateAuthenticatorCounter(credentialID, counter) {
379373
const authenticator = await getAuthenticator(credentialID)
380374
authenticator.counter = Number(counter)
381-
setAuthenticator(credentialID, authenticator)
382-
return fromDBAuthenticator(authenticator)!
375+
await setAuthenticator(credentialID, authenticator)
376+
return authenticator
383377
},
384378
}
385379
}
386-
387-
function fromDBAuthenticator(
388-
authenticator: AdapterAuthenticator & { id?: string; user?: string }
389-
): AdapterAuthenticator | null {
390-
if (!authenticator) return null
391-
const { transports, id, user, ...other } = authenticator
392-
393-
return {
394-
...other,
395-
transports: transports || undefined,
396-
}
397-
}

packages/adapter-unstorage/test/filesystem.test.ts

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,41 +9,40 @@ const storage = createStorage({
99

1010
runBasicTests({
1111
adapter: UnstorageAdapter(storage, { baseKeyPrefix: "testApp:" }),
12-
// TODO: Reenable; failing in CI, passing locally
13-
testWebAuthnMethods: false,
14-
// Currently not fully implemented in KV Store
15-
skipTests: ["listAuthenticatorsByUserId"],
12+
testWebAuthnMethods: true,
1613
db: {
1714
disconnect: storage.dispose,
1815
async user(id: string) {
19-
const data = await storage.getItem<object>(`testApp:user:${id}`)
16+
const data = await storage.getItem<Record<string, unknown>>(
17+
`testApp:user:${id}`
18+
)
2019
if (!data) return null
2120
return hydrateDates(data)
2221
},
2322
async account({ provider, providerAccountId }) {
24-
const data = await storage.getItem<object>(
23+
const data = await storage.getItem<Record<string, unknown>>(
2524
`testApp:user:account:${provider}:${providerAccountId}`
2625
)
2726
if (!data) return null
2827
return hydrateDates(data)
2928
},
3029
async session(sessionToken) {
31-
const data = await storage.getItem<object>(
30+
const data = await storage.getItem<Record<string, unknown>>(
3231
`testApp:user:session:${sessionToken}`
3332
)
3433
if (!data) return null
3534
return hydrateDates(data)
3635
},
3736
async verificationToken(where) {
38-
const data = await storage.getItem<object>(
37+
const data = await storage.getItem<Record<string, unknown>>(
3938
`testApp:user:token:${where.identifier}:${where.token}`
4039
)
4140
if (!data) return null
4241
return hydrateDates(data)
4342
},
4443
async authenticator(id) {
45-
const data = await storage.getItem<object>(
46-
`testApp:authenticator:id:${id}`
44+
const data = await storage.getItem<Record<string, unknown>>(
45+
`testApp:authenticator:${id}`
4746
)
4847
if (!data) return null
4948
return hydrateDates(data)

0 commit comments

Comments
 (0)