mirror of
https://github.com/SrIzan10/next-auth.git
synced 2026-05-01 10:55:20 +00:00
Compare commits
4 Commits
@auth/edge
...
feat/token
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6266aa461f | ||
|
|
c3110dc8dd | ||
|
|
c206959637 | ||
|
|
f0a1ffa551 |
238
apps/dev/nextjs/lib/db.ts
Normal file
238
apps/dev/nextjs/lib/db.ts
Normal file
@@ -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<string | null>
|
||||||
|
setItem: (key: string, value: string) => Promise<void>
|
||||||
|
deleteItems: (...keys: string[]) => Promise<void>
|
||||||
|
},
|
||||||
|
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<AdapterSession> => {
|
||||||
|
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<AdapterUser> => {
|
||||||
|
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
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,7 +13,7 @@ import Credentials from "@auth/core/providers/credentials"
|
|||||||
import Descope from "@auth/core/providers/descope"
|
import Descope from "@auth/core/providers/descope"
|
||||||
import Discord from "@auth/core/providers/discord"
|
import Discord from "@auth/core/providers/discord"
|
||||||
import DuendeIDS6 from "@auth/core/providers/duende-identity-server6"
|
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 Facebook from "@auth/core/providers/facebook"
|
||||||
import Foursquare from "@auth/core/providers/foursquare"
|
import Foursquare from "@auth/core/providers/foursquare"
|
||||||
import Freshbooks from "@auth/core/providers/freshbooks"
|
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 Vk from "@auth/core/providers/vk"
|
||||||
import Wikimedia from "@auth/core/providers/wikimedia"
|
import Wikimedia from "@auth/core/providers/wikimedia"
|
||||||
import WorkOS from "@auth/core/providers/workos"
|
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
|
// // Prisma
|
||||||
// import { PrismaClient } from "@prisma/client"
|
// import { PrismaClient } from "@prisma/client"
|
||||||
@@ -71,8 +74,22 @@ import WorkOS from "@auth/core/providers/workos"
|
|||||||
// secret: process.env.SUPABASE_SERVICE_ROLE_KEY,
|
// secret: process.env.SUPABASE_SERVICE_ROLE_KEY,
|
||||||
// })
|
// })
|
||||||
|
|
||||||
|
const db = {}
|
||||||
|
|
||||||
export const authConfig: AuthConfig = {
|
export const authConfig: AuthConfig = {
|
||||||
// adapter,
|
adapter: TestAdapter({
|
||||||
|
getItem(key) {
|
||||||
|
return db[key]
|
||||||
|
},
|
||||||
|
setItem: function (key: string, value: string): Promise<void> {
|
||||||
|
db[key] = value
|
||||||
|
return Promise.resolve()
|
||||||
|
},
|
||||||
|
deleteItems: function (...keys: string[]): Promise<void> {
|
||||||
|
keys.forEach((key) => delete db[key])
|
||||||
|
return Promise.resolve()
|
||||||
|
},
|
||||||
|
}),
|
||||||
debug: process.env.NODE_ENV !== "production",
|
debug: process.env.NODE_ENV !== "production",
|
||||||
theme: {
|
theme: {
|
||||||
logo: "https://next-auth.js.org/img/logo/logo-sm.png",
|
logo: "https://next-auth.js.org/img/logo/logo-sm.png",
|
||||||
@@ -137,11 +154,19 @@ export const authConfig: AuthConfig = {
|
|||||||
|
|
||||||
if (authConfig.adapter) {
|
if (authConfig.adapter) {
|
||||||
// TODO:
|
// TODO:
|
||||||
// authOptions.providers.unshift(
|
authConfig.providers.unshift(
|
||||||
// // NOTE: You can start a fake e-mail server with `pnpm email`
|
// NOTE: You can start a fake e-mail server with `pnpm email`
|
||||||
// // and then go to `http://localhost:1080` in the browser
|
// and then go to `http://localhost:1080` in the browser
|
||||||
// Email({ server: "smtp://127.0.0.1:1025?tls.rejectUnauthorized=false" })
|
// 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
|
// TODO: move to next-auth/edge
|
||||||
|
|||||||
@@ -313,7 +313,7 @@ export function DgraphAdapter(
|
|||||||
|
|
||||||
return format.from<any>(result)
|
return format.from<any>(result)
|
||||||
},
|
},
|
||||||
async getUserByEmail(email) {
|
async getUserByTokenId(email) {
|
||||||
const [user] = await c.run<any>(
|
const [user] = await c.run<any>(
|
||||||
/* GraphQL */ `
|
/* GraphQL */ `
|
||||||
query ($email: String = "") {
|
query ($email: String = "") {
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ export function mySqlDrizzleAdapter(
|
|||||||
|
|
||||||
return thing
|
return thing
|
||||||
},
|
},
|
||||||
async getUserByEmail(data) {
|
async getUserByTokenId(data) {
|
||||||
const user =
|
const user =
|
||||||
(await client
|
(await client
|
||||||
.select()
|
.select()
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ export function pgDrizzleAdapter(
|
|||||||
.where(eq(users.id, data))
|
.where(eq(users.id, data))
|
||||||
.then((res) => res[0] ?? null)
|
.then((res) => res[0] ?? null)
|
||||||
},
|
},
|
||||||
async getUserByEmail(data) {
|
async getUserByTokenId(data) {
|
||||||
return await client
|
return await client
|
||||||
.select()
|
.select()
|
||||||
.from(users)
|
.from(users)
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ export function SQLiteDrizzleAdapter(
|
|||||||
getUser(data) {
|
getUser(data) {
|
||||||
return client.select().from(users).where(eq(users.id, data)).get() ?? null
|
return client.select().from(users).where(eq(users.id, data)).get() ?? null
|
||||||
},
|
},
|
||||||
getUserByEmail(data) {
|
getUserByTokenId(data) {
|
||||||
return (
|
return (
|
||||||
client.select().from(users).where(eq(users.email, data)).get() ?? null
|
client.select().from(users).where(eq(users.email, data)).get() ?? null
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -213,7 +213,7 @@ export function DynamoDBAdapter(
|
|||||||
})
|
})
|
||||||
return format.from<AdapterUser>(data.Item)
|
return format.from<AdapterUser>(data.Item)
|
||||||
},
|
},
|
||||||
async getUserByEmail(email) {
|
async getUserByTokenId(email) {
|
||||||
const data = await client.query({
|
const data = await client.query({
|
||||||
TableName,
|
TableName,
|
||||||
IndexName,
|
IndexName,
|
||||||
|
|||||||
@@ -214,7 +214,7 @@ export function FaunaAdapter(f: FaunaClient): Adapter {
|
|||||||
return {
|
return {
|
||||||
createUser: async (data) => (await q(Create(Users, { data: to(data) })))!,
|
createUser: async (data) => (await q(Create(Users, { data: to(data) })))!,
|
||||||
getUser: async (id) => await q(Get(Ref(Users, id))),
|
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 }) {
|
async getUserByAccount({ provider, providerAccountId }) {
|
||||||
const key = [provider, providerAccountId]
|
const key = [provider, providerAccountId]
|
||||||
const ref = Match(AccountByProviderAndProviderAccountId, key)
|
const ref = Match(AccountByProviderAndProviderAccountId, key)
|
||||||
|
|||||||
@@ -160,7 +160,7 @@ export function FirestoreAdapter(
|
|||||||
return await getDoc(C.users.doc(id))
|
return await getDoc(C.users.doc(id))
|
||||||
},
|
},
|
||||||
|
|
||||||
async getUserByEmail(email) {
|
async getUserByTokenId(email) {
|
||||||
return await getOneDoc(C.users.where("email", "==", email))
|
return await getOneDoc(C.users.where("email", "==", email))
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -307,7 +307,7 @@ export function KyselyAdapter(db: Kysely<Database>): Adapter {
|
|||||||
if (!result) return null
|
if (!result) return null
|
||||||
return to(result, "emailVerified")
|
return to(result, "emailVerified")
|
||||||
},
|
},
|
||||||
async getUserByEmail(email) {
|
async getUserByTokenId(email) {
|
||||||
const result =
|
const result =
|
||||||
(await db
|
(await db
|
||||||
.selectFrom("User")
|
.selectFrom("User")
|
||||||
|
|||||||
@@ -205,7 +205,7 @@ export function MikroOrmAdapter<
|
|||||||
|
|
||||||
return wrap(user).toObject()
|
return wrap(user).toObject()
|
||||||
},
|
},
|
||||||
async getUserByEmail(email) {
|
async getUserByTokenId(email) {
|
||||||
const em = await getEM()
|
const em = await getEM()
|
||||||
const user = await em.findOne(UserModel, { email })
|
const user = await em.findOne(UserModel, { email })
|
||||||
if (!user) return null
|
if (!user) return null
|
||||||
|
|||||||
@@ -172,7 +172,7 @@ export function MongoDBAdapter(
|
|||||||
if (!user) return null
|
if (!user) return null
|
||||||
return from<AdapterUser>(user)
|
return from<AdapterUser>(user)
|
||||||
},
|
},
|
||||||
async getUserByEmail(email) {
|
async getUserByTokenId(email) {
|
||||||
const user = await (await db).U.findOne({ email })
|
const user = await (await db).U.findOne({ email })
|
||||||
if (!user) return null
|
if (!user) return null
|
||||||
return from<AdapterUser>(user)
|
return from<AdapterUser>(user)
|
||||||
|
|||||||
@@ -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{.*}`, {
|
return await read(`MATCH (u:User { email: $email }) RETURN u{.*}`, {
|
||||||
email,
|
email,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ export function PouchDBAdapter(options: PouchDBAdapterOptions): Adapter {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async getUserByEmail(email) {
|
async getUserByTokenId(email) {
|
||||||
const res = await (
|
const res = await (
|
||||||
pouchdb as unknown as PouchDB.Database<AdapterUser>
|
pouchdb as unknown as PouchDB.Database<AdapterUser>
|
||||||
).find({
|
).find({
|
||||||
|
|||||||
@@ -222,7 +222,7 @@ export function PrismaAdapter(p: PrismaClient): Adapter {
|
|||||||
return {
|
return {
|
||||||
createUser: (data) => p.user.create({ data }),
|
createUser: (data) => p.user.create({ data }),
|
||||||
getUser: (id) => p.user.findUnique({ where: { id } }),
|
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) {
|
async getUserByAccount(provider_providerAccountId) {
|
||||||
const account = await p.account.findUnique({
|
const account = await p.account.findUnique({
|
||||||
where: { provider_providerAccountId },
|
where: { provider_providerAccountId },
|
||||||
|
|||||||
@@ -206,7 +206,7 @@ export default function SequelizeAdapter(
|
|||||||
|
|
||||||
return userInstance?.get({ plain: true }) ?? null
|
return userInstance?.get({ plain: true }) ?? null
|
||||||
},
|
},
|
||||||
async getUserByEmail(email) {
|
async getUserByTokenId(email) {
|
||||||
await sync()
|
await sync()
|
||||||
|
|
||||||
const userInstance = await User.findOne({
|
const userInstance = await User.findOne({
|
||||||
|
|||||||
@@ -378,7 +378,7 @@ export function SupabaseAdapter(options: SupabaseAdapterOptions): Adapter {
|
|||||||
|
|
||||||
return format<AdapterUser>(data)
|
return format<AdapterUser>(data)
|
||||||
},
|
},
|
||||||
async getUserByEmail(email) {
|
async getUserByTokenId(email) {
|
||||||
const { data, error } = await supabase
|
const { data, error } = await supabase
|
||||||
.from("users")
|
.from("users")
|
||||||
.select()
|
.select()
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { createHash, randomUUID } from "crypto"
|
|||||||
const requiredMethods = [
|
const requiredMethods = [
|
||||||
"createUser",
|
"createUser",
|
||||||
"getUser",
|
"getUser",
|
||||||
"getUserByEmail",
|
"getUserByTokenId",
|
||||||
"getUserByAccount",
|
"getUserByAccount",
|
||||||
"updateUser",
|
"updateUser",
|
||||||
"linkAccount",
|
"linkAccount",
|
||||||
@@ -21,7 +21,7 @@ export interface TestOptions {
|
|||||||
account?: any
|
account?: any
|
||||||
sessionUpdateExpires?: Date
|
sessionUpdateExpires?: Date
|
||||||
verificationTokenExpires?: Date
|
verificationTokenExpires?: Date
|
||||||
},
|
}
|
||||||
db: {
|
db: {
|
||||||
/** Generates UUID v4 by default. Use it to override how the test suite should generate IDs, like user id. */
|
/** Generates UUID v4 by default. Use it to override how the test suite should generate IDs, like user id. */
|
||||||
id?: () => string
|
id?: () => string
|
||||||
@@ -78,7 +78,7 @@ export async function runBasicTests(options: TestOptions) {
|
|||||||
email: "fill@murray.com",
|
email: "fill@murray.com",
|
||||||
image: "https://www.fillmurray.com/460/300",
|
image: "https://www.fillmurray.com/460/300",
|
||||||
name: "Fill Murray",
|
name: "Fill Murray",
|
||||||
emailVerified: new Date()
|
emailVerified: new Date(),
|
||||||
}
|
}
|
||||||
|
|
||||||
if (process.env.CUSTOM_MODEL === "1") {
|
if (process.env.CUSTOM_MODEL === "1") {
|
||||||
@@ -110,7 +110,7 @@ export async function runBasicTests(options: TestOptions) {
|
|||||||
const requiredMethods = [
|
const requiredMethods = [
|
||||||
"createUser",
|
"createUser",
|
||||||
"getUser",
|
"getUser",
|
||||||
"getUserByEmail",
|
"getUserByTokenId",
|
||||||
"getUserByAccount",
|
"getUserByAccount",
|
||||||
"updateUser",
|
"updateUser",
|
||||||
"linkAccount",
|
"linkAccount",
|
||||||
@@ -138,9 +138,9 @@ export async function runBasicTests(options: TestOptions) {
|
|||||||
expect(await adapter.getUser(user.id)).toEqual(user)
|
expect(await adapter.getUser(user.id)).toEqual(user)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("getUserByEmail", async () => {
|
test("getUserByTokenId", async () => {
|
||||||
expect(await adapter.getUserByEmail("non-existent-email")).toBeNull()
|
expect(await adapter.getUserByTokenId("non-existent-email")).toBeNull()
|
||||||
expect(await adapter.getUserByEmail(user.email)).toEqual(user)
|
expect(await adapter.getUserByTokenId(user.email)).toEqual(user)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("createSession", async () => {
|
test("createSession", async () => {
|
||||||
@@ -241,7 +241,8 @@ export async function runBasicTests(options: TestOptions) {
|
|||||||
const verificationToken = {
|
const verificationToken = {
|
||||||
token: hashedToken,
|
token: hashedToken,
|
||||||
identifier,
|
identifier,
|
||||||
expires: options.fixtures?.verificationTokenExpires ?? FIFTEEN_MINUTES_FROM_NOW,
|
expires:
|
||||||
|
options.fixtures?.verificationTokenExpires ?? FIFTEEN_MINUTES_FROM_NOW,
|
||||||
}
|
}
|
||||||
await adapter.createVerificationToken?.(verificationToken)
|
await adapter.createVerificationToken?.(verificationToken)
|
||||||
|
|
||||||
@@ -260,7 +261,8 @@ export async function runBasicTests(options: TestOptions) {
|
|||||||
const verificationToken = {
|
const verificationToken = {
|
||||||
token: hashedToken,
|
token: hashedToken,
|
||||||
identifier,
|
identifier,
|
||||||
expires: options.fixtures?.verificationTokenExpires ?? FIFTEEN_MINUTES_FROM_NOW,
|
expires:
|
||||||
|
options.fixtures?.verificationTokenExpires ?? FIFTEEN_MINUTES_FROM_NOW,
|
||||||
}
|
}
|
||||||
await adapter.createVerificationToken?.(verificationToken)
|
await adapter.createVerificationToken?.(verificationToken)
|
||||||
|
|
||||||
|
|||||||
@@ -320,7 +320,7 @@ export function TypeORMAdapter(
|
|||||||
return { ...user }
|
return { ...user }
|
||||||
},
|
},
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
async getUserByEmail(email) {
|
async getUserByTokenId(email) {
|
||||||
const m = await getManager(c)
|
const m = await getManager(c)
|
||||||
const user = await m.findOne("UserEntity", { where: { email } })
|
const user = await m.findOne("UserEntity", { where: { email } })
|
||||||
if (!user) return null
|
if (!user) return null
|
||||||
|
|||||||
@@ -221,7 +221,7 @@ export function UpstashRedisAdapter(
|
|||||||
return await setUser(id, { ...user, id })
|
return await setUser(id, { ...user, id })
|
||||||
},
|
},
|
||||||
getUser,
|
getUser,
|
||||||
async getUserByEmail(email) {
|
async getUserByTokenId(email) {
|
||||||
const userId = await client.get<string>(emailKeyPrefix + email)
|
const userId = await client.get<string>(emailKeyPrefix + email)
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
return null
|
return null
|
||||||
|
|||||||
@@ -248,7 +248,7 @@ export function XataAdapter(client: XataClient): Adapter {
|
|||||||
const user = await client.db.nextauth_users.filter({ id }).getFirst()
|
const user = await client.db.nextauth_users.filter({ id }).getFirst()
|
||||||
return user ?? null
|
return user ?? null
|
||||||
},
|
},
|
||||||
async getUserByEmail(email) {
|
async getUserByTokenId(email) {
|
||||||
const user = await client.db.nextauth_users.filter({ email }).getFirst()
|
const user = await client.db.nextauth_users.filter({ email }).getFirst()
|
||||||
return user ?? null
|
return user ?? null
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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.
|
* 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
|
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 {
|
export interface AdapterAccount extends Account {
|
||||||
userId: string
|
userId: string
|
||||||
type: Extract<ProviderType, "oauth" | "oidc" | "email">
|
type: Extract<ProviderType, "oauth" | "oidc" | "token">
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -218,12 +221,14 @@ export interface VerificationToken {
|
|||||||
export interface Adapter {
|
export interface Adapter {
|
||||||
createUser?(user: Omit<AdapterUser, "id">): Awaitable<AdapterUser>
|
createUser?(user: Omit<AdapterUser, "id">): Awaitable<AdapterUser>
|
||||||
getUser?(id: string): Awaitable<AdapterUser | null>
|
getUser?(id: string): Awaitable<AdapterUser | null>
|
||||||
getUserByEmail?(email: string): Awaitable<AdapterUser | null>
|
getUserByTokenId?(id: string): Awaitable<AdapterUser | null>
|
||||||
/** Using the provider id and the id of the user for a specific account, get the user. */
|
/** Using the provider id and the id of the user for a specific account, get the user. */
|
||||||
getUserByAccount?(
|
getUserByAccount?(
|
||||||
providerAccountId: Pick<AdapterAccount, "provider" | "providerAccountId">
|
providerAccountId: Pick<AdapterAccount, "provider" | "providerAccountId">
|
||||||
): Awaitable<AdapterUser | null>
|
): Awaitable<AdapterUser | null>
|
||||||
updateUser?(user: Partial<AdapterUser> & Pick<AdapterUser, 'id'>): Awaitable<AdapterUser>
|
updateUser?(
|
||||||
|
user: Partial<AdapterUser> & Pick<AdapterUser, "id">
|
||||||
|
): Awaitable<AdapterUser>
|
||||||
/** @todo This method is currently not invoked yet. */
|
/** @todo This method is currently not invoked yet. */
|
||||||
deleteUser?(
|
deleteUser?(
|
||||||
userId: string
|
userId: string
|
||||||
|
|||||||
@@ -35,18 +35,18 @@ function isValidHttpUrl(url: string, baseUrl: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let hasCredentials = false
|
let hasCredentials = false
|
||||||
let hasEmail = false
|
let hasToken = false
|
||||||
|
|
||||||
const emailMethods = [
|
const tokenMethods = [
|
||||||
"createVerificationToken",
|
"createVerificationToken",
|
||||||
"useVerificationToken",
|
"useVerificationToken",
|
||||||
"getUserByEmail",
|
"getUserByTokenId",
|
||||||
]
|
]
|
||||||
|
|
||||||
const sessionMethods = [
|
const sessionMethods = [
|
||||||
"createUser",
|
"createUser",
|
||||||
"getUser",
|
"getUser",
|
||||||
"getUserByEmail",
|
"getUserByTokenId",
|
||||||
"getUserByAccount",
|
"getUserByAccount",
|
||||||
"updateUser",
|
"updateUser",
|
||||||
"linkAccount",
|
"linkAccount",
|
||||||
@@ -122,7 +122,7 @@ export function assertConfig(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (provider.type === "credentials") hasCredentials = true
|
if (provider.type === "credentials") hasCredentials = true
|
||||||
else if (provider.type === "email") hasEmail = true
|
else if (provider.type === "token") hasToken = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasCredentials) {
|
if (hasCredentials) {
|
||||||
@@ -149,16 +149,16 @@ export function assertConfig(
|
|||||||
|
|
||||||
const { adapter, session } = options
|
const { adapter, session } = options
|
||||||
if (
|
if (
|
||||||
hasEmail ||
|
hasToken ||
|
||||||
session?.strategy === "database" ||
|
session?.strategy === "database" ||
|
||||||
(!session?.strategy && adapter)
|
(!session?.strategy && adapter)
|
||||||
) {
|
) {
|
||||||
let methods: string[]
|
let methods: string[]
|
||||||
|
|
||||||
if (hasEmail) {
|
if (hasToken) {
|
||||||
if (!adapter)
|
if (!adapter)
|
||||||
return new MissingAdapter("Email login requires an adapter.")
|
return new MissingAdapter("Token login requires an adapter.")
|
||||||
methods = emailMethods
|
methods = tokenMethods
|
||||||
} else {
|
} else {
|
||||||
if (!adapter)
|
if (!adapter)
|
||||||
return new MissingAdapter("Database session requires an adapter.")
|
return new MissingAdapter("Database session requires an adapter.")
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export async function handleLogin(
|
|||||||
// Input validation
|
// Input validation
|
||||||
if (!_account?.providerAccountId || !_account.type)
|
if (!_account?.providerAccountId || !_account.type)
|
||||||
throw new Error("Missing or invalid provider account")
|
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")
|
throw new Error("Provider not supported")
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -56,7 +56,7 @@ export async function handleLogin(
|
|||||||
updateUser,
|
updateUser,
|
||||||
getUser,
|
getUser,
|
||||||
getUserByAccount,
|
getUserByAccount,
|
||||||
getUserByEmail,
|
getUserByTokenId,
|
||||||
linkAccount,
|
linkAccount,
|
||||||
createSession,
|
createSession,
|
||||||
getSessionAndUser,
|
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
|
// If signing in with an email, check if an account with the same email address exists already
|
||||||
const userByEmail = await getUserByEmail(profile.email)
|
const userByTokenId = await getUserByTokenId(
|
||||||
if (userByEmail) {
|
profile.tokenId ?? profile.email
|
||||||
|
)
|
||||||
|
if (userByTokenId) {
|
||||||
// If they are not already signed in as the same user, this flow will
|
// 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
|
// 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.
|
// 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
|
// This will switch user accounts for the session in cases where the user was
|
||||||
// already logged in with a different account.
|
// already logged in with a different account.
|
||||||
await deleteSession(sessionToken)
|
await deleteSession(sessionToken)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const updateUserPayload: Parameters<typeof updateUser>[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
|
// Update emailVerified property on the user object
|
||||||
user = await updateUser({ id: userByEmail.id, emailVerified: new Date() })
|
user = await updateUser(updateUserPayload)
|
||||||
await events.updateUser?.({ user })
|
await events.updateUser?.({ user })
|
||||||
} else {
|
} 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
|
// Create user account if there isn't one for the email address already
|
||||||
user = await createUser(newUser)
|
user = await createUser(newUser)
|
||||||
await events.createUser?.({ user })
|
await events.createUser?.({ user })
|
||||||
@@ -187,15 +205,15 @@ export async function handleLogin(
|
|||||||
//
|
//
|
||||||
// OAuth providers should require email address verification to prevent this, but in
|
// OAuth providers should require email address verification to prevent this, but in
|
||||||
// practice that is not always the case; this helps protect against that.
|
// practice that is not always the case; this helps protect against that.
|
||||||
const userByEmail = profile.email
|
const userByTokenId = profile.tokenId
|
||||||
? await getUserByEmail(profile.email)
|
? await getUserByTokenId(profile.tokenId)
|
||||||
: null
|
: null
|
||||||
if (userByEmail) {
|
if (userByTokenId) {
|
||||||
const provider = options.provider as OAuthConfig<any>
|
const provider = options.provider as OAuthConfig<any>
|
||||||
if (provider?.allowDangerousEmailAccountLinking) {
|
if (provider?.allowDangerousEmailAccountLinking) {
|
||||||
// If you trust the oauth provider to correctly verify email addresses, you can opt-in to
|
// 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.
|
// account linking even when the user is not signed-in.
|
||||||
user = userByEmail
|
user = userByTokenId
|
||||||
} else {
|
} else {
|
||||||
// We end up here when we don't have an account with the same [provider].id *BUT*
|
// 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
|
// we do already have an account with the same email address as the one in the
|
||||||
|
|||||||
@@ -51,8 +51,8 @@ export default function renderPage(params: RenderPageParams) {
|
|||||||
// We only want to render providers
|
// We only want to render providers
|
||||||
providers: params.providers?.filter(
|
providers: params.providers?.filter(
|
||||||
(provider) =>
|
(provider) =>
|
||||||
// Always render oauth and email type providers
|
// Always render oauth and token type providers
|
||||||
["email", "oauth", "oidc"].includes(provider.type) ||
|
["token", "oauth", "oidc"].includes(provider.type) ||
|
||||||
// Only render credentials type provider if credentials are defined
|
// Only render credentials type provider if credentials are defined
|
||||||
(provider.type === "credentials" && provider.credentials) ||
|
(provider.type === "credentials" && provider.credentials) ||
|
||||||
// Don't render other provider types
|
// Don't render other provider types
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export default function SigninPage(props: {
|
|||||||
csrfToken: string
|
csrfToken: string
|
||||||
providers: InternalProvider[]
|
providers: InternalProvider[]
|
||||||
callbackUrl: string
|
callbackUrl: string
|
||||||
email: string
|
token: string
|
||||||
error?: SignInPageErrorParam
|
error?: SignInPageErrorParam
|
||||||
theme: Theme
|
theme: Theme
|
||||||
}) {
|
}) {
|
||||||
@@ -36,7 +36,7 @@ export default function SigninPage(props: {
|
|||||||
providers = [],
|
providers = [],
|
||||||
callbackUrl,
|
callbackUrl,
|
||||||
theme,
|
theme,
|
||||||
email,
|
token,
|
||||||
error: errorType,
|
error: errorType,
|
||||||
} = props
|
} = props
|
||||||
|
|
||||||
@@ -131,25 +131,25 @@ export default function SigninPage(props: {
|
|||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
) : null}
|
) : null}
|
||||||
{(provider.type === "email" || provider.type === "credentials") &&
|
{(provider.type === "token" || provider.type === "credentials") &&
|
||||||
i > 0 &&
|
i > 0 &&
|
||||||
providers[i - 1].type !== "email" &&
|
providers[i - 1].type !== "token" &&
|
||||||
providers[i - 1].type !== "credentials" && <hr />}
|
providers[i - 1].type !== "credentials" && <hr />}
|
||||||
{provider.type === "email" && (
|
{provider.type === "token" && (
|
||||||
<form action={provider.signinUrl} method="POST">
|
<form action={provider.signinUrl} method="POST">
|
||||||
<input type="hidden" name="csrfToken" value={csrfToken} />
|
<input type="hidden" name="csrfToken" value={csrfToken} />
|
||||||
<label
|
<label
|
||||||
className="section-header"
|
className="section-header"
|
||||||
htmlFor={`input-email-for-${provider.id}-provider`}
|
htmlFor={`input-for-${provider.id}-provider`}
|
||||||
>
|
>
|
||||||
Email
|
Token
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
id={`input-email-for-${provider.id}-provider`}
|
id={`input-for-${provider.id}-provider`}
|
||||||
autoFocus
|
autoFocus
|
||||||
type="email"
|
type="tokenId"
|
||||||
name="email"
|
name="tokenId"
|
||||||
value={email}
|
value={token}
|
||||||
placeholder="email@example.com"
|
placeholder="email@example.com"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
@@ -185,7 +185,7 @@ export default function SigninPage(props: {
|
|||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
)}
|
)}
|
||||||
{(provider.type === "email" || provider.type === "credentials") &&
|
{(provider.type === "token" || provider.type === "credentials") &&
|
||||||
i + 1 < providers.length && <hr />}
|
i + 1 < providers.length && <hr />}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -23,8 +23,8 @@ export default function VerifyRequestPage(props: VerifyRequestPageProps) {
|
|||||||
)}
|
)}
|
||||||
<div className="card">
|
<div className="card">
|
||||||
{theme.logo && <img src={theme.logo} alt="Logo" className="logo" />}
|
{theme.logo && <img src={theme.logo} alt="Logo" className="logo" />}
|
||||||
<h1>Check your email</h1>
|
<h1>Verification sent</h1>
|
||||||
<p>A sign in link has been sent to your email address.</p>
|
<p>A sign in link has been sent to you.</p>
|
||||||
<p>
|
<p>
|
||||||
<a className="site" href={url.origin}>
|
<a className="site" href={url.origin}>
|
||||||
{url.host}
|
{url.host}
|
||||||
|
|||||||
@@ -104,6 +104,7 @@ export async function callback(params: {
|
|||||||
|
|
||||||
const unauthorizedOrError = await handleAuthorized(
|
const unauthorizedOrError = await handleAuthorized(
|
||||||
{
|
{
|
||||||
|
// @ts-expect-error
|
||||||
user: userByAccountOrFromProvider,
|
user: userByAccountOrFromProvider,
|
||||||
account,
|
account,
|
||||||
profile: OAuthProfile,
|
profile: OAuthProfile,
|
||||||
@@ -180,14 +181,14 @@ export async function callback(params: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return { redirect: callbackUrl, cookies }
|
return { redirect: callbackUrl, cookies }
|
||||||
} else if (provider.type === "email") {
|
} else if (provider.type === "token") {
|
||||||
const token = query?.token as string | undefined
|
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(
|
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.",
|
"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, hasEmail: !!identifier } }
|
{ cause: { hasToken: !!token, hasIdentifier: !!tokenId } }
|
||||||
)
|
)
|
||||||
e.name = "Configuration"
|
e.name = "Configuration"
|
||||||
throw e
|
throw e
|
||||||
@@ -196,7 +197,7 @@ export async function callback(params: {
|
|||||||
const secret = provider.secret ?? options.secret
|
const secret = provider.secret ?? options.secret
|
||||||
// @ts-expect-error -- Verified in `assertConfig`.
|
// @ts-expect-error -- Verified in `assertConfig`.
|
||||||
const invite = await adapter.useVerificationToken({
|
const invite = await adapter.useVerificationToken({
|
||||||
identifier,
|
identifier: tokenId,
|
||||||
token: await createHash(`${token}${secret}`),
|
token: await createHash(`${token}${secret}`),
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -205,16 +206,18 @@ export async function callback(params: {
|
|||||||
const invalidInvite = !hasInvite || expired
|
const invalidInvite = !hasInvite || expired
|
||||||
if (invalidInvite) throw new Verification({ hasInvite, expired })
|
if (invalidInvite) throw new Verification({ hasInvite, expired })
|
||||||
|
|
||||||
const user = (await adapter!.getUserByEmail(identifier)) ?? {
|
const user = (await adapter!.getUserByTokenId(tokenId)) ?? {
|
||||||
id: identifier,
|
id: tokenId,
|
||||||
email: identifier,
|
email: tokenId,
|
||||||
|
tokenId,
|
||||||
emailVerified: null,
|
emailVerified: null,
|
||||||
|
tokenVerified: null,
|
||||||
}
|
}
|
||||||
|
|
||||||
const account: Account = {
|
const account: Account = {
|
||||||
providerAccountId: user.email,
|
providerAccountId: user.tokenId,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
type: "email" as const,
|
type: provider.type,
|
||||||
provider: provider.id,
|
provider: provider.id,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -315,8 +318,7 @@ export async function callback(params: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @type {import("src").Account} */
|
const account: Account = {
|
||||||
const account = {
|
|
||||||
providerAccountId: user.id,
|
providerAccountId: user.id,
|
||||||
type: "credentials",
|
type: "credentials",
|
||||||
provider: provider.id,
|
provider: provider.id,
|
||||||
@@ -339,7 +341,6 @@ export async function callback(params: {
|
|||||||
const token = await callbacks.jwt({
|
const token = await callbacks.jwt({
|
||||||
token: defaultToken,
|
token: defaultToken,
|
||||||
user,
|
user,
|
||||||
// @ts-expect-error
|
|
||||||
account,
|
account,
|
||||||
isNewUser: false,
|
isNewUser: false,
|
||||||
trigger: "signIn",
|
trigger: "signIn",
|
||||||
@@ -363,7 +364,6 @@ export async function callback(params: {
|
|||||||
cookies.push(...sessionCookies)
|
cookies.push(...sessionCookies)
|
||||||
}
|
}
|
||||||
|
|
||||||
// @ts-expect-error
|
|
||||||
await events.signIn?.({ user, account })
|
await events.signIn?.({ user, account })
|
||||||
|
|
||||||
return { redirect: callbackUrl, cookies }
|
return { redirect: callbackUrl, cookies }
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { AuthorizedCallbackError } from "../../errors.js"
|
|||||||
import { InternalOptions } from "../../types.js"
|
import { InternalOptions } from "../../types.js"
|
||||||
|
|
||||||
export async function handleAuthorized(
|
export async function handleAuthorized(
|
||||||
params: any,
|
params: Parameters<typeof signIn>[0],
|
||||||
{ url, logger, callbacks: { signIn } }: InternalOptions
|
{ url, logger, callbacks: { signIn } }: InternalOptions
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import emailSignin from "../email/signin.js"
|
import tokenSignin from "../token/signin.js"
|
||||||
import { SignInError } from "../../errors.js"
|
import { SignInError } from "../../errors.js"
|
||||||
import { getAuthorizationUrl } from "../oauth/authorization-url.js"
|
import { getAuthorizationUrl } from "../oauth/authorization-url.js"
|
||||||
import { handleAuthorized } from "./shared.js"
|
import { handleAuthorized } from "./shared.js"
|
||||||
@@ -17,27 +17,28 @@ import type {
|
|||||||
*/
|
*/
|
||||||
export async function signin(
|
export async function signin(
|
||||||
request: RequestInternal,
|
request: RequestInternal,
|
||||||
options: InternalOptions<"oauth" | "oidc" | "email">
|
options: InternalOptions<"oauth" | "oidc" | "token">
|
||||||
): Promise<ResponseInternal> {
|
): Promise<ResponseInternal> {
|
||||||
const { query, body } = request
|
const { query, body } = request
|
||||||
const { url, logger, provider } = options
|
const { url, logger, provider } = options
|
||||||
try {
|
try {
|
||||||
if (provider.type === "oauth" || provider.type === "oidc") {
|
if (provider.type === "oauth" || provider.type === "oidc") {
|
||||||
return await getAuthorizationUrl(query, options)
|
return await getAuthorizationUrl(query, options)
|
||||||
} else if (provider.type === "email") {
|
} else if (provider.type === "token") {
|
||||||
const normalizer = provider.normalizeIdentifier ?? defaultNormalizer
|
const tokenId = provider.normalizeIdentifier?.(body?.tokenId) ?? ""
|
||||||
const email = normalizer(body?.email)
|
|
||||||
|
|
||||||
const user = (await options.adapter!.getUserByEmail(email)) ?? {
|
const user = (await options.adapter!.getUserByTokenId(tokenId)) ?? {
|
||||||
id: email,
|
id: tokenId,
|
||||||
email,
|
email: tokenId,
|
||||||
|
tokenId,
|
||||||
|
tokenVerified: null,
|
||||||
emailVerified: null,
|
emailVerified: null,
|
||||||
}
|
}
|
||||||
|
|
||||||
const account: Account = {
|
const account: Account = {
|
||||||
providerAccountId: email,
|
providerAccountId: tokenId,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
type: "email",
|
type: "token",
|
||||||
provider: provider.id,
|
provider: provider.id,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,27 +49,16 @@ export async function signin(
|
|||||||
|
|
||||||
if (unauthorizedOrError) return unauthorizedOrError
|
if (unauthorizedOrError) return unauthorizedOrError
|
||||||
|
|
||||||
const redirect = await emailSignin(email, options, request)
|
const redirect = await tokenSignin(tokenId, options, request)
|
||||||
return { redirect }
|
return { redirect }
|
||||||
}
|
}
|
||||||
return { redirect: `${url}/signin` }
|
return { redirect: `${url}/signin` }
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const error = new SignInError(e as Error, { provider: provider.id })
|
const error = new SignInError(e as Error, { provider: provider.id })
|
||||||
logger.error(error)
|
logger.error(error)
|
||||||
const code = provider.type === "email" ? "EmailSignin" : "OAuthSignin"
|
const code = provider.type === "token" ? "TokenSignin" : "OAuthSignin"
|
||||||
url.searchParams.set("error", code)
|
url.searchParams.set("error", code)
|
||||||
url.pathname += "/signin"
|
url.pathname += "/signin"
|
||||||
return { redirect: url.toString() }
|
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}`
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ import { createHash, randomString, toRequest } from "../web.js"
|
|||||||
|
|
||||||
import type { InternalOptions, RequestInternal } from "../../types.js"
|
import type { InternalOptions, RequestInternal } from "../../types.js"
|
||||||
/**
|
/**
|
||||||
* Starts an e-mail login flow, by generating a token,
|
* Starts an token login flow, by generating a token,
|
||||||
* and sending it to the user's e-mail (with the help of a DB adapter)
|
* 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,
|
identifier: string,
|
||||||
options: InternalOptions<"email">,
|
options: InternalOptions<"token">,
|
||||||
request: RequestInternal
|
request: RequestInternal
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const { url, adapter, provider, callbackUrl, theme } = options
|
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
|
Date.now() + (provider.maxAge ?? ONE_DAY_IN_SECONDS) * 1000
|
||||||
)
|
)
|
||||||
|
|
||||||
// Generate a link with email, unhashed token and callback url
|
// Generate a link with token, unhashed token and callback url
|
||||||
const params = new URLSearchParams({ callbackUrl, token, email: identifier })
|
const params = new URLSearchParams({
|
||||||
|
callbackUrl,
|
||||||
|
token,
|
||||||
|
tokenId: identifier,
|
||||||
|
})
|
||||||
const _url = `${url}/callback/${provider.id}?${params}`
|
const _url = `${url}/callback/${provider.id}?${params}`
|
||||||
|
|
||||||
const secret = provider.secret ?? options.secret
|
const secret = provider.secret ?? options.secret
|
||||||
@@ -1,6 +1,3 @@
|
|||||||
import type { CommonProviderOptions } from "./index.js"
|
|
||||||
import type { Awaitable, Theme } from "../types.js"
|
|
||||||
|
|
||||||
import { Transport, TransportOptions, createTransport } from "nodemailer"
|
import { Transport, TransportOptions, createTransport } from "nodemailer"
|
||||||
import * as JSONTransport from "nodemailer/lib/json-transport/index.js"
|
import * as JSONTransport from "nodemailer/lib/json-transport/index.js"
|
||||||
import * as SendmailTransport from "nodemailer/lib/sendmail-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 SMTPPool from "nodemailer/lib/smtp-pool/index.js"
|
||||||
import * as StreamTransport from "nodemailer/lib/stream-transport/index.js"
|
import * as StreamTransport from "nodemailer/lib/stream-transport/index.js"
|
||||||
|
|
||||||
|
import type { TokenConfig } from "./token"
|
||||||
|
import type { Theme } from "../types"
|
||||||
|
|
||||||
// TODO: Make use of https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html for the 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<any> | TransportOptions
|
type AllTransportOptions =
|
||||||
|
| string
|
||||||
|
| SMTPTransport
|
||||||
|
| SMTPTransport.Options
|
||||||
|
| SMTPPool
|
||||||
|
| SMTPPool.Options
|
||||||
|
| SendmailTransport
|
||||||
|
| SendmailTransport.Options
|
||||||
|
| StreamTransport
|
||||||
|
| StreamTransport.Options
|
||||||
|
| JSONTransport
|
||||||
|
| JSONTransport.Options
|
||||||
|
| SESTransport
|
||||||
|
| SESTransport.Options
|
||||||
|
| Transport<any>
|
||||||
|
| TransportOptions
|
||||||
|
|
||||||
export interface SendVerificationRequestParams {
|
export interface SmtpEmailConfig extends Record<string, unknown> {
|
||||||
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 <no-reply@authjs.dev>"` */
|
|
||||||
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<void>
|
|
||||||
/**
|
|
||||||
* 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<string>
|
|
||||||
/** 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
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface EmailConfig extends CommonProviderOptions {
|
|
||||||
// defaults
|
|
||||||
id: "email"
|
|
||||||
type: "email"
|
|
||||||
name: "Email"
|
|
||||||
server: AllTransportOptions
|
server: AllTransportOptions
|
||||||
from: string
|
from?: string
|
||||||
maxAge: number
|
|
||||||
sendVerificationRequest: (
|
|
||||||
params: SendVerificationRequestParams
|
|
||||||
) => Awaitable<void>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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<string>
|
|
||||||
normalizeIdentifier?: (identifier: string) => string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// TODO: Rename to Token provider
|
|
||||||
// when started working on https://github.com/nextauthjs/next-auth/discussions/1465
|
|
||||||
export type EmailProviderType = "email"
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ## Overview
|
* ## 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.
|
* 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.
|
* 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<SmtpEmailConfig> {
|
||||||
return {
|
return {
|
||||||
id: "email",
|
id: "smtp-email",
|
||||||
type: "email",
|
type: "token",
|
||||||
name: "Email",
|
name: "SMTP Email",
|
||||||
server: { host: "localhost", port: 25, auth: { user: "", pass: "" } },
|
server: { host: "localhost", port: 25, auth: { user: "", pass: "" } },
|
||||||
from: "Auth.js <no-reply@authjs.dev>",
|
from: "Auth.js <no-reply@authjs.dev>",
|
||||||
maxAge: 24 * 60 * 60,
|
maxAge: 24 * 60 * 60,
|
||||||
@@ -362,14 +282,36 @@ export default function Email(config: EmailUserConfig): EmailConfig {
|
|||||||
to: identifier,
|
to: identifier,
|
||||||
from: provider.from,
|
from: provider.from,
|
||||||
subject: `Sign in to ${host}`,
|
subject: `Sign in to ${host}`,
|
||||||
text: text({ url, host }),
|
text: emailTextBody({ url, host }),
|
||||||
html: html({ url, host, theme }),
|
html: emailHtmlBody({ url, host, theme }),
|
||||||
})
|
})
|
||||||
const failed = result.rejected.concat(result.pending).filter(Boolean)
|
const failed = result.rejected.concat(result.pending).filter(Boolean)
|
||||||
if (failed.length) {
|
if (failed.length) {
|
||||||
throw new Error(`Email (${failed.join(", ")}) could not be sent`)
|
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,
|
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!
|
* @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 { url, host, theme } = params
|
||||||
|
|
||||||
const escapedHost = host.replace(/\./g, "​.")
|
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) */
|
/** 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`
|
return `Sign in to ${host}\n${url}\n\n`
|
||||||
}
|
}
|
||||||
@@ -4,17 +4,17 @@ import type {
|
|||||||
CredentialsConfig,
|
CredentialsConfig,
|
||||||
CredentialsProviderType,
|
CredentialsProviderType,
|
||||||
} from "./credentials.js"
|
} from "./credentials.js"
|
||||||
import type EmailProvider from "./email.js"
|
import type EmailProvider from "./email-smtp.js"
|
||||||
import type { EmailConfig, EmailProviderType } from "./email.js"
|
|
||||||
import type {
|
import type {
|
||||||
OAuth2Config,
|
OAuth2Config,
|
||||||
OAuthConfig,
|
OAuthConfig,
|
||||||
OAuthProviderType,
|
OAuthProviderType,
|
||||||
OIDCConfig,
|
OIDCConfig,
|
||||||
} from "./oauth.js"
|
} from "./oauth.js"
|
||||||
|
import type { TokenConfig, TokenProviderType } from "./token.js"
|
||||||
|
|
||||||
export * from "./credentials.js"
|
export * from "./credentials.js"
|
||||||
export * from "./email.js"
|
export * from "./email-smtp.js"
|
||||||
export * from "./oauth.js"
|
export * from "./oauth.js"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -25,7 +25,7 @@ export * from "./oauth.js"
|
|||||||
* @see [Email or Passwordless Authentication](https://authjs.dev/concepts/oauth)
|
* @see [Email or Passwordless Authentication](https://authjs.dev/concepts/oauth)
|
||||||
* @see [Credentials-based Authentication](https://authjs.dev/concepts/credentials)
|
* @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} */
|
/** Shared across all {@link ProviderType} */
|
||||||
export interface CommonProviderOptions {
|
export interface CommonProviderOptions {
|
||||||
@@ -63,11 +63,11 @@ interface InternalProviderOptions {
|
|||||||
* @see [Credentials guide](https://authjs.dev/guides/providers/credentials)
|
* @see [Credentials guide](https://authjs.dev/guides/providers/credentials)
|
||||||
*/
|
*/
|
||||||
export type Provider<P extends Profile = any> = (
|
export type Provider<P extends Profile = any> = (
|
||||||
| ((OIDCConfig<P> | OAuth2Config<P> | EmailConfig | CredentialsConfig) &
|
| ((OIDCConfig<P> | OAuth2Config<P> | TokenConfig | CredentialsConfig) &
|
||||||
InternalProviderOptions)
|
InternalProviderOptions)
|
||||||
| ((
|
| ((
|
||||||
...args: any
|
...args: any
|
||||||
) => (OAuth2Config<P> | OIDCConfig<P> | EmailConfig | CredentialsConfig) &
|
) => (OAuth2Config<P> | OIDCConfig<P> | TokenConfig | CredentialsConfig) &
|
||||||
InternalProviderOptions)
|
InternalProviderOptions)
|
||||||
) &
|
) &
|
||||||
InternalProviderOptions
|
InternalProviderOptions
|
||||||
@@ -77,7 +77,7 @@ export type BuiltInProviders = Record<
|
|||||||
(config: Partial<OAuthConfig<any>>) => OAuthConfig<any>
|
(config: Partial<OAuthConfig<any>>) => OAuthConfig<any>
|
||||||
> &
|
> &
|
||||||
Record<CredentialsProviderType, typeof CredentialsProvider> &
|
Record<CredentialsProviderType, typeof CredentialsProvider> &
|
||||||
Record<EmailProviderType, typeof EmailProvider>
|
Record<TokenProviderType, typeof EmailProvider>
|
||||||
|
|
||||||
export type AppProviders = Array<
|
export type AppProviders = Array<
|
||||||
Provider | ReturnType<BuiltInProviders[keyof BuiltInProviders]>
|
Provider | ReturnType<BuiltInProviders[keyof BuiltInProviders]>
|
||||||
|
|||||||
64
packages/core/src/providers/token.ts
Normal file
64
packages/core/src/providers/token.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import type { CommonProviderOptions, ProviderType } from "./index.js"
|
||||||
|
import type { Awaitable, Theme } from "../types.js"
|
||||||
|
|
||||||
|
export interface SendVerificationRequestParams<ProviderConfig> {
|
||||||
|
identifier: string
|
||||||
|
url: string
|
||||||
|
expires: Date
|
||||||
|
provider: ProviderConfig
|
||||||
|
token: string
|
||||||
|
theme: Theme
|
||||||
|
request: Request
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TokenProviderType = Extract<ProviderType, "email" | "token">
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Token Provider needs to be configured with a token provider that can send the token to the end user.
|
||||||
|
*/
|
||||||
|
export type TokenConfig<ProviderConfig = {}> = CommonProviderOptions &
|
||||||
|
ProviderConfig & {
|
||||||
|
type: "token"
|
||||||
|
maxAge?: number
|
||||||
|
/** [Documentation](https://authjs.dev/reference/providers/email#customizing-emails) */
|
||||||
|
sendVerificationRequest: (
|
||||||
|
params: SendVerificationRequestParams<ProviderConfig>
|
||||||
|
) => Awaitable<void>
|
||||||
|
/**
|
||||||
|
* 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<string>
|
||||||
|
/** 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -68,11 +68,11 @@ import type { LoggerInstance } from "./lib/utils/logger.js"
|
|||||||
import type {
|
import type {
|
||||||
CredentialInput,
|
CredentialInput,
|
||||||
CredentialsConfig,
|
CredentialsConfig,
|
||||||
EmailConfig,
|
|
||||||
OAuthConfigInternal,
|
OAuthConfigInternal,
|
||||||
OIDCConfigInternal,
|
OIDCConfigInternal,
|
||||||
ProviderType,
|
ProviderType,
|
||||||
} from "./providers/index.js"
|
} from "./providers/index.js"
|
||||||
|
import { TokenConfig } from "./providers/token.js"
|
||||||
|
|
||||||
export type { AuthConfig } from "./index.js"
|
export type { AuthConfig } from "./index.js"
|
||||||
export type { LoggerInstance }
|
export type { LoggerInstance }
|
||||||
@@ -471,10 +471,10 @@ export type InternalProvider<T = ProviderType> = (T extends "oauth"
|
|||||||
? OAuthConfigInternal<any>
|
? OAuthConfigInternal<any>
|
||||||
: T extends "oidc"
|
: T extends "oidc"
|
||||||
? OIDCConfigInternal<any>
|
? OIDCConfigInternal<any>
|
||||||
: T extends "email"
|
|
||||||
? EmailConfig
|
|
||||||
: T extends "credentials"
|
: T extends "credentials"
|
||||||
? CredentialsConfig
|
? CredentialsConfig
|
||||||
|
: T extends "token"
|
||||||
|
? TokenConfig
|
||||||
: never) & {
|
: never) & {
|
||||||
signinUrl: string
|
signinUrl: string
|
||||||
/** @example `"https://example.com/api/auth/callback/id"` */
|
/** @example `"https://example.com/api/auth/callback/id"` */
|
||||||
|
|||||||
Reference in New Issue
Block a user