Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(prisma): support webauthn #9876

Merged
merged 4 commits into from Feb 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
@@ -0,0 +1,71 @@
-- CreateTable
CREATE TABLE "Account" (
"userId" TEXT NOT NULL,
"type" TEXT NOT NULL,
"provider" TEXT NOT NULL,
"providerAccountId" TEXT NOT NULL,
"refresh_token" TEXT,
"access_token" TEXT,
"expires_at" INTEGER,
"token_type" TEXT,
"scope" TEXT,
"id_token" TEXT,
"session_state" TEXT,

PRIMARY KEY ("provider", "providerAccountId"),
CONSTRAINT "Account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);

-- CreateTable
CREATE TABLE "Session" (
"id" TEXT NOT NULL PRIMARY KEY,
"sessionToken" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"expires" DATETIME NOT NULL,
CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);

-- CreateTable
CREATE TABLE "User" (
"id" TEXT NOT NULL PRIMARY KEY,
"name" TEXT,
"email" TEXT,
"emailVerified" DATETIME,
"image" TEXT
);

-- CreateTable
CREATE TABLE "VerificationToken" (
"identifier" TEXT NOT NULL,
"token" TEXT NOT NULL,
"expires" DATETIME NOT NULL
);

-- CreateTable
CREATE TABLE "Authenticator" (
"id" TEXT NOT NULL PRIMARY KEY,
"credentialID" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"providerAccountId" TEXT NOT NULL,
"credentialPublicKey" TEXT NOT NULL,
"counter" INTEGER NOT NULL,
"credentialDeviceType" TEXT NOT NULL,
"credentialBackedUp" BOOLEAN NOT NULL,
"transports" TEXT,
CONSTRAINT "Authenticator_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);

-- CreateIndex
CREATE UNIQUE INDEX "Session_sessionToken_key" ON "Session"("sessionToken");

-- CreateIndex
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");

-- CreateIndex
CREATE UNIQUE INDEX "VerificationToken_token_key" ON "VerificationToken"("token");

-- CreateIndex
CREATE UNIQUE INDEX "VerificationToken_identifier_token_key" ON "VerificationToken"("identifier", "token");

-- CreateIndex
CREATE UNIQUE INDEX "Authenticator_credentialID_key" ON "Authenticator"("credentialID");
21 changes: 18 additions & 3 deletions apps/dev/nextjs/prisma/schema.prisma
Expand Up @@ -8,7 +8,6 @@ generator client {
}

model Account {
id String @id @default(cuid())
userId String
type String
provider String
Expand All @@ -20,9 +19,10 @@ model Account {
scope String?
id_token String?
session_state String?
user User @relation(fields: [userId], references: [id])

@@unique([provider, providerAccountId])
user User @relation(fields: [userId], references: [id], onDelete: Cascade)

@@id([provider, providerAccountId])
}

model Session {
Expand All @@ -41,6 +41,7 @@ model User {
image String?
accounts Account[]
sessions Session[]
Authenticator Authenticator[]
}

model VerificationToken {
Expand All @@ -50,3 +51,17 @@ model VerificationToken {

@@unique([identifier, token])
}

model Authenticator {
id String @id @default(cuid())
credentialID String @unique
userId String
providerAccountId String
credentialPublicKey String
counter Int
credentialDeviceType String
credentialBackedUp Boolean
transports String?

user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}
1 change: 1 addition & 0 deletions packages/adapter-hasura/schema.gql
Expand Up @@ -1192,6 +1192,7 @@ enum provider_type_enum {
email
oauth
oidc
webauthn
}

"""
Expand Down
15 changes: 15 additions & 0 deletions packages/adapter-prisma/prisma/custom.prisma
Expand Up @@ -17,6 +17,7 @@ model User {
role String?
accounts Account[]
sessions Session[]
Authenticator Authenticator[]
}

model Account {
Expand Down Expand Up @@ -51,3 +52,17 @@ model VerificationToken {

@@id([identifier, token])
}

model Authenticator {
id String @id @default(cuid())
credentialID String @unique
userId String
providerAccountId String
credentialPublicKey String
counter Int
credentialDeviceType String
credentialBackedUp Boolean
transports String?

user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}
15 changes: 15 additions & 0 deletions packages/adapter-prisma/prisma/mongodb.prisma
Expand Up @@ -41,6 +41,7 @@ model User {
image String?
accounts Account[]
sessions Session[]
Authenticator Authenticator[]
}

model VerificationToken {
Expand All @@ -51,3 +52,17 @@ model VerificationToken {

@@unique([identifier, token])
}

model Authenticator {
id String @id @default(auto()) @map("_id") @db.ObjectId
credentialID String @unique
userId String
providerAccountId String
credentialPublicKey String
counter Int
credentialDeviceType String
credentialBackedUp Boolean
transports String?

user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}
15 changes: 15 additions & 0 deletions packages/adapter-prisma/prisma/schema.prisma
Expand Up @@ -15,6 +15,7 @@ model User {
image String?
accounts Account[]
sessions Session[]
Authenticator Authenticator[]
}

model Account {
Expand Down Expand Up @@ -49,3 +50,17 @@ model VerificationToken {

@@id([identifier, token])
}

model Authenticator {
id String @id @default(cuid())
credentialID String @unique
userId String
providerAccountId String
credentialPublicKey String
counter Int
credentialDeviceType String
credentialBackedUp Boolean
transports String?

user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}
61 changes: 56 additions & 5 deletions packages/adapter-prisma/src/index.ts
Expand Up @@ -16,7 +16,7 @@
* @module @auth/prisma-adapter
*/
import type { PrismaClient, Prisma } from "@prisma/client"
import type { Adapter, AdapterAccount } from "@auth/core/adapters"
import type { Adapter, AdapterAccount, AdapterAuthenticator, AdapterSession, AdapterUser } from "@auth/core/adapters"

/**
* ## Setup
Expand Down Expand Up @@ -215,6 +215,20 @@ import type { Adapter, AdapterAccount } from "@auth/core/adapters"
* @@unique([identifier, token])
* @@map("verificationtokens")
* }
*
* model Authenticator {
* id String @id @default(cuid())
* credentialID String @unique
* userId String
* providerAccountId String
* credentialPublicKey String
* counter Int
* credentialDeviceType String
* credentialBackedUp Boolean
* transports String?
*
* user User @relation(fields: [userId], references: [id], onDelete: Cascade)
* }
* ```
*
**/
Expand All @@ -234,10 +248,10 @@ export function PrismaAdapter(
where: { provider_providerAccountId },
select: { user: true },
})
return account?.user ?? null
return account?.user as AdapterUser ?? null
},
updateUser: ({ id, ...data }) => p.user.update({ where: { id }, data }),
deleteUser: (id) => p.user.delete({ where: { id } }),
updateUser: ({ id, ...data }) => p.user.update({ where: { id }, data }) as Promise<AdapterUser>,
deleteUser: (id) => p.user.delete({ where: { id } }) as Promise<AdapterUser>,
linkAccount: (data) =>
p.account.create({ data }) as unknown as AdapterAccount,
unlinkAccount: (provider_providerAccountId) =>
Expand All @@ -251,7 +265,7 @@ export function PrismaAdapter(
})
if (!userAndSession) return null
const { user, ...session } = userAndSession
return { user, session }
return { user, session } as { user: AdapterUser; session: AdapterSession }
},
createSession: (data) => p.session.create({ data }),
updateSession: (data) =>
Expand Down Expand Up @@ -280,5 +294,42 @@ export function PrismaAdapter(
throw error
}
},
async getAccount(providerAccountId, provider) {
return p.account.findFirst({
where: { providerAccountId, provider }
}) as Promise<AdapterAccount | null>
},
async createAuthenticator(authenticator) {
return p.authenticator.create({
data: authenticator
}).then(fromDBAuthenticator)
},
async getAuthenticator(credentialID) {
const authenticator = await p.authenticator.findUnique({ where: { credentialID } })
return authenticator ? fromDBAuthenticator(authenticator) : null
},
async listAuthenticatorsByUserId(userId) {
const authenticators = await p.authenticator.findMany({ where: { userId } })

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

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,
}
}
5 changes: 5 additions & 0 deletions packages/adapter-prisma/test/index.test.ts
Expand Up @@ -8,6 +8,7 @@ const prisma = new PrismaClient().$extends(withAccelerate())

runBasicTests({
adapter: PrismaAdapter(prisma),
testWebAuthnMethods: true,
db: {
id() {
if (process.env.CONTAINER_NAME !== "authjs-mongodb-test") return
Expand All @@ -19,6 +20,7 @@ runBasicTests({
prisma.account.deleteMany({}),
prisma.session.deleteMany({}),
prisma.verificationToken.deleteMany({}),
prisma.authenticator.deleteMany({}),
])
},
disconnect: async () => {
Expand All @@ -27,6 +29,7 @@ runBasicTests({
prisma.account.deleteMany({}),
prisma.session.deleteMany({}),
prisma.verificationToken.deleteMany({}),
prisma.authenticator.deleteMany({}),
])
await prisma.$disconnect()
},
Expand All @@ -44,5 +47,7 @@ runBasicTests({
delete result.id
return result
},
authenticator: (credentialID) =>
prisma.authenticator.findUnique({ where: { credentialID } }),
},
})
2 changes: 1 addition & 1 deletion packages/adapter-surrealdb/src/index.ts
Expand Up @@ -31,7 +31,7 @@ export type AccountDoc<T = string> = {
userId: T
refresh_token?: string
access_token?: string
type: Extract<ProviderType, "oauth" | "oidc" | "email">
type: Extract<ProviderType, "oauth" | "oidc" | "email" | "webauthn">
provider: string
providerAccountId: string
expires_at?: number
Expand Down