Skip to content

Commit

Permalink
fix(core): update WebAuthn authenticator schemas and types (#10861)
Browse files Browse the repository at this point in the history
Co-authored-by: Julius Marminge <julius0216@outlook.com>
  • Loading branch information
ndom91 and juliusmarminge committed May 10, 2024
1 parent 4bec046 commit 5e55331
Show file tree
Hide file tree
Showing 14 changed files with 112 additions and 136 deletions.
12 changes: 8 additions & 4 deletions docs/pages/getting-started/adapters/prisma.mdx
Expand Up @@ -222,7 +222,6 @@ model VerificationToken {
// Optional for WebAuthn support
model Authenticator {
id String @id @default(cuid())
credentialID String @unique
userId String
providerAccountId String
Expand All @@ -233,6 +232,8 @@ model Authenticator {
transports String?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@id([userId, credentialID])
}
```

Expand Down Expand Up @@ -311,7 +312,6 @@ model VerificationToken {
// Optional for WebAuthn support
model Authenticator {
id String @id @default(cuid())
credentialID String @unique
userId String
providerAccountId String
Expand All @@ -322,6 +322,8 @@ model Authenticator {
transports String?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@id([userId, credentialID])
}
```

Expand Down Expand Up @@ -405,7 +407,6 @@ model VerificationToken {
// Optional for WebAuthn support
model Authenticator {
id String @id @default(cuid())
credentialID String @unique
userId String
providerAccountId String
Expand All @@ -416,6 +417,8 @@ model Authenticator {
transports String?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@id([userId, credentialID])
}
```

Expand Down Expand Up @@ -491,7 +494,6 @@ model VerificationToken {
// Optional for WebAuthn support
model Authenticator {
id String @id @default(auto()) @map("_id") @db.ObjectId
credentialID String @unique
userId String @db.ObjectId
providerAccountId String
Expand All @@ -502,6 +504,8 @@ model Authenticator {
transports String?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@id([userId, credentialID])
}
```

Expand Down
3 changes: 2 additions & 1 deletion docs/pages/getting-started/authentication/webauthn.mdx
Expand Up @@ -39,7 +39,6 @@ In short, the Passkeys provider requires an additional table called `Authenticat
```sql filename="./migration/add-webauthn-authenticator-table.sql"
-- CreateTable
CREATE TABLE "Authenticator" (
"id" TEXT NOT NULL PRIMARY KEY,
"credentialID" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"providerAccountId" TEXT NOT NULL,
Expand All @@ -48,9 +47,11 @@ CREATE TABLE "Authenticator" (
"credentialDeviceType" TEXT NOT NULL,
"credentialBackedUp" BOOLEAN NOT NULL,
"transports" TEXT,
PRIMARY KEY ("userId", "credentialID"),
CONSTRAINT "Authenticator_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);


-- CreateIndex
CREATE UNIQUE INDEX "Authenticator_credentialID_key" ON "Authenticator"("credentialID");
```
Expand Down
3 changes: 2 additions & 1 deletion packages/adapter-prisma/prisma/schema.prisma
Expand Up @@ -52,7 +52,6 @@ model VerificationToken {
}

model Authenticator {
id String @id @default(cuid())
credentialID String @unique
userId String
providerAccountId String
Expand All @@ -63,4 +62,6 @@ model Authenticator {
transports String?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@id([userId, credentialID])
}
43 changes: 9 additions & 34 deletions packages/adapter-prisma/src/index.ts
Expand Up @@ -19,7 +19,6 @@ import type { PrismaClient, Prisma } from "@prisma/client"
import type {
Adapter,
AdapterAccount,
AdapterAuthenticator,
AdapterSession,
AdapterUser,
} from "@auth/core/adapters"
Expand Down Expand Up @@ -94,49 +93,25 @@ export function PrismaAdapter(
}) as Promise<AdapterAccount | null>
},
async createAuthenticator(authenticator) {
return p.authenticator
.create({
data: authenticator,
})
.then(fromDBAuthenticator)
return p.authenticator.create({
data: authenticator,
})
},
async getAuthenticator(credentialID) {
const authenticator = await p.authenticator.findUnique({
return p.authenticator.findUnique({
where: { credentialID },
})
return authenticator ? fromDBAuthenticator(authenticator) : null
},
async listAuthenticatorsByUserId(userId) {
const authenticators = await p.authenticator.findMany({
return p.authenticator.findMany({
where: { userId },
})

return authenticators.map(fromDBAuthenticator)
},
async updateAuthenticatorCounter(credentialID, counter) {
return p.authenticator
.update({
where: { credentialID: credentialID },
data: { counter },
})
.then(fromDBAuthenticator)
return p.authenticator.update({
where: { credentialID },
data: { counter },
})
},
}
}

type BasePrismaAuthenticator = Parameters<
PrismaClient["authenticator"]["create"]
>[0]["data"]
type PrismaAuthenticator = BasePrismaAuthenticator &
Required<Pick<BasePrismaAuthenticator, "userId">>

function fromDBAuthenticator(
authenticator: PrismaAuthenticator
): AdapterAuthenticator {
const { transports, id, user, ...other } = authenticator

return {
...other,
transports: transports || undefined,
}
}
74 changes: 28 additions & 46 deletions packages/adapter-unstorage/src/index.ts
Expand Up @@ -83,7 +83,7 @@ export const defaultOptions = {
sessionByUserIdKeyPrefix: "user:session:by-user-id:",
userKeyPrefix: "user:",
verificationTokenKeyPrefix: "user:token:",
authenticatorKeyPrefix: "authenticator:id:",
authenticatorKeyPrefix: "authenticator:",
authenticatorUserKeyPrefix: "authenticator:by-user-id:",
useItemRaw: false,
}
Expand All @@ -94,7 +94,7 @@ function isDate(value: any) {
return value && isoDateRE.test(value) && !isNaN(Date.parse(value))
}

export function hydrateDates(json: object) {
export function hydrateDates(json: Record<string, any>) {
return Object.entries(json).reduce((acc, [key, val]) => {
acc[key] = isDate(val) ? new Date(val as string) : val
return acc
Expand Down Expand Up @@ -134,15 +134,6 @@ export function UnstorageAdapter(
}
}

async function getItems(key: string[]) {
if (mergedOptions.useItemRaw) {
// Unstorage missing method to get multiple items raw, i.e. `getItemsRaw`
return JSON.stringify(await storage.getItems(key))
} else {
return await storage.getItems(key)
}
}

async function setItem(key: string, value: string) {
if (mergedOptions.useItemRaw) {
return await storage.setItemRaw(key, value)
Expand All @@ -151,7 +142,7 @@ export function UnstorageAdapter(
}
}

const setObjectAsJson = async (key: string, obj: any) => {
const setObjectAsJson = async (key: string, obj: Record<string, any>) => {
if (mergedOptions.useItemRaw) {
await storage.setItemRaw(key, obj)
} else {
Expand Down Expand Up @@ -213,11 +204,21 @@ export function UnstorageAdapter(
credentialId: string,
authenticator: AdapterAuthenticator
): Promise<AdapterAuthenticator> => {
let newCredsToSet = [credentialId]

const getItemReturn = await getItem<string[]>(
`${authenticatorUserKeyPrefix}${authenticator.userId}`
)

if (getItemReturn && getItemReturn[0] !== newCredsToSet[0]) {
newCredsToSet.push(...getItemReturn)
}

await Promise.all([
setObjectAsJson(authenticatorKeyPrefix + credentialId, authenticator),
setItem(
`${authenticatorUserKeyPrefix}${authenticator.userId}`,
credentialId
JSON.stringify(newCredsToSet)
),
])
return authenticator
Expand All @@ -231,24 +232,19 @@ export function UnstorageAdapter(
return hydrateDates(authenticator)
}

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

const authenticators = []
for (const credentialId of credentialIds) {
const credentialValue =
typeof credentialId === "string" ? credentialId : credentialId.value
if (!credentialIds) return []

const authenticator = await getAuthenticator(credentialValue as string)
const authenticators: AdapterAuthenticator[] = []

for (const credentialId of credentialIds) {
const authenticator = await getAuthenticator(credentialId)

if (authenticator) {
hydrateDates(authenticator)
Expand Down Expand Up @@ -362,36 +358,22 @@ export function UnstorageAdapter(
])
},
async createAuthenticator(authenticator) {
setAuthenticator(authenticator.credentialID, authenticator)
return fromDBAuthenticator(authenticator)!
await setAuthenticator(authenticator.credentialID, authenticator)
return authenticator
},
async getAuthenticator(credentialID) {
const authenticator = await getAuthenticator(credentialID)
return fromDBAuthenticator(authenticator)
return getAuthenticator(credentialID)
},
async listAuthenticatorsByUserId(userId) {
const user = await getUser(userId)
if (!user) return []
const authenticators = await getAuthenticatorByUserId(user.id)
return authenticators
return getAuthenticatorByUserId(user.id)
},
async updateAuthenticatorCounter(credentialID, counter) {
const authenticator = await getAuthenticator(credentialID)
authenticator.counter = Number(counter)
setAuthenticator(credentialID, authenticator)
return fromDBAuthenticator(authenticator)!
await setAuthenticator(credentialID, authenticator)
return authenticator
},
}
}

function fromDBAuthenticator(
authenticator: AdapterAuthenticator & { id?: string; user?: string }
): AdapterAuthenticator | null {
if (!authenticator) return null
const { transports, id, user, ...other } = authenticator

return {
...other,
transports: transports || undefined,
}
}
19 changes: 9 additions & 10 deletions packages/adapter-unstorage/test/filesystem.test.ts
Expand Up @@ -9,41 +9,40 @@ const storage = createStorage({

runBasicTests({
adapter: UnstorageAdapter(storage, { baseKeyPrefix: "testApp:" }),
// TODO: Reenable; failing in CI, passing locally
testWebAuthnMethods: false,
// Currently not fully implemented in KV Store
skipTests: ["listAuthenticatorsByUserId"],
testWebAuthnMethods: true,
db: {
disconnect: storage.dispose,
async user(id: string) {
const data = await storage.getItem<object>(`testApp:user:${id}`)
const data = await storage.getItem<Record<string, unknown>>(
`testApp:user:${id}`
)
if (!data) return null
return hydrateDates(data)
},
async account({ provider, providerAccountId }) {
const data = await storage.getItem<object>(
const data = await storage.getItem<Record<string, unknown>>(
`testApp:user:account:${provider}:${providerAccountId}`
)
if (!data) return null
return hydrateDates(data)
},
async session(sessionToken) {
const data = await storage.getItem<object>(
const data = await storage.getItem<Record<string, unknown>>(
`testApp:user:session:${sessionToken}`
)
if (!data) return null
return hydrateDates(data)
},
async verificationToken(where) {
const data = await storage.getItem<object>(
const data = await storage.getItem<Record<string, unknown>>(
`testApp:user:token:${where.identifier}:${where.token}`
)
if (!data) return null
return hydrateDates(data)
},
async authenticator(id) {
const data = await storage.getItem<object>(
`testApp:authenticator:id:${id}`
const data = await storage.getItem<Record<string, unknown>>(
`testApp:authenticator:${id}`
)
if (!data) return null
return hydrateDates(data)
Expand Down

0 comments on commit 5e55331

Please sign in to comment.