diff --git a/apps/dev/nextjs/lib/db.ts b/apps/dev/nextjs/lib/db.ts new file mode 100644 index 0000000000..571f810a55 --- /dev/null +++ b/apps/dev/nextjs/lib/db.ts @@ -0,0 +1,238 @@ +// Copy from RedisUpstashAdapter +import type { + Adapter, + AdapterUser, + AdapterAccount, + AdapterSession, +} from "@auth/core/adapters" + +export interface AdapterOptions { + /** + * The base prefix for your keys + */ + baseKeyPrefix?: string + /** + * The prefix for the `account` key + */ + accountKeyPrefix?: string + /** + * The prefix for the `accountByUserId` key + */ + accountByUserIdPrefix?: string + /** + * The prefix for the `emailKey` key + */ + emailKeyPrefix?: string + /** + * The prefix for the `sessionKey` key + */ + sessionKeyPrefix?: string + /** + * The prefix for the `sessionByUserId` key + */ + sessionByUserIdKeyPrefix?: string + /** + * The prefix for the `user` key + */ + userKeyPrefix?: string + /** + * The prefix for the `verificationToken` key + */ + verificationTokenKeyPrefix?: string +} + +export const defaultOptions = { + baseKeyPrefix: "", + accountKeyPrefix: "user:account:", + accountByUserIdPrefix: "user:account:by-user-id:", + emailKeyPrefix: "user:email:", + sessionKeyPrefix: "user:session:", + sessionByUserIdKeyPrefix: "user:session:by-user-id:", + userKeyPrefix: "user:", + verificationTokenKeyPrefix: "user:token:", +} + +const isoDateRE = + /(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))/ +function isDate(value: any) { + return value && isoDateRE.test(value) && !isNaN(Date.parse(value)) +} + +export function hydrateDates(text: string) { + return Object.entries(JSON.parse(text)).reduce((acc, [key, val]) => { + acc[key] = isDate(val) ? new Date(val as string) : val + return acc + }, {} as any) +} + +export function TestAdapter( + client: { + getItem: (key: string) => Promise + setItem: (key: string, value: string) => Promise + deleteItems: (...keys: string[]) => Promise + }, + options: AdapterOptions = {} +): Adapter { + const mergedOptions = { + ...defaultOptions, + ...options, + } + + const { baseKeyPrefix } = mergedOptions + const accountKeyPrefix = baseKeyPrefix + mergedOptions.accountKeyPrefix + const accountByUserIdPrefix = + baseKeyPrefix + mergedOptions.accountByUserIdPrefix + const emailKeyPrefix = baseKeyPrefix + mergedOptions.emailKeyPrefix + const sessionKeyPrefix = baseKeyPrefix + mergedOptions.sessionKeyPrefix + const sessionByUserIdKeyPrefix = + baseKeyPrefix + mergedOptions.sessionByUserIdKeyPrefix + const userKeyPrefix = baseKeyPrefix + mergedOptions.userKeyPrefix + const verificationTokenKeyPrefix = + baseKeyPrefix + mergedOptions.verificationTokenKeyPrefix + + const setObjectAsJson = async (key: string, obj: any) => + await client.setItem(key, JSON.stringify(obj)) + + const setAccount = async (id: string, account: AdapterAccount) => { + const accountKey = accountKeyPrefix + id + await setObjectAsJson(accountKey, account) + await client.setItem(accountByUserIdPrefix + account.userId, accountKey) + return account + } + + const getAccount = async (id: string) => { + const account = await client.getItem(accountKeyPrefix + id) + if (!account) return null + return hydrateDates(account) + } + + const setSession = async ( + id: string, + session: AdapterSession + ): Promise => { + const sessionKey = sessionKeyPrefix + id + await setObjectAsJson(sessionKey, session) + await client.setItem(sessionByUserIdKeyPrefix + session.userId, sessionKey) + return session + } + + const getSession = async (id: string) => { + const session = await client.getItem(sessionKeyPrefix + id) + if (!session) return null + return hydrateDates(session) + } + + const setUser = async ( + id: string, + user: AdapterUser + ): Promise => { + await setObjectAsJson(userKeyPrefix + id, user) + await client.setItem(`${emailKeyPrefix}${user.email as string}`, id) + return user + } + + const getUser = async (id: string) => { + const user = await client.getItem(userKeyPrefix + id) + if (!user) return null + return hydrateDates(user) + } + + return { + async createUser(user) { + const id = crypto.randomUUID() + // TypeScript thinks the emailVerified field is missing + // but all fields are copied directly from user, so it's there + return await setUser(id, { ...user, id }) + }, + getUser, + async getUserByTokenId(email) { + const userId = await client.getItem(emailKeyPrefix + email) + if (!userId) { + return null + } + return await getUser(userId) + }, + async getUserByAccount(account) { + const dbAccount = await getAccount( + `${account.provider}:${account.providerAccountId}` + ) + if (!dbAccount) return null + return await getUser(dbAccount.userId) + }, + async updateUser(updates) { + const userId = updates.id as string + const user = await getUser(userId) + return await setUser(userId, { ...(user as AdapterUser), ...updates }) + }, + async linkAccount(account) { + const id = `${account.provider}:${account.providerAccountId}` + return await setAccount(id, { ...account, id }) + }, + createSession: (session) => setSession(session.sessionToken, session), + async getSessionAndUser(sessionToken) { + const session = await getSession(sessionToken) + if (!session) return null + const user = await getUser(session.userId) + if (!user) return null + return { session, user } + }, + async updateSession(updates) { + const session = await getSession(updates.sessionToken) + if (!session) return null + return await setSession(updates.sessionToken, { ...session, ...updates }) + }, + async deleteSession(sessionToken) { + await client.deleteItems(sessionKeyPrefix + sessionToken) + }, + async createVerificationToken(verificationToken) { + await setObjectAsJson( + verificationTokenKeyPrefix + + verificationToken.identifier + + ":" + + verificationToken.token, + verificationToken + ) + return verificationToken + }, + async useVerificationToken(verificationToken) { + const tokenKey = + verificationTokenKeyPrefix + + verificationToken.identifier + + ":" + + verificationToken.token + + const token = await client.getItem(tokenKey) + if (!token) return null + + await client.deleteItems(tokenKey) + return hydrateDates(token) + // return reviveFromJson(token) + }, + async unlinkAccount(account) { + const id = `${account.provider}:${account.providerAccountId}` + const dbAccount = await getAccount(id) + if (!dbAccount) return + const accountKey = `${accountKeyPrefix}${id}` + await client.deleteItems( + accountKey, + `${accountByUserIdPrefix} + ${dbAccount.userId as string}` + ) + }, + async deleteUser(userId) { + const user = await getUser(userId) + if (!user) return + const accountByUserKey = accountByUserIdPrefix + userId + const accountKey = await client.getItem(accountByUserKey) + const sessionByUserIdKey = sessionByUserIdKeyPrefix + userId + const sessionKey = await client.getItem(sessionByUserIdKey) + await client.deleteItems( + userKeyPrefix + userId, + `${emailKeyPrefix}${user.email as string}`, + accountKey as string, + accountByUserKey, + sessionKey as string, + sessionByUserIdKey + ) + }, + } +} diff --git a/apps/dev/nextjs/pages/api/auth/[...nextauth].ts b/apps/dev/nextjs/pages/api/auth/[...nextauth].ts index 37fa3d9e2f..a6732abe61 100644 --- a/apps/dev/nextjs/pages/api/auth/[...nextauth].ts +++ b/apps/dev/nextjs/pages/api/auth/[...nextauth].ts @@ -13,7 +13,7 @@ import Credentials from "@auth/core/providers/credentials" import Descope from "@auth/core/providers/descope" import Discord from "@auth/core/providers/discord" import DuendeIDS6 from "@auth/core/providers/duende-identity-server6" -// import Email from "@auth/core/providers/email" +import SmtpEmail from "@auth/core/providers/email-smtp" import Facebook from "@auth/core/providers/facebook" import Foursquare from "@auth/core/providers/foursquare" import Freshbooks from "@auth/core/providers/freshbooks" @@ -39,6 +39,9 @@ import Yandex from "@auth/core/providers/yandex" import Vk from "@auth/core/providers/vk" import Wikimedia from "@auth/core/providers/wikimedia" import WorkOS from "@auth/core/providers/workos" +import { AdapterUser } from "next-auth/adapters" +import Token from "@auth/core/providers/token" +import { TestAdapter } from "lib/db" // // Prisma // import { PrismaClient } from "@prisma/client" @@ -71,8 +74,22 @@ import WorkOS from "@auth/core/providers/workos" // secret: process.env.SUPABASE_SERVICE_ROLE_KEY, // }) +const db = {} + export const authConfig: AuthConfig = { - // adapter, + adapter: TestAdapter({ + getItem(key) { + return db[key] + }, + setItem: function (key: string, value: string): Promise { + db[key] = value + return Promise.resolve() + }, + deleteItems: function (...keys: string[]): Promise { + keys.forEach((key) => delete db[key]) + return Promise.resolve() + }, + }), debug: process.env.NODE_ENV !== "production", theme: { logo: "https://next-auth.js.org/img/logo/logo-sm.png", @@ -137,11 +154,19 @@ export const authConfig: AuthConfig = { if (authConfig.adapter) { // TODO: - // authOptions.providers.unshift( - // // NOTE: You can start a fake e-mail server with `pnpm email` - // // and then go to `http://localhost:1080` in the browser - // Email({ server: "smtp://127.0.0.1:1025?tls.rejectUnauthorized=false" }) - // ) + authConfig.providers.unshift( + // NOTE: You can start a fake e-mail server with `pnpm email` + // and then go to `http://localhost:1080` in the browser + // SmtpEmail({ server: "smtp://127.0.0.1:1025?tls.rejectUnauthorized=false" }), + Token({ + id: "token", + name: "Token", + type: "token", + async sendVerificationRequest(params) { + console.log({ verificationUrl: params.url }) + }, + }) + ) } // TODO: move to next-auth/edge diff --git a/packages/adapter-dgraph/src/index.ts b/packages/adapter-dgraph/src/index.ts index 9f3ce1607e..e8911c7749 100644 --- a/packages/adapter-dgraph/src/index.ts +++ b/packages/adapter-dgraph/src/index.ts @@ -313,7 +313,7 @@ export function DgraphAdapter( return format.from(result) }, - async getUserByEmail(email) { + async getUserByTokenId(email) { const [user] = await c.run( /* GraphQL */ ` query ($email: String = "") { diff --git a/packages/adapter-drizzle/src/lib/mysql.ts b/packages/adapter-drizzle/src/lib/mysql.ts index 87c645dad0..6218eddcb2 100644 --- a/packages/adapter-drizzle/src/lib/mysql.ts +++ b/packages/adapter-drizzle/src/lib/mysql.ts @@ -105,7 +105,7 @@ export function mySqlDrizzleAdapter( return thing }, - async getUserByEmail(data) { + async getUserByTokenId(data) { const user = (await client .select() diff --git a/packages/adapter-drizzle/src/lib/pg.ts b/packages/adapter-drizzle/src/lib/pg.ts index ec33d7cf96..71e7ea5e8b 100644 --- a/packages/adapter-drizzle/src/lib/pg.ts +++ b/packages/adapter-drizzle/src/lib/pg.ts @@ -89,7 +89,7 @@ export function pgDrizzleAdapter( .where(eq(users.id, data)) .then((res) => res[0] ?? null) }, - async getUserByEmail(data) { + async getUserByTokenId(data) { return await client .select() .from(users) diff --git a/packages/adapter-drizzle/src/lib/sqlite.ts b/packages/adapter-drizzle/src/lib/sqlite.ts index 81fdaa5728..96b10d291f 100644 --- a/packages/adapter-drizzle/src/lib/sqlite.ts +++ b/packages/adapter-drizzle/src/lib/sqlite.ts @@ -84,7 +84,7 @@ export function SQLiteDrizzleAdapter( getUser(data) { return client.select().from(users).where(eq(users.id, data)).get() ?? null }, - getUserByEmail(data) { + getUserByTokenId(data) { return ( client.select().from(users).where(eq(users.email, data)).get() ?? null ) diff --git a/packages/adapter-dynamodb/src/index.ts b/packages/adapter-dynamodb/src/index.ts index bf377377c2..2349a07bd3 100644 --- a/packages/adapter-dynamodb/src/index.ts +++ b/packages/adapter-dynamodb/src/index.ts @@ -213,7 +213,7 @@ export function DynamoDBAdapter( }) return format.from(data.Item) }, - async getUserByEmail(email) { + async getUserByTokenId(email) { const data = await client.query({ TableName, IndexName, diff --git a/packages/adapter-fauna/src/index.ts b/packages/adapter-fauna/src/index.ts index 567c638a91..e8b4978a8e 100644 --- a/packages/adapter-fauna/src/index.ts +++ b/packages/adapter-fauna/src/index.ts @@ -214,7 +214,7 @@ export function FaunaAdapter(f: FaunaClient): Adapter { return { createUser: async (data) => (await q(Create(Users, { data: to(data) })))!, getUser: async (id) => await q(Get(Ref(Users, id))), - getUserByEmail: async (email) => await q(Get(Match(UserByEmail, email))), + getUserByTokenId: async (email) => await q(Get(Match(UserByEmail, email))), async getUserByAccount({ provider, providerAccountId }) { const key = [provider, providerAccountId] const ref = Match(AccountByProviderAndProviderAccountId, key) diff --git a/packages/adapter-firebase/src/index.ts b/packages/adapter-firebase/src/index.ts index d4d4ef6fba..ea6f3ad80b 100644 --- a/packages/adapter-firebase/src/index.ts +++ b/packages/adapter-firebase/src/index.ts @@ -160,7 +160,7 @@ export function FirestoreAdapter( return await getDoc(C.users.doc(id)) }, - async getUserByEmail(email) { + async getUserByTokenId(email) { return await getOneDoc(C.users.where("email", "==", email)) }, diff --git a/packages/adapter-kysely/src/index.ts b/packages/adapter-kysely/src/index.ts index 1788030d23..a05e09894a 100644 --- a/packages/adapter-kysely/src/index.ts +++ b/packages/adapter-kysely/src/index.ts @@ -307,7 +307,7 @@ export function KyselyAdapter(db: Kysely): Adapter { if (!result) return null return to(result, "emailVerified") }, - async getUserByEmail(email) { + async getUserByTokenId(email) { const result = (await db .selectFrom("User") diff --git a/packages/adapter-mikro-orm/src/index.ts b/packages/adapter-mikro-orm/src/index.ts index 641b412a66..eabce5ba3e 100644 --- a/packages/adapter-mikro-orm/src/index.ts +++ b/packages/adapter-mikro-orm/src/index.ts @@ -205,7 +205,7 @@ export function MikroOrmAdapter< return wrap(user).toObject() }, - async getUserByEmail(email) { + async getUserByTokenId(email) { const em = await getEM() const user = await em.findOne(UserModel, { email }) if (!user) return null diff --git a/packages/adapter-mongodb/src/index.ts b/packages/adapter-mongodb/src/index.ts index 53bfb72ea0..aab31e2fcb 100644 --- a/packages/adapter-mongodb/src/index.ts +++ b/packages/adapter-mongodb/src/index.ts @@ -172,7 +172,7 @@ export function MongoDBAdapter( if (!user) return null return from(user) }, - async getUserByEmail(email) { + async getUserByTokenId(email) { const user = await (await db).U.findOne({ email }) if (!user) return null return from(user) diff --git a/packages/adapter-neo4j/src/index.ts b/packages/adapter-neo4j/src/index.ts index 1044440410..d77734dc8c 100644 --- a/packages/adapter-neo4j/src/index.ts +++ b/packages/adapter-neo4j/src/index.ts @@ -141,7 +141,7 @@ export function Neo4jAdapter(session: Session): Adapter { }) }, - async getUserByEmail(email) { + async getUserByTokenId(email) { return await read(`MATCH (u:User { email: $email }) RETURN u{.*}`, { email, }) diff --git a/packages/adapter-pouchdb/src/index.ts b/packages/adapter-pouchdb/src/index.ts index d6bcc630a9..6de6e975c6 100644 --- a/packages/adapter-pouchdb/src/index.ts +++ b/packages/adapter-pouchdb/src/index.ts @@ -167,7 +167,7 @@ export function PouchDBAdapter(options: PouchDBAdapterOptions): Adapter { } }, - async getUserByEmail(email) { + async getUserByTokenId(email) { const res = await ( pouchdb as unknown as PouchDB.Database ).find({ diff --git a/packages/adapter-prisma/src/index.ts b/packages/adapter-prisma/src/index.ts index 1d48d7268a..70be960588 100644 --- a/packages/adapter-prisma/src/index.ts +++ b/packages/adapter-prisma/src/index.ts @@ -222,7 +222,7 @@ export function PrismaAdapter(p: PrismaClient): Adapter { return { createUser: (data) => p.user.create({ data }), getUser: (id) => p.user.findUnique({ where: { id } }), - getUserByEmail: (email) => p.user.findUnique({ where: { email } }), + getUserByTokenId: (email) => p.user.findUnique({ where: { email } }), async getUserByAccount(provider_providerAccountId) { const account = await p.account.findUnique({ where: { provider_providerAccountId }, diff --git a/packages/adapter-sequelize/src/index.ts b/packages/adapter-sequelize/src/index.ts index aed2a55989..b003913991 100644 --- a/packages/adapter-sequelize/src/index.ts +++ b/packages/adapter-sequelize/src/index.ts @@ -206,7 +206,7 @@ export default function SequelizeAdapter( return userInstance?.get({ plain: true }) ?? null }, - async getUserByEmail(email) { + async getUserByTokenId(email) { await sync() const userInstance = await User.findOne({ diff --git a/packages/adapter-supabase/src/index.ts b/packages/adapter-supabase/src/index.ts index 13628c9ad1..99ea3f3b77 100644 --- a/packages/adapter-supabase/src/index.ts +++ b/packages/adapter-supabase/src/index.ts @@ -378,7 +378,7 @@ export function SupabaseAdapter(options: SupabaseAdapterOptions): Adapter { return format(data) }, - async getUserByEmail(email) { + async getUserByTokenId(email) { const { data, error } = await supabase .from("users") .select() diff --git a/packages/adapter-test/index.ts b/packages/adapter-test/index.ts index bb3d8c163b..dd7e9e4e54 100644 --- a/packages/adapter-test/index.ts +++ b/packages/adapter-test/index.ts @@ -4,7 +4,7 @@ import { createHash, randomUUID } from "crypto" const requiredMethods = [ "createUser", "getUser", - "getUserByEmail", + "getUserByTokenId", "getUserByAccount", "updateUser", "linkAccount", @@ -21,7 +21,7 @@ export interface TestOptions { account?: any sessionUpdateExpires?: Date verificationTokenExpires?: Date - }, + } db: { /** Generates UUID v4 by default. Use it to override how the test suite should generate IDs, like user id. */ id?: () => string @@ -78,7 +78,7 @@ export async function runBasicTests(options: TestOptions) { email: "fill@murray.com", image: "https://www.fillmurray.com/460/300", name: "Fill Murray", - emailVerified: new Date() + emailVerified: new Date(), } if (process.env.CUSTOM_MODEL === "1") { @@ -110,7 +110,7 @@ export async function runBasicTests(options: TestOptions) { const requiredMethods = [ "createUser", "getUser", - "getUserByEmail", + "getUserByTokenId", "getUserByAccount", "updateUser", "linkAccount", @@ -138,9 +138,9 @@ export async function runBasicTests(options: TestOptions) { expect(await adapter.getUser(user.id)).toEqual(user) }) - test("getUserByEmail", async () => { - expect(await adapter.getUserByEmail("non-existent-email")).toBeNull() - expect(await adapter.getUserByEmail(user.email)).toEqual(user) + test("getUserByTokenId", async () => { + expect(await adapter.getUserByTokenId("non-existent-email")).toBeNull() + expect(await adapter.getUserByTokenId(user.email)).toEqual(user) }) test("createSession", async () => { @@ -241,7 +241,8 @@ export async function runBasicTests(options: TestOptions) { const verificationToken = { token: hashedToken, identifier, - expires: options.fixtures?.verificationTokenExpires ?? FIFTEEN_MINUTES_FROM_NOW, + expires: + options.fixtures?.verificationTokenExpires ?? FIFTEEN_MINUTES_FROM_NOW, } await adapter.createVerificationToken?.(verificationToken) @@ -260,7 +261,8 @@ export async function runBasicTests(options: TestOptions) { const verificationToken = { token: hashedToken, identifier, - expires: options.fixtures?.verificationTokenExpires ?? FIFTEEN_MINUTES_FROM_NOW, + expires: + options.fixtures?.verificationTokenExpires ?? FIFTEEN_MINUTES_FROM_NOW, } await adapter.createVerificationToken?.(verificationToken) diff --git a/packages/adapter-typeorm/src/index.ts b/packages/adapter-typeorm/src/index.ts index c57a816a5e..6093f498aa 100644 --- a/packages/adapter-typeorm/src/index.ts +++ b/packages/adapter-typeorm/src/index.ts @@ -320,7 +320,7 @@ export function TypeORMAdapter( return { ...user } }, // @ts-expect-error - async getUserByEmail(email) { + async getUserByTokenId(email) { const m = await getManager(c) const user = await m.findOne("UserEntity", { where: { email } }) if (!user) return null diff --git a/packages/adapter-upstash-redis/src/index.ts b/packages/adapter-upstash-redis/src/index.ts index d45d73ebea..79d439e3ba 100644 --- a/packages/adapter-upstash-redis/src/index.ts +++ b/packages/adapter-upstash-redis/src/index.ts @@ -221,7 +221,7 @@ export function UpstashRedisAdapter( return await setUser(id, { ...user, id }) }, getUser, - async getUserByEmail(email) { + async getUserByTokenId(email) { const userId = await client.get(emailKeyPrefix + email) if (!userId) { return null diff --git a/packages/adapter-xata/src/index.ts b/packages/adapter-xata/src/index.ts index 52c07ac2f5..6e6df297fb 100644 --- a/packages/adapter-xata/src/index.ts +++ b/packages/adapter-xata/src/index.ts @@ -248,7 +248,7 @@ export function XataAdapter(client: XataClient): Adapter { const user = await client.db.nextauth_users.filter({ id }).getFirst() return user ?? null }, - async getUserByEmail(email) { + async getUserByTokenId(email) { const user = await client.db.nextauth_users.filter({ email }).getFirst() return user ?? null }, diff --git a/packages/core/src/adapters.ts b/packages/core/src/adapters.ts index a2d39c2a72..84c6db10b3 100644 --- a/packages/core/src/adapters.ts +++ b/packages/core/src/adapters.ts @@ -137,6 +137,9 @@ export interface AdapterUser extends User { * It is `null` if the user has not signed in with the Email provider yet, or the date of the first successful signin. */ emailVerified: Date | null + /** The user's token identifier - could be a phone number or an email. */ + tokenId: string + tokenVerified: Date | null } /** @@ -150,7 +153,7 @@ export interface AdapterUser extends User { */ export interface AdapterAccount extends Account { userId: string - type: Extract + type: Extract } /** @@ -218,12 +221,14 @@ export interface VerificationToken { export interface Adapter { createUser?(user: Omit): Awaitable getUser?(id: string): Awaitable - getUserByEmail?(email: string): Awaitable + getUserByTokenId?(id: string): Awaitable /** Using the provider id and the id of the user for a specific account, get the user. */ getUserByAccount?( providerAccountId: Pick ): Awaitable - updateUser?(user: Partial & Pick): Awaitable + updateUser?( + user: Partial & Pick + ): Awaitable /** @todo This method is currently not invoked yet. */ deleteUser?( userId: string diff --git a/packages/core/src/lib/assert.ts b/packages/core/src/lib/assert.ts index b5b4e9c55d..a3b66353ee 100644 --- a/packages/core/src/lib/assert.ts +++ b/packages/core/src/lib/assert.ts @@ -35,18 +35,18 @@ function isValidHttpUrl(url: string, baseUrl: string) { } let hasCredentials = false -let hasEmail = false +let hasToken = false -const emailMethods = [ +const tokenMethods = [ "createVerificationToken", "useVerificationToken", - "getUserByEmail", + "getUserByTokenId", ] const sessionMethods = [ "createUser", "getUser", - "getUserByEmail", + "getUserByTokenId", "getUserByAccount", "updateUser", "linkAccount", @@ -122,7 +122,7 @@ export function assertConfig( } if (provider.type === "credentials") hasCredentials = true - else if (provider.type === "email") hasEmail = true + else if (provider.type === "token") hasToken = true } if (hasCredentials) { @@ -149,16 +149,16 @@ export function assertConfig( const { adapter, session } = options if ( - hasEmail || + hasToken || session?.strategy === "database" || (!session?.strategy && adapter) ) { let methods: string[] - if (hasEmail) { + if (hasToken) { if (!adapter) - return new MissingAdapter("Email login requires an adapter.") - methods = emailMethods + return new MissingAdapter("Token login requires an adapter.") + methods = tokenMethods } else { if (!adapter) return new MissingAdapter("Database session requires an adapter.") diff --git a/packages/core/src/lib/callback-handler.ts b/packages/core/src/lib/callback-handler.ts index 4250a6fda0..9fa5d89340 100644 --- a/packages/core/src/lib/callback-handler.ts +++ b/packages/core/src/lib/callback-handler.ts @@ -32,7 +32,7 @@ export async function handleLogin( // Input validation if (!_account?.providerAccountId || !_account.type) throw new Error("Missing or invalid provider account") - if (!["email", "oauth", "oidc"].includes(_account.type)) + if (!["token", "oauth", "oidc"].includes(_account.type)) throw new Error("Provider not supported") const { @@ -56,7 +56,7 @@ export async function handleLogin( updateUser, getUser, getUserByAccount, - getUserByEmail, + getUserByTokenId, linkAccount, createSession, getSessionAndUser, @@ -88,24 +88,42 @@ export async function handleLogin( } } - if (account.type === "email") { + const tokenId = profile.tokenId ?? profile.email + + if (account.type === "token") { // If signing in with an email, check if an account with the same email address exists already - const userByEmail = await getUserByEmail(profile.email) - if (userByEmail) { + const userByTokenId = await getUserByTokenId( + profile.tokenId ?? profile.email + ) + if (userByTokenId) { // If they are not already signed in as the same user, this flow will // sign them out of the current session and sign them in as the new user - if (user?.id !== userByEmail.id && !useJwtSession && sessionToken) { + if (user?.id !== userByTokenId.id && !useJwtSession && sessionToken) { // Delete existing session if they are currently signed in as another user. // This will switch user accounts for the session in cases where the user was // already logged in with a different account. await deleteSession(sessionToken) } + const updateUserPayload: Parameters[0] = { + id: userByTokenId.id, + } + + if (profile.email) updateUserPayload.emailVerified = new Date() + if (profile.tokenId) updateUserPayload.tokenVerified = new Date() + // Update emailVerified property on the user object - user = await updateUser({ id: userByEmail.id, emailVerified: new Date() }) + user = await updateUser(updateUserPayload) await events.updateUser?.({ user }) } else { - const { id: _, ...newUser } = { ...profile, emailVerified: new Date() } + const createUserPayload = { + ...profile, + } + + if (profile.email) createUserPayload.emailVerified = new Date() + if (profile.tokenId) createUserPayload.tokenVerified = new Date() + + const { id: _, ...newUser } = createUserPayload // Create user account if there isn't one for the email address already user = await createUser(newUser) await events.createUser?.({ user }) @@ -187,15 +205,15 @@ export async function handleLogin( // // OAuth providers should require email address verification to prevent this, but in // practice that is not always the case; this helps protect against that. - const userByEmail = profile.email - ? await getUserByEmail(profile.email) + const userByTokenId = profile.tokenId + ? await getUserByTokenId(profile.tokenId) : null - if (userByEmail) { + if (userByTokenId) { const provider = options.provider as OAuthConfig if (provider?.allowDangerousEmailAccountLinking) { // If you trust the oauth provider to correctly verify email addresses, you can opt-in to // account linking even when the user is not signed-in. - user = userByEmail + user = userByTokenId } else { // We end up here when we don't have an account with the same [provider].id *BUT* // we do already have an account with the same email address as the one in the diff --git a/packages/core/src/lib/pages/index.ts b/packages/core/src/lib/pages/index.ts index 1c66c36fc8..8742f7fa10 100644 --- a/packages/core/src/lib/pages/index.ts +++ b/packages/core/src/lib/pages/index.ts @@ -51,8 +51,8 @@ export default function renderPage(params: RenderPageParams) { // We only want to render providers providers: params.providers?.filter( (provider) => - // Always render oauth and email type providers - ["email", "oauth", "oidc"].includes(provider.type) || + // Always render oauth and token type providers + ["token", "oauth", "oidc"].includes(provider.type) || // Only render credentials type provider if credentials are defined (provider.type === "credentials" && provider.credentials) || // Don't render other provider types diff --git a/packages/core/src/lib/pages/signin.tsx b/packages/core/src/lib/pages/signin.tsx index a429688f68..8efad09c2f 100644 --- a/packages/core/src/lib/pages/signin.tsx +++ b/packages/core/src/lib/pages/signin.tsx @@ -27,7 +27,7 @@ export default function SigninPage(props: { csrfToken: string providers: InternalProvider[] callbackUrl: string - email: string + token: string error?: SignInPageErrorParam theme: Theme }) { @@ -36,7 +36,7 @@ export default function SigninPage(props: { providers = [], callbackUrl, theme, - email, + token, error: errorType, } = props @@ -131,25 +131,25 @@ export default function SigninPage(props: { ) : null} - {(provider.type === "email" || provider.type === "credentials") && + {(provider.type === "token" || provider.type === "credentials") && i > 0 && - providers[i - 1].type !== "email" && + providers[i - 1].type !== "token" && providers[i - 1].type !== "credentials" &&
} - {provider.type === "email" && ( + {provider.type === "token" && (
@@ -185,7 +185,7 @@ export default function SigninPage(props: {
)} - {(provider.type === "email" || provider.type === "credentials") && + {(provider.type === "token" || provider.type === "credentials") && i + 1 < providers.length &&
} ))} diff --git a/packages/core/src/lib/pages/verify-request.tsx b/packages/core/src/lib/pages/verify-request.tsx index 7f096ccf56..003202005f 100644 --- a/packages/core/src/lib/pages/verify-request.tsx +++ b/packages/core/src/lib/pages/verify-request.tsx @@ -23,8 +23,8 @@ export default function VerifyRequestPage(props: VerifyRequestPageProps) { )}
{theme.logo && Logo} -

Check your email

-

A sign in link has been sent to your email address.

+

Verification sent

+

A sign in link has been sent to you.

{url.host} diff --git a/packages/core/src/lib/routes/callback.ts b/packages/core/src/lib/routes/callback.ts index 94945855e0..39b12400cd 100644 --- a/packages/core/src/lib/routes/callback.ts +++ b/packages/core/src/lib/routes/callback.ts @@ -104,6 +104,7 @@ export async function callback(params: { const unauthorizedOrError = await handleAuthorized( { + // @ts-expect-error user: userByAccountOrFromProvider, account, profile: OAuthProfile, @@ -180,14 +181,14 @@ export async function callback(params: { } return { redirect: callbackUrl, cookies } - } else if (provider.type === "email") { + } else if (provider.type === "token") { const token = query?.token as string | undefined - const identifier = query?.email as string | undefined + const tokenId = query?.tokenId as string | undefined - if (!token || !identifier) { + if (!token || !tokenId) { const e = new TypeError( - "Missing token or email. The sign-in URL was manually opened without token/identifier or the link was not sent correctly in the email.", - { cause: { hasToken: !!token, hasEmail: !!identifier } } + "Missing token or identifier. The sign-in URL was manually opened without token/identifier or the link was not sent correctly in the identifier.", + { cause: { hasToken: !!token, hasIdentifier: !!tokenId } } ) e.name = "Configuration" throw e @@ -196,7 +197,7 @@ export async function callback(params: { const secret = provider.secret ?? options.secret // @ts-expect-error -- Verified in `assertConfig`. const invite = await adapter.useVerificationToken({ - identifier, + identifier: tokenId, token: await createHash(`${token}${secret}`), }) @@ -205,16 +206,18 @@ export async function callback(params: { const invalidInvite = !hasInvite || expired if (invalidInvite) throw new Verification({ hasInvite, expired }) - const user = (await adapter!.getUserByEmail(identifier)) ?? { - id: identifier, - email: identifier, + const user = (await adapter!.getUserByTokenId(tokenId)) ?? { + id: tokenId, + email: tokenId, + tokenId, emailVerified: null, + tokenVerified: null, } const account: Account = { - providerAccountId: user.email, + providerAccountId: user.tokenId, userId: user.id, - type: "email" as const, + type: provider.type, provider: provider.id, } @@ -315,8 +318,7 @@ export async function callback(params: { } } - /** @type {import("src").Account} */ - const account = { + const account: Account = { providerAccountId: user.id, type: "credentials", provider: provider.id, @@ -339,7 +341,6 @@ export async function callback(params: { const token = await callbacks.jwt({ token: defaultToken, user, - // @ts-expect-error account, isNewUser: false, trigger: "signIn", @@ -363,7 +364,6 @@ export async function callback(params: { cookies.push(...sessionCookies) } - // @ts-expect-error await events.signIn?.({ user, account }) return { redirect: callbackUrl, cookies } diff --git a/packages/core/src/lib/routes/shared.ts b/packages/core/src/lib/routes/shared.ts index 1216d0b5c2..ff29da4919 100644 --- a/packages/core/src/lib/routes/shared.ts +++ b/packages/core/src/lib/routes/shared.ts @@ -2,7 +2,7 @@ import { AuthorizedCallbackError } from "../../errors.js" import { InternalOptions } from "../../types.js" export async function handleAuthorized( - params: any, + params: Parameters[0], { url, logger, callbacks: { signIn } }: InternalOptions ) { try { diff --git a/packages/core/src/lib/routes/signin.ts b/packages/core/src/lib/routes/signin.ts index 041a665071..54e16d4539 100644 --- a/packages/core/src/lib/routes/signin.ts +++ b/packages/core/src/lib/routes/signin.ts @@ -1,4 +1,4 @@ -import emailSignin from "../email/signin.js" +import tokenSignin from "../token/signin.js" import { SignInError } from "../../errors.js" import { getAuthorizationUrl } from "../oauth/authorization-url.js" import { handleAuthorized } from "./shared.js" @@ -17,27 +17,28 @@ import type { */ export async function signin( request: RequestInternal, - options: InternalOptions<"oauth" | "oidc" | "email"> + options: InternalOptions<"oauth" | "oidc" | "token"> ): Promise { const { query, body } = request const { url, logger, provider } = options try { if (provider.type === "oauth" || provider.type === "oidc") { return await getAuthorizationUrl(query, options) - } else if (provider.type === "email") { - const normalizer = provider.normalizeIdentifier ?? defaultNormalizer - const email = normalizer(body?.email) + } else if (provider.type === "token") { + const tokenId = provider.normalizeIdentifier?.(body?.tokenId) ?? "" - const user = (await options.adapter!.getUserByEmail(email)) ?? { - id: email, - email, + const user = (await options.adapter!.getUserByTokenId(tokenId)) ?? { + id: tokenId, + email: tokenId, + tokenId, + tokenVerified: null, emailVerified: null, } const account: Account = { - providerAccountId: email, + providerAccountId: tokenId, userId: user.id, - type: "email", + type: "token", provider: provider.id, } @@ -48,27 +49,16 @@ export async function signin( if (unauthorizedOrError) return unauthorizedOrError - const redirect = await emailSignin(email, options, request) + const redirect = await tokenSignin(tokenId, options, request) return { redirect } } return { redirect: `${url}/signin` } } catch (e) { const error = new SignInError(e as Error, { provider: provider.id }) logger.error(error) - const code = provider.type === "email" ? "EmailSignin" : "OAuthSignin" + const code = provider.type === "token" ? "TokenSignin" : "OAuthSignin" url.searchParams.set("error", code) url.pathname += "/signin" return { redirect: url.toString() } } } - -function defaultNormalizer(email?: string) { - if (!email) throw new Error("Missing email from request body.") - // Get the first two elements only, - // separated by `@` from user input. - let [local, domain] = email.toLowerCase().trim().split("@") - // The part before "@" can contain a "," - // but we remove it on the domain part - domain = domain.split(",")[0] - return `${local}@${domain}` -} diff --git a/packages/core/src/lib/email/signin.ts b/packages/core/src/lib/token/signin.ts similarity index 75% rename from packages/core/src/lib/email/signin.ts rename to packages/core/src/lib/token/signin.ts index 575f45c5af..031cf9967c 100644 --- a/packages/core/src/lib/email/signin.ts +++ b/packages/core/src/lib/token/signin.ts @@ -2,12 +2,12 @@ import { createHash, randomString, toRequest } from "../web.js" import type { InternalOptions, RequestInternal } from "../../types.js" /** - * Starts an e-mail login flow, by generating a token, - * and sending it to the user's e-mail (with the help of a DB adapter) + * Starts an token login flow, by generating a token, + * and sending it to the user's token identifier (with the help of a DB adapter) */ -export default async function email( +export default async function token( identifier: string, - options: InternalOptions<"email">, + options: InternalOptions<"token">, request: RequestInternal ): Promise { const { url, adapter, provider, callbackUrl, theme } = options @@ -19,8 +19,12 @@ export default async function email( Date.now() + (provider.maxAge ?? ONE_DAY_IN_SECONDS) * 1000 ) - // Generate a link with email, unhashed token and callback url - const params = new URLSearchParams({ callbackUrl, token, email: identifier }) + // Generate a link with token, unhashed token and callback url + const params = new URLSearchParams({ + callbackUrl, + token, + tokenId: identifier, + }) const _url = `${url}/callback/${provider.id}?${params}` const secret = provider.secret ?? options.secret diff --git a/packages/core/src/providers/email.ts b/packages/core/src/providers/email-smtp.ts similarity index 76% rename from packages/core/src/providers/email.ts rename to packages/core/src/providers/email-smtp.ts index ec8e1e0ed2..19ea29e72c 100644 --- a/packages/core/src/providers/email.ts +++ b/packages/core/src/providers/email-smtp.ts @@ -1,6 +1,3 @@ -import type { CommonProviderOptions } from "./index.js" -import type { Awaitable, Theme } from "../types.js" - import { Transport, TransportOptions, createTransport } from "nodemailer" import * as JSONTransport from "nodemailer/lib/json-transport/index.js" import * as SendmailTransport from "nodemailer/lib/sendmail-transport/index.js" @@ -9,111 +6,32 @@ import * as SMTPTransport from "nodemailer/lib/smtp-transport/index.js" import * as SMTPPool from "nodemailer/lib/smtp-pool/index.js" import * as StreamTransport from "nodemailer/lib/stream-transport/index.js" -// TODO: Make use of https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html for the string -type AllTransportOptions = string | SMTPTransport | SMTPTransport.Options | SMTPPool | SMTPPool.Options | SendmailTransport | SendmailTransport.Options | StreamTransport | StreamTransport.Options | JSONTransport | JSONTransport.Options | SESTransport | SESTransport.Options | Transport | TransportOptions +import type { TokenConfig } from "./token" +import type { Theme } from "../types" -export interface SendVerificationRequestParams { - identifier: string - url: string - expires: Date - provider: EmailConfig - token: string - theme: Theme - request: Request -} - -/** - * The Email Provider needs to be configured with an e-mail client. - * By default, it uses `nodemailer`, which you have to install if this - * provider is present. - * - * You can use a other services as well, like: - * - [Postmark](https://postmarkapp.com) - * - [Mailgun](https://www.mailgun.com) - * - [SendGrid](https://sendgrid.com) - * - etc. - * - * [Custom email service with Auth.js](https://authjs.dev/guides/providers/email#custom-email-service) - */ -export interface EmailUserConfig { - server?: AllTransportOptions - type?: "email" - /** @default `"Auth.js "` */ - from?: string - /** - * How long until the e-mail can be used to log the user in, - * in seconds. Defaults to 1 day - * - * @default 86400 - */ - maxAge?: number - /** [Documentation](https://authjs.dev/guides/providers/email#customizing-emails) */ - sendVerificationRequest?: ( - params: SendVerificationRequestParams - ) => Awaitable - /** - * By default, we are generating a random verification token. - * You can make it predictable or modify it as you like with this method. - * - * @example - * ```ts - * Providers.Email({ - * async generateVerificationToken() { - * return "ABC123" - * } - * }) - * ``` - * [Documentation](https://authjs.dev/guides/providers/email#customizing-the-verification-token) - */ - generateVerificationToken?: () => Awaitable - /** If defined, it is used to hash the verification token when saving to the database . */ - secret?: string - /** - * Normalizes the user input before sending the verification request. - * - * ⚠️ Always make sure this method returns a single email address. - * - * @note Technically, the part of the email address local mailbox element - * (everything before the `@` symbol) should be treated as 'case sensitive' - * according to RFC 2821, but in practice this causes more problems than - * it solves, e.g.: when looking up users by e-mail from databases. - * By default, we treat email addresses as all lower case, - * but you can override this function to change this behavior. - * - * [Normalizing the email address](https://authjs.dev/reference/core/providers_email#normalizing-the-email-address) | [RFC 2821](https://tools.ietf.org/html/rfc2821) | [Email syntax](https://en.wikipedia.org/wiki/Email_address#Syntax) - */ - normalizeIdentifier?: (identifier: string) => string -} +// TODO: Make use of https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html for the string +type AllTransportOptions = + | string + | SMTPTransport + | SMTPTransport.Options + | SMTPPool + | SMTPPool.Options + | SendmailTransport + | SendmailTransport.Options + | StreamTransport + | StreamTransport.Options + | JSONTransport + | JSONTransport.Options + | SESTransport + | SESTransport.Options + | Transport + | TransportOptions -export interface EmailConfig extends CommonProviderOptions { - // defaults - id: "email" - type: "email" - name: "Email" +export interface SmtpEmailConfig extends Record { server: AllTransportOptions - from: string - maxAge: number - sendVerificationRequest: ( - params: SendVerificationRequestParams - ) => Awaitable - - /** - * This is copied into EmailConfig in parseProviders() don't use elsewhere - */ - options: EmailUserConfig - - // user options - // TODO figure out a better way than copying from EmailUserConfig - secret?: string - generateVerificationToken?: () => Awaitable - normalizeIdentifier?: (identifier: string) => string + from?: string } - -// TODO: Rename to Token provider -// when started working on https://github.com/nextauthjs/next-auth/discussions/1465 -export type EmailProviderType = "email" - /** * ## Overview * The Email provider uses email to send "magic links" that can be used to sign in, you will likely have seen these if you have used services like Slack before. @@ -346,11 +264,13 @@ export type EmailProviderType = "email" * Always make sure this returns a single e-mail address, even if multiple ones were passed in. * ::: */ -export default function Email(config: EmailUserConfig): EmailConfig { +export default function SmtpEmail( + config: SmtpEmailConfig +): TokenConfig { return { - id: "email", - type: "email", - name: "Email", + id: "smtp-email", + type: "token", + name: "SMTP Email", server: { host: "localhost", port: 25, auth: { user: "", pass: "" } }, from: "Auth.js ", maxAge: 24 * 60 * 60, @@ -362,14 +282,36 @@ export default function Email(config: EmailUserConfig): EmailConfig { to: identifier, from: provider.from, subject: `Sign in to ${host}`, - text: text({ url, host }), - html: html({ url, host, theme }), + text: emailTextBody({ url, host }), + html: emailHtmlBody({ url, host, theme }), }) const failed = result.rejected.concat(result.pending).filter(Boolean) if (failed.length) { throw new Error(`Email (${failed.join(", ")}) could not be sent`) } }, + /** + * ⚠️ Always make sure this method returns a single email address. + * + * @note Technically, the part of the email address local mailbox element + * (everything before the `@` symbol) should be treated as 'case sensitive' + * according to RFC 2821, but in practice this causes more problems than + * it solves, e.g.: when looking up users by e-mail from databases. + * By default, we treat email addresses as all lower case, + * but you can override this function to change this behavior. + * + * [Documentation](https://authjs.dev/reference/providers/email#normalizing-the-e-mail-address) | [RFC 2821](https://tools.ietf.org/html/rfc2821) | [Email syntax](https://en.wikipedia.org/wiki/Email_address#Syntax) + */ + normalizeIdentifier(email) { + if (!email) throw new Error("Missing email from request body.") + // Get the first two elements only, + // separated by `@` from user input. + let [local, domain] = email.toLowerCase().trim().split("@") + // The part before "@" can contain a "," + // but we remove it on the domain part + domain = domain.split(",")[0] + return `${local}@${domain}` + }, options: config, } } @@ -382,7 +324,7 @@ export default function Email(config: EmailUserConfig): EmailConfig { * * @note We don't add the email address to avoid needing to escape it, if you do, remember to sanitize it! */ -function html(params: { url: string; host: string; theme: Theme }) { +function emailHtmlBody(params: { url: string; host: string; theme: Theme }) { const { url, host, theme } = params const escapedHost = host.replace(/\./g, "​.") @@ -435,6 +377,6 @@ function html(params: { url: string; host: string; theme: Theme }) { } /** Email Text body (fallback for email clients that don't render HTML, e.g. feature phones) */ -function text({ url, host }: { url: string; host: string }) { +function emailTextBody({ url, host }: { url: string; host: string }) { return `Sign in to ${host}\n${url}\n\n` } diff --git a/packages/core/src/providers/index.ts b/packages/core/src/providers/index.ts index a41c566360..088bc92d84 100644 --- a/packages/core/src/providers/index.ts +++ b/packages/core/src/providers/index.ts @@ -4,17 +4,17 @@ import type { CredentialsConfig, CredentialsProviderType, } from "./credentials.js" -import type EmailProvider from "./email.js" -import type { EmailConfig, EmailProviderType } from "./email.js" +import type EmailProvider from "./email-smtp.js" import type { OAuth2Config, OAuthConfig, OAuthProviderType, OIDCConfig, } from "./oauth.js" +import type { TokenConfig, TokenProviderType } from "./token.js" export * from "./credentials.js" -export * from "./email.js" +export * from "./email-smtp.js" export * from "./oauth.js" /** @@ -25,7 +25,7 @@ export * from "./oauth.js" * @see [Email or Passwordless Authentication](https://authjs.dev/concepts/oauth) * @see [Credentials-based Authentication](https://authjs.dev/concepts/credentials) */ -export type ProviderType = "oidc" | "oauth" | "email" | "credentials" +export type ProviderType = "oidc" | "oauth" | "token" | "credentials" /** Shared across all {@link ProviderType} */ export interface CommonProviderOptions { @@ -63,11 +63,11 @@ interface InternalProviderOptions { * @see [Credentials guide](https://authjs.dev/guides/providers/credentials) */ export type Provider

= ( - | ((OIDCConfig

| OAuth2Config

| EmailConfig | CredentialsConfig) & + | ((OIDCConfig

| OAuth2Config

| TokenConfig | CredentialsConfig) & InternalProviderOptions) | (( ...args: any - ) => (OAuth2Config

| OIDCConfig

| EmailConfig | CredentialsConfig) & + ) => (OAuth2Config

| OIDCConfig

| TokenConfig | CredentialsConfig) & InternalProviderOptions) ) & InternalProviderOptions @@ -77,7 +77,7 @@ export type BuiltInProviders = Record< (config: Partial>) => OAuthConfig > & Record & - Record + Record export type AppProviders = Array< Provider | ReturnType diff --git a/packages/core/src/providers/token.ts b/packages/core/src/providers/token.ts new file mode 100644 index 0000000000..7b5c11481b --- /dev/null +++ b/packages/core/src/providers/token.ts @@ -0,0 +1,64 @@ +import type { CommonProviderOptions, ProviderType } from "./index.js" +import type { Awaitable, Theme } from "../types.js" + +export interface SendVerificationRequestParams { + identifier: string + url: string + expires: Date + provider: ProviderConfig + token: string + theme: Theme + request: Request +} + +export type TokenProviderType = Extract + +/** + * The Token Provider needs to be configured with a token provider that can send the token to the end user. + */ +export type TokenConfig = CommonProviderOptions & + ProviderConfig & { + type: "token" + maxAge?: number + /** [Documentation](https://authjs.dev/reference/providers/email#customizing-emails) */ + sendVerificationRequest: ( + params: SendVerificationRequestParams + ) => Awaitable + /** + * By default, we are generating a random verification token. + * You can make it predictable or modify it as you like with this method. + * + * @example + * ```ts + * Providers.Token({ + * async generateVerificationToken() { + * return "ABC123" + * } + * }) + * ``` + * [Documentation](https://authjs.dev/reference/providers/token#customizing-the-verification-token) + */ + generateVerificationToken?: () => Awaitable + /** If defined, it is used to hash the verification token when saving to the database . */ + secret?: string + /** + * Normalizes the user input before sending the verification request. + */ + normalizeIdentifier?: (identifier: string) => string + options?: ProviderConfig + } + +export default function Token(config: TokenConfig): TokenConfig { + return { + id: "token", + type: "token", + name: "Token", + maxAge: 24 * 60 * 60, + async sendVerificationRequest() { + throw new Error( + "Not implemented. When using the vanilla Token provider, we expect the developer to implement this method in the application code." + ) + }, + options: config, + } +} diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 9b6197c229..efa885bb3f 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -68,11 +68,11 @@ import type { LoggerInstance } from "./lib/utils/logger.js" import type { CredentialInput, CredentialsConfig, - EmailConfig, OAuthConfigInternal, OIDCConfigInternal, ProviderType, } from "./providers/index.js" +import { TokenConfig } from "./providers/token.js" export type { AuthConfig } from "./index.js" export type { LoggerInstance } @@ -471,10 +471,10 @@ export type InternalProvider = (T extends "oauth" ? OAuthConfigInternal : T extends "oidc" ? OIDCConfigInternal - : T extends "email" - ? EmailConfig : T extends "credentials" ? CredentialsConfig + : T extends "token" + ? TokenConfig : never) & { signinUrl: string /** @example `"https://example.com/api/auth/callback/id"` */