diff --git a/.gitignore b/.gitignore index f1cc5554ca..9c9a3ce2f1 100644 --- a/.gitignore +++ b/.gitignore @@ -51,6 +51,8 @@ app/next-auth app/dist/css app/package-lock.json app/yarn.lock +app/prisma/migrations +app/prisma/dev.db* # VS /.vs/slnx.sqlite-journal diff --git a/app/.env.local.example b/app/.env.local.example index a9f2ce3a18..6f0e347bde 100644 --- a/app/.env.local.example +++ b/app/.env.local.example @@ -7,7 +7,7 @@ NEXTAUTH_URL=http://localhost:3000 # https://generate-secret.vercel.app/32 to generate a secret. # Note: Changing a secret may invalidate existing sessions # and/or verification tokens. -SECRET= +SECRET=secret AUTH0_ID= AUTH0_SECRET= @@ -24,15 +24,4 @@ TWITCH_ID= TWITCH_SECRET= TWITTER_ID= -TWITTER_SECRET= - -# Example configuration for a Gmail account (will need SMTP enabled) -EMAIL_SERVER=smtps://user@gmail.com:password@smtp.gmail.com:465 -EMAIL_FROM=user@gmail.com - -# Note: If using with Prisma adapter, you need to use a `.env` -# file rather than a `.env.local` file to configure env vars. -# Postgres: DATABASE_URL=postgres://nextauth:password@127.0.0.1:5432/nextauth?synchronize=true -# MySQL: DATABASE_URL=mysql://nextauth:password@127.0.0.1:3306/nextauth?synchronize=true -# MongoDB: DATABASE_URL=mongodb://nextauth:password@127.0.0.1:27017/nextauth?synchronize=true -DATABASE_URL= \ No newline at end of file +TWITTER_SECRET= \ No newline at end of file diff --git a/app/next-env.d.ts b/app/next-env.d.ts index 7b7aa2c772..9bc3dd46b9 100644 --- a/app/next-env.d.ts +++ b/app/next-env.d.ts @@ -1,2 +1,6 @@ /// /// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/app/package.json b/app/package.json index 2318c95a85..f2d0e87d47 100644 --- a/app/package.json +++ b/app/package.json @@ -10,17 +10,21 @@ "copy:css": "cpx \"../dist/css/**/*\" dist/css --watch", "watch:css": "cd .. && npm run watch:css", "dev:css": "npm-run-all --parallel watch:css copy:css", - "start": "next start" + "start": "next start", + "start:email": "npx fake-smtp-server" }, "license": "ISC", "dependencies": { - "next": "^11.0.1", - "nodemailer": "^6.6.1", + "@prisma/client": "^2.29.1", + "fake-smtp-server": "^0.8.0", + "next": "^11.1.0", + "nodemailer": "^6.6.3", "react": "^17.0.2", "react-dom": "^17.0.2" }, "devDependencies": { "cpx": "^1.5.0", - "npm-run-all": "^4.1.5" + "npm-run-all": "^4.1.5", + "prisma": "^2.29.1" } } diff --git a/app/pages/api/auth/[...nextauth].js b/app/pages/api/auth/[...nextauth].ts similarity index 89% rename from app/pages/api/auth/[...nextauth].js rename to app/pages/api/auth/[...nextauth].ts index 1048acedcf..d6ea8325fb 100644 --- a/app/pages/api/auth/[...nextauth].js +++ b/app/pages/api/auth/[...nextauth].ts @@ -16,14 +16,23 @@ import LineProvider from "next-auth/providers/line" import LinkedInProvider from "next-auth/providers/linkedin" import MailchimpProvider from "next-auth/providers/mailchimp" import DiscordProvider from "next-auth/providers/discord" -import OneLoginProvider from "next-auth/providers/onelogin" +import { PrismaAdapter } from "@next-auth/prisma-adapter" +import { PrismaClient } from "@prisma/client" +const prisma = new PrismaClient() export default NextAuth({ + adapter: PrismaAdapter(prisma), providers: [ // E-mail + // Start fake e-mail server with `npm run start:email` EmailProvider({ - server: process.env.EMAIL_SERVER, - from: process.env.EMAIL_FROM, + server: { + host: "127.0.0.1", + auth: null, + secure: false, + port: 1025, + tls: { rejectUnauthorized: false }, + }, }), // Credentials CredentialsProvider({ @@ -34,7 +43,6 @@ export default NextAuth({ async authorize(credentials, req) { if (credentials.password === "password") { return { - id: 1, name: "Fill Murray", email: "bill@fillmurray.com", image: "https://www.fillmurray.com/64/64", @@ -107,12 +115,10 @@ export default NextAuth({ clientId: process.env.DISCORD_ID, clientSecret: process.env.DISCORD_SECRET, }), - OneLoginProvider({ - clientId: process.env.ONELOGIN_ID, - clientSecret: process.env.ONELOGIN_SECRET, - issuer: process.env.ONELOGIN_ISSUER, - }), ], + session: { + jwt: true, + }, jwt: { encryption: true, secret: process.env.SECRET, diff --git a/app/prisma/schema.prisma b/app/prisma/schema.prisma index 2a520f67e3..476ca5ce2a 100644 --- a/app/prisma/schema.prisma +++ b/app/prisma/schema.prisma @@ -1,63 +1,57 @@ -generator client { - provider = "prisma-client-js" +datasource db { + provider = "sqlite" + url = "file:./dev.db" } -datasource db { - provider = "postgresql" - url = env("DATABASE_URL") +generator client { + provider = "prisma-client-js" } model Account { - id Int @default(autoincrement()) @id - compoundId String @unique @map(name: "compound_id") - userId Int @map(name: "user_id") - providerType String @map(name: "provider_type") - providerId String @map(name: "provider_id") - providerAccountId String @map(name: "provider_account_id") - refreshToken String? @map(name: "refresh_token") - accessToken String? @map(name: "access_token") - accessTokenExpires DateTime? @map(name: "access_token_expires") - createdAt DateTime @default(now()) @map(name: "created_at") - updatedAt DateTime @default(now()) @map(name: "updated_at") - - @@index([providerAccountId], name: "providerAccountId") - @@index([providerId], name: "providerId") - @@index([userId], name: "userId") - - @@map(name: "accounts") + id String @id @default(cuid()) + userId String + type String + provider String + providerAccountId String + refresh_token String? + access_token String? + expires_at Int? + token_type String? + scope String? + id_token String? + session_state String? + oauth_token_secret String? + oauth_token String? + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + user User @relation(fields: [userId], references: [id]) + + @@unique([provider, providerAccountId]) } model Session { - id Int @default(autoincrement()) @id - userId Int @map(name: "user_id") + id String @id @default(cuid()) + sessionToken String @unique + userId String expires DateTime - sessionToken String @unique @map(name: "session_token") - accessToken String @unique @map(name: "access_token") - createdAt DateTime @default(now()) @map(name: "created_at") - updatedAt DateTime @default(now()) @map(name: "updated_at") - - @@map(name: "sessions") + user User @relation(fields: [userId], references: [id]) } model User { - id Int @default(autoincrement()) @id + id String @id @default(cuid()) name String? email String? @unique - emailVerified DateTime? @map(name: "email_verified") + emailVerified DateTime? image String? - createdAt DateTime @default(now()) @map(name: "created_at") - updatedAt DateTime @default(now()) @map(name: "updated_at") - - @@map(name: "users") + accounts Account[] + sessions Session[] } -model VerificationRequest { - id Int @default(autoincrement()) @id +model VerificationToken { identifier String token String @unique expires DateTime - createdAt DateTime @default(now()) @map(name: "created_at") - updatedAt DateTime @default(now()) @map(name: "updated_at") - @@map(name: "verification_requests") -} \ No newline at end of file + @@unique([identifier, token]) +} diff --git a/app/tsconfig.json b/app/tsconfig.json new file mode 100644 index 0000000000..694c90fc77 --- /dev/null +++ b/app/tsconfig.json @@ -0,0 +1,30 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "skipLibCheck": true, + "strict": false, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "baseUrl": "." + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx" + ], + "exclude": [ + "node_modules" + ] +} diff --git a/config/build.js b/config/build.js index e418afc594..b75b3ab253 100644 --- a/config/build.js +++ b/config/build.js @@ -6,7 +6,6 @@ const MODULE_ENTRIES = { REACT: "react", ADAPTERS: "adapters", JWT: "jwt", - ERRORS: "errors", } // Building submodule entries @@ -18,8 +17,6 @@ const BUILD_TARGETS = { "module.exports = require('./dist/client/react').default\n", [`${MODULE_ENTRIES.JWT}.js`]: "module.exports = require('./dist/lib/jwt').default\n", - [`${MODULE_ENTRIES.ERRORS}.js`]: - "module.exports = require('./dist/lib/errors').default\n", } Object.entries(BUILD_TARGETS).forEach(([target, content]) => { @@ -38,7 +35,6 @@ const TYPES_TARGETS = [ `${MODULE_ENTRIES.ADAPTERS}.d.ts`, "providers", `${MODULE_ENTRIES.JWT}.d.ts`, - `${MODULE_ENTRIES.ERRORS}.d.ts`, "internals", ] diff --git a/package.json b/package.json index 7f5b926ee3..7f55785ba4 100644 --- a/package.json +++ b/package.json @@ -23,8 +23,7 @@ ".": "./dist/server/index.js", "./jwt": "./dist/lib/jwt.js", "./react": "./dist/client/react.js", - "./providers/*": "./dist/providers/*.js", - "./errors": "./dist/lib/errors.js" + "./providers/*": "./dist/providers/*.js" }, "scripts": { "build": "npm run build:js && npm run build:css", @@ -49,12 +48,9 @@ "index.js", "index.d.ts", "providers", - "adapters.js", "adapters.d.ts", "react.js", "react.d.ts", - "errors.js", - "errors.d.ts", "jwt.js", "jwt.d.ts", "internals" @@ -148,7 +144,9 @@ "fetch": "readonly" }, "rules": { - "camelcase": "off" + "camelcase": "off", + "@typescript-eslint/naming-convention": "off", + "@typescript-eslint/strict-boolean-expressions": "off" }, "overrides": [ { diff --git a/src/adapters/error-handler.js b/src/adapters/error-handler.js deleted file mode 100644 index 2dee788849..0000000000 --- a/src/adapters/error-handler.js +++ /dev/null @@ -1,28 +0,0 @@ -import { capitalize, UnknownError, upperSnake } from "../lib/errors" - -/** - * Handles adapter induced errors. - * @param {import("types/adapters").AdapterInstance} adapter - * @param {import("types").LoggerInstance} logger - * @return {import("types/adapters").AdapterInstance} - */ -export default function adapterErrorHandler(adapter, logger) { - return Object.keys(adapter).reduce((acc, method) => { - const name = capitalize(method) - const code = `${adapter.displayName ?? "ADAPTER"}_${upperSnake(name)}` - - const adapterMethod = adapter[method] - acc[method] = async (...args) => { - try { - logger.debug(code, ...args) - return await adapterMethod(...args) - } catch (error) { - logger.error(`${code}_ERROR`, error) - const e = new UnknownError(error) - e.name = `${name}Error` - throw e - } - } - return acc - }, {}) -} diff --git a/src/lib/errors.js b/src/lib/errors.js index fd267b9230..cf3823277a 100644 --- a/src/lib/errors.js +++ b/src/lib/errors.js @@ -40,3 +40,48 @@ export function upperSnake(s) { export function capitalize(s) { return `${s[0].toUpperCase()}${s.slice(1)}` } + +/** + * Wraps an object of methods and adds error handling. + * @param {import("types").EventCallbacks} methods + * @param {import("types").LoggerInstance} logger + * @return {import("types").EventCallbacks} + */ +export function eventsErrorHandler(methods, logger) { + return Object.entries(methods).reduce((acc, [name, method]) => { + acc[name] = async (...args) => { + try { + return await method(...args) + } catch (e) { + logger.error(`${upperSnake(name)}_EVENT_ERROR`, e) + } + } + return acc + }, {}) +} + +/** + * Handles adapter induced errors. + * @param {import("types/adapters").Adapter} [adapter] + * @param {import("types").LoggerInstance} logger + * @return {import("types/adapters").Adapter} + */ +export function adapterErrorHandler(adapter, logger) { + if (!adapter) return + + return Object.keys(adapter).reduce((acc, method) => { + acc[method] = async (...args) => { + try { + logger.debug(`adapter_${method}`, ...args) + const adapterMethod = adapter[method] + return await adapterMethod(...args) + } catch (error) { + logger.error(`adapter_error_${method}`, error) + const e = new UnknownError(error) + e.name = `${capitalize(method)}Error` + throw e + } + } + return acc + }, {}) +} diff --git a/src/lib/jwt.js b/src/lib/jwt.js index b3ded27dc6..6a8545cd39 100644 --- a/src/lib/jwt.js +++ b/src/lib/jwt.js @@ -200,9 +200,3 @@ function getDerivedEncryptionKey(secret) { }) return key } - -export default { - encode, - decode, - getToken, -} diff --git a/src/lib/logger.js b/src/lib/logger.js index 630cb27ba5..bec48e534d 100644 --- a/src/lib/logger.js +++ b/src/lib/logger.js @@ -17,7 +17,7 @@ const _logger = { error(code, metadata) { metadata = formatError(metadata) console.error( - `[next-auth][error][${code.toLowerCase()}]`, + `[next-auth][error][${code}]`, `\nhttps://next-auth.js.org/errors#${code.toLowerCase()}`, metadata.message, metadata @@ -25,13 +25,13 @@ const _logger = { }, warn(code) { console.warn( - `[next-auth][warn][${code.toLowerCase()}]`, + `[next-auth][warn][${code}]`, `\nhttps://next-auth.js.org/warnings#${code.toLowerCase()}` ) }, debug(code, metadata) { if (!process?.env?._NEXTAUTH_DEBUG) return - console.log(`[next-auth][debug][${code.toLowerCase()}]`, metadata) + console.log(`[next-auth][debug][${code}]`, metadata) }, } diff --git a/src/providers/email.js b/src/providers/email.js index 75926f3cc5..d8200e325e 100644 --- a/src/providers/email.js +++ b/src/providers/email.js @@ -1,6 +1,7 @@ -import logger from "../lib/logger" +// @ts-check import nodemailer from "nodemailer" +/** @type {import("providers").EmailProvider} */ export default function Email(options) { return { id: "email", @@ -17,42 +18,34 @@ export default function Email(options) { }, from: "NextAuth ", maxAge: 24 * 60 * 60, - sendVerificationRequest, + async sendVerificationRequest({ + identifier: email, + url, + provider: { server, from }, + }) { + const { host } = new URL(url) + console.log(server) + const transport = nodemailer.createTransport(server) + await transport.sendMail({ + to: email, + from, + subject: `Sign in to ${host}`, + text: text({ url, host }), + html: html({ url, host, email }), + }) + }, options, } } -async function sendVerificationRequest({ - identifier: email, - url, - baseUrl, - provider, -}) { - const { server, from } = provider - // Strip protocol from URL and use domain as site name - const site = baseUrl.replace(/^https?:\/\//, "") - try { - await nodemailer.createTransport(server).sendMail({ - to: email, - from, - subject: `Sign in to ${site}`, - text: text({ url, site, email }), - html: html({ url, site, email }), - }) - } catch (error) { - logger.error("SEND_VERIFICATION_EMAIL_ERROR", email, error) - throw new Error("SEND_VERIFICATION_EMAIL_ERROR") - } -} - // Email HTML body -const html = ({ url, site, email }) => { +function html({ url, host, email }) { // Insert invisible space into domains and email address to prevent both the // email address and the domain from being turned into a hyperlink by email // clients like Outlook and Apple mail, as this is confusing because it seems // like they are supposed to click on their email address to sign in. const escapedEmail = `${email.replace(/\./g, "​.")}` - const escapedSite = `${site.replace(/\./g, "​.")}` + const escapedHost = `${host.replace(/\./g, "​.")}` // Some simple styling options const backgroundColor = "#f9f9f9" @@ -67,7 +60,7 @@ const html = ({ url, site, email }) => {
- ${escapedSite} + ${escapedHost}
@@ -97,4 +90,6 @@ const html = ({ url, site, email }) => { } // Email Text body (fallback for email clients that don't render HTML, e.g. feature phones) -const text = ({ url, site }) => `Sign in to ${site}\n${url}\n\n` +function text({ url, host }) { + return `Sign in to ${host}\n${url}\n\n` +} diff --git a/src/server/index.js b/src/server/index.js index 8e5b774adc..0ea7c71162 100644 --- a/src/server/index.js +++ b/src/server/index.js @@ -1,8 +1,7 @@ -import jwt from "../lib/jwt" +import * as jwt from "../lib/jwt" import parseUrl from "../lib/parse-url" import logger, { setLogger } from "../lib/logger" import * as cookie from "./lib/cookie" -import { withErrorHandling, defaultEvents } from "./lib/default-events" import * as defaultCallbacks from "./lib/default-callbacks" import parseProviders from "./lib/providers" import * as routes from "./routes" @@ -11,6 +10,7 @@ import createSecret from "./lib/create-secret" import callbackUrlHandler from "./lib/callback-url-handler" import extendRes from "./lib/extend-res" import csrfTokenHandler from "./lib/csrf-token-handler" +import { eventsErrorHandler, adapterErrorHandler } from "../lib/errors" // To work properly in production with OAuth providers the NEXTAUTH_URL // environment variable must be set. @@ -83,11 +83,12 @@ async function NextAuthHandler(req, res, userOptions) { provider.checks = ["state"] } - const maxAge = 30 * 24 * 60 * 60 // Sessions expire after 30 days of being idle + const maxAge = 30 * 24 * 60 * 60 // Sessions expire after 30 days of being idle by default // User provided options are overriden by other options, // except for the options with special handling above - req.options = { + /** @type {import("types/internals").InternalOptions} */ + const options = { debug: false, pages: {}, theme: "auto", @@ -106,7 +107,7 @@ async function NextAuthHandler(req, res, userOptions) { session: { jwt: !userOptions.adapter, // If no adapter specified, force use of JSON Web Tokens (stateless) maxAge, - updateAge: 24 * 60 * 60, // Sessions updated only if session is greater than this value (0 = always, 24*60*60 = every 24 hours) + updateAge: 24 * 60 * 60, ...userOptions.session, }, // JWT options @@ -118,10 +119,8 @@ async function NextAuthHandler(req, res, userOptions) { ...userOptions.jwt, }, // Event messages - events: withErrorHandling( - { ...defaultEvents, ...userOptions.events }, - logger - ), + events: eventsErrorHandler(userOptions.events ?? {}, logger), + adapter: adapterErrorHandler(userOptions.adapter, logger), // Callback functions callbacks: { ...defaultCallbacks, @@ -130,6 +129,8 @@ async function NextAuthHandler(req, res, userOptions) { logger, } + req.options = options + csrfTokenHandler(req, res) await callbackUrlHandler(req, res) diff --git a/src/server/lib/callback-handler.js b/src/server/lib/callback-handler.js index 646889b853..79b731e469 100644 --- a/src/server/lib/callback-handler.js +++ b/src/server/lib/callback-handler.js @@ -1,5 +1,7 @@ +// @ts-check import { AccountNotLinkedError } from "../../lib/errors" -import adapterErrorHandler from "../../adapters/error-handler" +import { fromDate } from "./utils" +import { randomBytes, randomUUID } from "crypto" /** * This function handles the complex flow of signing users in, and either creating, @@ -12,22 +14,21 @@ import adapterErrorHandler from "../../adapters/error-handler" * All verification (e.g. OAuth flows or email address verificaiton flows) are * done prior to this handler being called to avoid additonal complexity in this * handler. - * @param {import("types").Session} sessionToken - * @param {import("types").Profile} profile + * @param {import("types/internals/cookies").SessionToken | null} sessionToken + * @param {import("types").User} profile * @param {import("types").Account} account * @param {import("types/internals").InternalOptions} options */ export default async function callbackHandler( sessionToken, profile, - providerAccount, + account, options ) { // Input validation - if (!profile) throw new Error("Missing profile") - if (!providerAccount?.id || !providerAccount.type) + if (!account?.providerAccountId || !account.type) throw new Error("Missing or invalid provider account") - if (!["email", "oauth"].includes(providerAccount.type)) + if (!["email", "oauth"].includes(account.type)) throw new Error("Provider not supported") const { @@ -40,28 +41,25 @@ export default async function callbackHandler( // If no adapter is configured then we don't have a database and cannot // persist data; in this mode we just return a dummy session object. if (!adapter) { - return { - user: profile, - account: providerAccount, - session: {}, - } + return { user: profile, account, session: {} } } const { createUser, updateUser, getUser, - getUserByProviderAccountId, + getUserByAccount, getUserByEmail, linkAccount, createSession, - getSession, + getSessionAndUser, deleteSession, - } = adapterErrorHandler(await adapter.getAdapter(options), options.logger) + } = adapter + /** @type {import("types/adapters").AdapterSession | import("types/jwt").JWT | null} */ let session = null + /** @type {import("types/adapters").AdapterUser | null} */ let user = null - let isSignedIn = null let isNewUser = false if (sessionToken) { @@ -70,20 +68,20 @@ export default async function callbackHandler( session = await jwt.decode({ ...jwt, token: sessionToken }) if (session?.sub) { user = await getUser(session.sub) - isSignedIn = !!user } } catch { // If session can't be verified, treat as no session } - } - session = await getSession(sessionToken) - if (session?.userId) { - user = await getUser(session.userId) - isSignedIn = !!user + } else { + const userAndSession = await getSessionAndUser(sessionToken) + if (userAndSession) { + session = userAndSession.session + user = userAndSession.user + } } } - if (providerAccount.type === "email") { + if (account.type === "email") { // If signing in with an email, check if an account with the same email address exists already const userByEmail = profile.email ? await getUserByEmail(profile.email) @@ -91,53 +89,47 @@ export default async function callbackHandler( if (userByEmail) { // 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 (isSignedIn) { - if (user.id !== userByEmail.id && !useJwtSession) { - // 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) - } + if (user?.id !== userByEmail.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) } // Update emailVerified property on the user object - const currentDate = new Date() - user = await updateUser({ ...userByEmail, emailVerified: currentDate }) - await events.updateUser({ user }) + user = await updateUser({ id: userByEmail.id, emailVerified: new Date() }) + await events.updateUser?.({ user }) } else { + const newUser = { ...profile, emailVerified: new Date() } + // @ts-ignore Force the adapter to create its own user id + delete newUser.id // Create user account if there isn't one for the email address already - const currentDate = new Date() - user = await createUser({ ...profile, emailVerified: currentDate }) - await events.createUser({ user }) + user = await createUser(newUser) + await events.createUser?.({ user }) isNewUser = true } // Create new session - session = useJwtSession ? {} : await createSession(user) + session = useJwtSession + ? {} + : await createSession({ + sessionToken: generateSessionToken(), + userId: user.id, + expires: fromDate(options.session.maxAge), + }) - return { - session, - user, - isNewUser, - } - } else if (providerAccount.type === "oauth") { - // If signing in with oauth account, check to see if the account exists already - const userByProviderAccountId = await getUserByProviderAccountId( - providerAccount.provider, - providerAccount.id - ) - if (userByProviderAccountId) { - if (isSignedIn) { + return { session, user, isNewUser } + } else if (account.type === "oauth") { + // If signing in with OAuth account, check to see if the account exists already + const userByAccount = await getUserByAccount({ + providerAccountId: account.providerAccountId, + provider: account.provider, + }) + if (userByAccount) { + if (user) { // If the user is already signed in with this account, we don't need to do anything - // Note: These are cast as strings here to ensure they match as in - // some flows (e.g. JWT with a database) one of the values might be a - // string and the other might be an ObjectID and would otherwise fail. - if (`${userByProviderAccountId.id}` === `${user.id}`) { - return { - session, - user, - isNewUser, - } + if (userByAccount.id === user.id) { + return { session, user, isNewUser } } // If the user is currently signed in, but the new account they are signing in // with is already associated with another account, then we cannot link them @@ -148,33 +140,22 @@ export default async function callbackHandler( // associated with a valid user then create session to sign the user in. session = useJwtSession ? {} - : await createSession(userByProviderAccountId) - return { - session, - user: userByProviderAccountId, - isNewUser, - } + : await createSession({ + sessionToken: generateSessionToken(), + userId: userByAccount.id, + expires: fromDate(options.session.maxAge), + }) + + return { session, user: userByAccount, isNewUser } } else { - if (isSignedIn) { + if (user) { // If the user is already signed in and the OAuth account isn't already associated // with another user account then we can go ahead and link the accounts safely. - await linkAccount( - user.id, - providerAccount.provider, - providerAccount.type, - providerAccount.id, - providerAccount.refreshToken, - providerAccount.accessToken, - providerAccount.accessTokenExpires - ) - await events.linkAccount({ user, providerAccount }) + await linkAccount({ ...account, userId: user.id }) + await events.linkAccount?.({ user, account }) // As they are already signed in, we don't need to do anything after linking them - return { - session, - user, - isNewUser, - } + return { session, user, isNewUser } } // If the user is not signed in and it looks like a new OAuth account then we @@ -213,27 +194,29 @@ export default async function callbackHandler( // If no account matching the same [provider].id or .email exists, we can // create a new account for the user, link it to the OAuth acccount and // create a new session for them so they are signed in with it. - user = await createUser(profile) - await events.createUser({ user }) - - await linkAccount( - user.id, - providerAccount.provider, - providerAccount.type, - providerAccount.id, - providerAccount.refreshToken, - providerAccount.accessToken, - providerAccount.accessTokenExpires - ) - await events.linkAccount({ user, providerAccount }) - - session = useJwtSession ? {} : await createSession(user) - isNewUser = true - return { - session, - user, - isNewUser, - } + const newUser = { ...profile, emailVerified: null } + // @ts-ignore Force the adapter to create its own user id + delete newUser.id + user = await createUser(newUser) + await events.createUser?.({ user }) + + await linkAccount({ ...account, userId: user.id }) + await events.linkAccount?.({ user, account }) + + session = useJwtSession + ? {} + : await createSession({ + sessionToken: generateSessionToken(), + userId: user.id, + expires: fromDate(options.session.maxAge), + }) + + return { session, user, isNewUser: true } } } } + +function generateSessionToken() { + // Use `randomUUID` if available. (Node 15.6++) + return randomUUID?.() ?? randomBytes(32).toString("hex") +} diff --git a/src/server/lib/default-events.js b/src/server/lib/default-events.js deleted file mode 100644 index a5c7f7c339..0000000000 --- a/src/server/lib/default-events.js +++ /dev/null @@ -1,30 +0,0 @@ -import { upperSnake } from "../../lib/errors" - -/** @type {import("types").EventCallbacks} */ -export const defaultEvents = { - signIn() {}, - signOut() {}, - createUser() {}, - updateUser() {}, - linkAccount() {}, - session() {}, -} - -/** - * Wraps an object of methods and adds error handling. - * @param {import("types").EventCallbacks} methods - * @param {import("types").LoggerInstance} logger - * @return {import("types").EventCallbacks} - */ -export function withErrorHandling(methods, logger) { - return Object.entries(methods).reduce((acc, [name, method]) => { - acc[name] = async (...args) => { - try { - return await method(...args) - } catch (e) { - logger.error(`${upperSnake(name)}_EVENT_ERROR`, e) - } - } - return acc - }, {}) -} diff --git a/src/server/lib/email/signin.js b/src/server/lib/email/signin.js index 67236a5091..0eef771c45 100644 --- a/src/server/lib/email/signin.js +++ b/src/server/lib/email/signin.js @@ -1,48 +1,57 @@ +// @ts-check import { randomBytes } from "crypto" -import adapterErrorHandler from "../../../adapters/error-handler" +import { hashToken } from "../utils" /** - * - * @param {string} email - * @param {import("types/providers").EmailConfig} provider - * @param {import("types/internals").InternalOptions} options - * @returns + * @typedef {import("types/providers").EmailConfig} EmailConfig */ -export default async function email(email, provider, options) { - try { - const { baseUrl, basePath, adapter, logger } = options - const { createVerificationRequest } = adapterErrorHandler( - await adapter.getAdapter(options), - logger - ) +/** + * 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) + * @param {string} identifier + * @param {import("types/internals").InternalOptions} options + */ +export default async function email(identifier, options) { + const { baseUrl, basePath, adapter, provider, logger } = options - // Prefer provider specific secret, but use default secret if none specified - const secret = provider.secret || options.secret + // Generate token + const token = + (await provider.generateVerificationToken?.()) ?? + randomBytes(32).toString("hex") - // Generate token - const token = - (await provider.generateVerificationToken?.()) ?? - randomBytes(32).toString("hex") + const ONE_DAY_IN_SECONDS = 86400 + const expires = new Date( + Date.now() + (provider.maxAge ?? ONE_DAY_IN_SECONDS) * 1000 + ) - // Send email with link containing token (the unhashed version) - const url = `${baseUrl}${basePath}/callback/${encodeURIComponent( - provider.id - )}?email=${encodeURIComponent(email)}&token=${encodeURIComponent(token)}` + // Save in database + // @ts-expect-error + await adapter.createVerificationToken({ + identifier, + token: hashToken(token, options), + expires, + }) - // @TODO Create invite (send secret so can be hashed) - await createVerificationRequest( - email, - url, + // Generate a link with email and the unhashed token + const params = new URLSearchParams({ token, email: identifier }) + const url = `${baseUrl}${basePath}/callback/${provider.id}?${params}` + + try { + // Send to user + await provider.sendVerificationRequest({ + identifier, token, - secret, + expires, + url, provider, - options - ) - - // Return promise - return Promise.resolve() + }) } catch (error) { - return Promise.reject(error) + logger.error("SEND_VERIFICATION_EMAIL_ERROR", { + identifier, + url, + error, + }) + throw new Error("SEND_VERIFICATION_EMAIL_ERROR") } } diff --git a/src/server/lib/oauth/callback.js b/src/server/lib/oauth/callback.js index 57ebce1f26..c3389143fe 100644 --- a/src/server/lib/oauth/callback.js +++ b/src/server/lib/oauth/callback.js @@ -5,7 +5,7 @@ import { usePKCECodeVerifier } from "./pkce-handler" import { OAuthCallbackError } from "../../../lib/errors" import { TokenSet } from "openid-client" -/** @type {import("types/internals").NextAuthApiHandler} */ +/** @type {import("types/internals").NextAuthApiHandler} */ export default async function oAuthCallback(req, res) { const { logger } = req.options @@ -77,6 +77,11 @@ export default async function oAuthCallback(req, res) { tokens = await client.oauthCallback(provider.callbackUrl, params, checks) } + // REVIEW: How can scope be returned as an array? + if (Array.isArray(tokens.scope)) { + tokens.scope = tokens.scope.join(" ") + } + /** @type {import("types").Profile} */ let profile if (provider.userinfo?.request) { @@ -106,7 +111,7 @@ export default async function oAuthCallback(req, res) { /** * Returns profile, raw profile and auth provider details - * @param {import("types/internals/oauth").GetProfileParams} params + * @type {import("types/internals/oauth").GetProfile} */ async function getProfile({ profile: OAuthProfile, tokens, provider, logger }) { try { @@ -119,7 +124,7 @@ async function getProfile({ profile: OAuthProfile, tokens, provider, logger }) { account: { provider: provider.id, type: provider.type, - id: profile.id, + providerAccountId: profile.id.toString(), ...tokens, }, OAuthProfile, diff --git a/src/server/lib/oauth/client-legacy.js b/src/server/lib/oauth/client-legacy.js index ceaa6a2a0d..0364721f56 100644 --- a/src/server/lib/oauth/client-legacy.js +++ b/src/server/lib/oauth/client-legacy.js @@ -40,15 +40,11 @@ export function oAuth1Client(options) { return new Promise((resolve, reject) => { originalGetOAuth1AccessToken( ...args, - (error, oauth_token, oauth_token_secret, params) => { + (error, oauth_token, oauth_token_secret) => { if (error) { return reject(error) } - resolve({ - oauth_token, - oauth_token_secret, - params, - }) + resolve({ oauth_token, oauth_token_secret }) } ) }) diff --git a/src/server/lib/oauth/pkce-handler.js b/src/server/lib/oauth/pkce-handler.js index 133688796c..1f42062414 100644 --- a/src/server/lib/oauth/pkce-handler.js +++ b/src/server/lib/oauth/pkce-handler.js @@ -1,5 +1,5 @@ import * as cookie from "../cookie" -import jwt from "../../../lib/jwt" +import * as jwt from "../../../lib/jwt" import { generators } from "openid-client" const PKCE_LENGTH = 64 diff --git a/src/server/lib/utils.js b/src/server/lib/utils.js new file mode 100644 index 0000000000..96ecc073c5 --- /dev/null +++ b/src/server/lib/utils.js @@ -0,0 +1,24 @@ +import { createHash } from "crypto" + +/** + * Takes a number in seconds and returns the date in the future. + * Optionally takes a second date parameter. In that case + * the date in the future will be calculated from that date instead of now. + */ +export function fromDate(time, date = Date.now()) { + return new Date(date + time * 1000) +} + +/** + * @param {string} token + * @param {import("types/internals").InternalOptions} options + */ +export function hashToken(token, options) { + const { provider, secret } = options + return ( + createHash("sha256") + // Prefer provider specific secret, but use default secret if none specified + .update(`${token}${provider.secret ?? secret}`) + .digest("hex") + ) +} diff --git a/src/server/routes/callback.js b/src/server/routes/callback.js index 53611ed671..8eb4dfd100 100644 --- a/src/server/routes/callback.js +++ b/src/server/routes/callback.js @@ -1,7 +1,7 @@ import oAuthCallback from "../lib/oauth/callback" import callbackHandler from "../lib/callback-handler" import * as cookie from "../lib/cookie" -import adapterErrorHandler from "../../adapters/error-handler" +import { hashToken } from "../lib/utils" /** * Handle callbacks from login services @@ -13,7 +13,6 @@ export default async function callback(req, res) { adapter, baseUrl, basePath, - secret, cookies, callbackUrl, pages, @@ -24,7 +23,6 @@ export default async function callback(req, res) { logger, } = req.options - // Get session ID (if set) const sessionToken = req.cookies?.[cookies.sessionToken.name] ?? null if (provider.type === "oauth") { @@ -56,31 +54,27 @@ export default async function callback(req, res) { // (that just means it's a new user signing in for the first time). let userOrProfile = profile if (adapter) { - const { getUserByProviderAccountId } = adapterErrorHandler( - await adapter.getAdapter(req.options), - logger - ) - const userFromProviderAccountId = await getUserByProviderAccountId( - account.provider, - account.id - ) - if (userFromProviderAccountId) { - userOrProfile = userFromProviderAccountId - } + const { getUserByAccount } = adapter + const userByAccount = await getUserByAccount({ + providerAccountId: account.providerAccountId, + provider: provider.id, + }) + + if (userByAccount) userOrProfile = userByAccount } try { - const signInCallbackResponse = await callbacks.signIn({ + const isAllowed = await callbacks.signIn({ user: userOrProfile, account, profile: OAuthProfile, }) - if (!signInCallbackResponse) { + if (!isAllowed) { return res.redirect( `${baseUrl}${basePath}/error?error=AccessDenied` ) - } else if (typeof signInCallbackResponse === "string") { - return res.redirect(signInCallbackResponse) + } else if (typeof isAllowed === "string") { + return res.redirect(isAllowed) } } catch (error) { return res.redirect( @@ -127,12 +121,12 @@ export default async function callback(req, res) { } else { // Save Session Token in cookie cookie.set(res, cookies.sessionToken.name, session.sessionToken, { - expires: session.expires || null, + expires: session.expires, ...cookies.sessionToken.options, }) } - await events.signIn({ user, account, profile, isNewUser }) + await events.signIn?.({ user, account, profile, isNewUser }) // Handle first logins on new accounts // e.g. option to send users to a new account landing page on initial login @@ -172,44 +166,40 @@ export default async function callback(req, res) { } else if (provider.type === "email") { try { if (!adapter) { - logger.error("EMAIL_REQUIRES_ADAPTER_ERROR") + logger.error( + "EMAIL_REQUIRES_ADAPTER_ERROR", + new Error("E-mail login requires an adapter but it was undefined") + ) return res.redirect(`${baseUrl}${basePath}/error?error=Configuration`) } - const { - getVerificationRequest, - deleteVerificationRequest, - getUserByEmail, - } = adapterErrorHandler(await adapter.getAdapter(req.options), logger) - const verificationToken = req.query.token - const email = req.query.email - - // Verify email and verification token exist in database - const invite = await getVerificationRequest( - email, - verificationToken, - secret, - provider - ) - if (!invite) { + const { useVerificationToken, getUserByEmail } = adapter + + const token = req.query.token + const identifier = req.query.email + + const invite = await useVerificationToken({ + identifier, + token: hashToken(token, req.options), + }) + + const invalidInvite = !invite || invite.expires.valueOf() < Date.now() + if (invalidInvite) { return res.redirect(`${baseUrl}${basePath}/error?error=Verification`) } - // If verification token is valid, delete verification request token from - // the database so it cannot be used again - await deleteVerificationRequest( - email, - verificationToken, - secret, - provider - ) + // If it is an existing user, use that, otherwise use a placeholder + const profile = (identifier + ? await getUserByEmail(identifier) + : null) ?? { + email: identifier, + } - // If is an existing user return a user object (otherwise use placeholder) - const profile = (await getUserByEmail(email)) || { email } + /** @type {import("types").Account} */ const account = { - id: provider.id, + providerAccountId: profile.email, type: "email", - providerAccountId: email, + provider: provider.id, } // Check if user is allowed to sign in @@ -217,7 +207,7 @@ export default async function callback(req, res) { const signInCallbackResponse = await callbacks.signIn({ user: profile, account, - email: { email }, + email: { email: identifier }, }) if (!signInCallbackResponse) { return res.redirect(`${baseUrl}${basePath}/error?error=AccessDenied`) @@ -251,7 +241,6 @@ export default async function callback(req, res) { token: defaultToken, user, account, - profile, isNewUser, }) @@ -269,12 +258,12 @@ export default async function callback(req, res) { } else { // Save Session Token in cookie cookie.set(res, cookies.sessionToken.name, session.sessionToken, { - expires: session.expires || null, + expires: session.expires, ...cookies.sessionToken.options, }) } - await events.signIn({ user, account, profile, isNewUser }) + await events.signIn?.({ user, account, isNewUser }) // Handle first logins on new accounts // e.g. option to send users to a new account landing page on initial login @@ -325,22 +314,20 @@ export default async function callback(req, res) { const credentials = req.body - let userObjectReturnedFromAuthorizeHandler + let user try { - userObjectReturnedFromAuthorizeHandler = await provider.authorize( - credentials, - { ...req, options: {}, cookies: {} } - ) - if (!userObjectReturnedFromAuthorizeHandler) { - return res - .status(401) - .redirect( - `${baseUrl}${basePath}/error?error=CredentialsSignin&provider=${encodeURIComponent( - provider.id - )}` - ) - } else if (typeof userObjectReturnedFromAuthorizeHandler === "string") { - return res.redirect(userObjectReturnedFromAuthorizeHandler) + user = await provider.authorize(credentials, { + ...req, + options: {}, + cookies: {}, + }) + if (!user) { + return res.status(401).redirect( + `${baseUrl}${basePath}/error?${new URLSearchParams({ + error: "CredentialsSignin", + provider: provider.id, + })}` + ) } } catch (error) { return res.redirect( @@ -348,21 +335,25 @@ export default async function callback(req, res) { ) } - const user = userObjectReturnedFromAuthorizeHandler - const account = { id: provider.id, type: "credentials" } + /** @type {import("types").Account} */ + const account = { + providerAccountId: user.id, + type: "credentials", + provider: provider.id, + } try { - const signInCallbackResponse = await callbacks.signIn({ + const isAllowed = await callbacks.signIn({ user, account, credentials, }) - if (!signInCallbackResponse) { + if (!isAllowed) { return res .status(403) .redirect(`${baseUrl}${basePath}/error?error=AccessDenied`) - } else if (typeof signInCallbackResponse === "string") { - return res.redirect(signInCallbackResponse) + } else if (typeof isAllowed === "string") { + return res.redirect(isAllowed) } } catch (error) { return res.redirect( @@ -376,11 +367,11 @@ export default async function callback(req, res) { picture: user.image, sub: user.id?.toString(), } + const token = await callbacks.jwt({ token: defaultToken, user, account, - profile: userObjectReturnedFromAuthorizeHandler, isNewUser: false, }) @@ -396,7 +387,7 @@ export default async function callback(req, res) { ...cookies.sessionToken.options, }) - await events.signIn({ user, account }) + await events.signIn?.({ user, account }) return res.redirect(callbackUrl || baseUrl) } diff --git a/src/server/routes/session.js b/src/server/routes/session.js index bad322e08d..7add2fee62 100644 --- a/src/server/routes/session.js +++ b/src/server/routes/session.js @@ -1,5 +1,5 @@ import * as cookie from "../lib/cookie" -import adapterErrorHandler from "../../adapters/error-handler" +import { fromDate } from "../lib/utils" /** * Return a session object (without any private fields) @@ -24,21 +24,17 @@ export default async function session(req, res) { const decodedToken = await jwt.decode({ ...jwt, token: sessionToken }) // Generate new session expiry date - const sessionExpiresDate = new Date() - sessionExpiresDate.setTime( - sessionExpiresDate.getTime() + sessionMaxAge * 1000 - ) - const sessionExpires = sessionExpiresDate.toISOString() + const newExpires = fromDate(sessionMaxAge) // By default, only exposes a limited subset of information to the client - // as needed for presentation purposes (e.g. "you are logged in as…"). + // as needed for presentation purposes (e.g. "you are logged in as..."). const defaultSession = { user: { - name: decodedToken.name || null, - email: decodedToken.email || null, - image: decodedToken.picture || null, + name: decodedToken?.name, + email: decodedToken?.email, + image: decodedToken?.picture, }, - expires: sessionExpires, + expires: newExpires.toISOString(), } // Pass Session and JSON Web Token through to the session callback @@ -56,11 +52,11 @@ export default async function session(req, res) { // Set cookie, to also update expiry date on cookie cookie.set(res, cookies.sessionToken.name, newToken, { - expires: sessionExpires, + expires: newExpires, ...cookies.sessionToken.options, }) - await events.session({ session, token }) + await events.session?.({ session, token }) } catch (error) { // If JWT not verifiable, make sure the cookie for it is removed and return empty object logger.error("JWT_SESSION_ERROR", error) @@ -71,32 +67,49 @@ export default async function session(req, res) { } } else { try { - const { getUser, getSession, updateSession } = adapterErrorHandler( - await adapter.getAdapter(req.options), - logger - ) - const session = await getSession(sessionToken) - if (session) { - // Trigger update to session object to update session expiry - await updateSession(session) - - const user = await getUser(session.userId) - - // By default, only exposes a limited subset of information to the client - // as needed for presentation purposes (e.g. "you are logged in as…"). - const defaultSession = { - user: { - name: user.name, - email: user.email, - image: user.image, - }, - accessToken: session.accessToken, - expires: session.expires, + const { getSessionAndUser, deleteSession, updateSession } = adapter + let userAndSession = await getSessionAndUser(sessionToken) + + // If session has expired, clean up the database + if ( + userAndSession && + userAndSession.session.expires.valueOf() < Date.now() + ) { + await deleteSession(sessionToken) + userAndSession = null + } + + if (userAndSession) { + const { user, session } = userAndSession + + const sessionUpdateAge = req.options.session.updateAge + // Calculate last updated date to throttle write updates to database + // Formula: ({expiry date} - sessionMaxAge) + sessionUpdateAge + // e.g. ({expiry date} - 30 days) + 1 hour + const sessionIsDueToBeUpdatedDate = + session.expires.valueOf() - + sessionMaxAge * 1000 + + sessionUpdateAge * 1000 + + const newExpires = fromDate(sessionMaxAge) + // Trigger update of session expiry date and write to database, only + // if the session was last updated more than {sessionUpdateAge} ago + if (sessionIsDueToBeUpdatedDate <= Date.now()) { + await updateSession({ sessionToken, expires: newExpires }) } // Pass Session through to the session callback const sessionPayload = await callbacks.session({ - session: defaultSession, + // By default, only exposes a limited subset of information to the client + // as needed for presentation purposes (e.g. "you are logged in as..."). + session: { + user: { + name: user.name, + email: user.email, + image: user.image, + }, + expires: session.expires.toISOString(), + }, user, }) @@ -105,11 +118,11 @@ export default async function session(req, res) { // Set cookie again to update expiry cookie.set(res, cookies.sessionToken.name, sessionToken, { - expires: session.expires, + expires: newExpires, ...cookies.sessionToken.options, }) - await events.session({ session: sessionPayload }) + await events.session?.({ session: sessionPayload }) } else if (sessionToken) { // If sessionToken was found set but it's not valid for a session then // remove the sessionToken cookie from browser. diff --git a/src/server/routes/signin.js b/src/server/routes/signin.js index 328f6f48ee..82cd98c645 100644 --- a/src/server/routes/signin.js +++ b/src/server/routes/signin.js @@ -1,11 +1,9 @@ import getAuthorizationUrl from "../lib/oauth/authorization-url" import emailSignin from "../lib/email/signin" -import adapterErrorHandler from "../../adapters/error-handler" /** * Handle requests to /api/auth/signin - * @param {import("types/internals").NextAuthRequest} req - * @param {import("types/internals").NextAuthResponse} res + * @type {import("types/internals").NextAuthApiHandler} */ export default async function signin(req, res) { const { baseUrl, basePath, adapter, callbacks, logger } = req.options @@ -27,13 +25,12 @@ export default async function signin(req, res) { } } else if (provider.type === "email") { if (!adapter) { - logger.error("EMAIL_REQUIRES_ADAPTER_ERROR") + logger.error( + "EMAIL_REQUIRES_ADAPTER_ERROR", + new Error("E-mail login requires an adapter but it was undefined") + ) return res.redirect(`${baseUrl}${basePath}/error?error=Configuration`) } - const { getUserByEmail } = adapterErrorHandler( - await adapter.getAdapter(req.options), - logger - ) // Note: Technically the part of the email address local mailbox element // (everything before the @ symbol) should be treated as 'case sensitive' @@ -42,16 +39,23 @@ export default async function signin(req, res) { // complains about this we can make strict RFC 2821 compliance an option. const email = req.body.email?.toLowerCase() ?? null + const { getUserByEmail } = adapter // If is an existing user return a user object (otherwise use placeholder) - const user = (await getUserByEmail(email)) || { email } - const account = { id: provider.id, type: "email", providerAccountId: email } + const user = (email ? await getUserByEmail(email) : null) ?? { email } + + /** @type {import("types").Account} */ + const account = { + providerAccountId: user.email, + type: "email", + provider: provider.id, + } // Check if user is allowed to sign in try { const signInCallbackResponse = await callbacks.signIn({ user, account, - email: { email, verificationRequest: true }, + email: { verificationRequest: true }, }) if (!signInCallbackResponse) { return res.redirect(`${baseUrl}${basePath}/error?error=AccessDenied`) @@ -60,22 +64,23 @@ export default async function signin(req, res) { } } catch (error) { return res.redirect( - `${baseUrl}${basePath}/error?error=${encodeURIComponent(error)}` + `${baseUrl}${basePath}/error?${new URLSearchParams({ error })}}` ) } try { - await emailSignin(email, provider, req.options) + await emailSignin(email, req.options) } catch (error) { logger.error("SIGNIN_EMAIL_ERROR", error) return res.redirect(`${baseUrl}${basePath}/error?error=EmailSignin`) } - return res.redirect( - `${baseUrl}${basePath}/verify-request?provider=${encodeURIComponent( - provider.id - )}&type=${encodeURIComponent(provider.type)}` - ) + const params = new URLSearchParams({ + provider: provider.id, + type: provider.type, + }) + const url = `${baseUrl}${basePath}/verify-request?${params}` + return res.redirect(url) } return res.redirect(`${baseUrl}${basePath}/signin`) } diff --git a/src/server/routes/signout.js b/src/server/routes/signout.js index dd759936b3..2996335b95 100644 --- a/src/server/routes/signout.js +++ b/src/server/routes/signout.js @@ -1,5 +1,4 @@ import * as cookie from "../lib/cookie" -import adapterErrorHandler from "../../adapters/error-handler" /** * Handle requests to /api/auth/signout @@ -15,28 +14,15 @@ export default async function signout(req, res) { // Dispatch signout event try { const decodedJwt = await jwt.decode({ ...jwt, token: sessionToken }) - await events.signOut({ token: decodedJwt }) + await events.signOut?.({ token: decodedJwt }) } catch (error) { // Do nothing if decoding the JWT fails } } else { - // Get session from database - const { getSession, deleteSession } = adapterErrorHandler( - await adapter.getAdapter(req.options), - logger - ) - try { + const session = await adapter.deleteSession(sessionToken) // Dispatch signout event - const session = await getSession(sessionToken) - await events.signOut({ session }) - } catch (error) { - // Do nothing if looking up the session fails - } - - try { - // Remove session from database - await deleteSession(sessionToken) + await events.signOut?.({ session }) } catch (error) { // If error, log it but continue logger.error("SIGNOUT_ERROR", error) diff --git a/types/adapters.d.ts b/types/adapters.d.ts index 9c2544ecca..ca6496f6a3 100644 --- a/types/adapters.d.ts +++ b/types/adapters.d.ts @@ -1,6 +1,25 @@ -import { InternalOptions } from "./internals" -import { User, Profile, Session } from "." -import { EmailConfig } from "./providers" +import { Account, User } from "." +import { Awaitable } from "./internals/utils" + +export interface AdapterUser extends User { + id: string + emailVerified: Date | null +} + +export interface AdapterSession { + id: string + /** A randomly generated value that is used to get hold of the session. */ + sessionToken: string + /** Used to connect the session to a particular user */ + userId: string + expires: Date +} + +export interface VerificationToken { + identifier: string + expires: Date + token: string +} /** * Using a custom adapter you can connect to any database backend or even several different databases. @@ -9,97 +28,24 @@ import { EmailConfig } from "./providers" * or even become a maintainer of a certain adapter. * Custom adapters can still be created and used in a project without being added to the repository. * - * [Community adapters](https://github.com/nextauthjs/adapters) | - * [Create a custom adapter](https://next-auth.js.org/tutorials/creating-a-database-adapter) - */ -export interface AdapterInstance { - /** Used as a prefix for adapter related log messages. (Defaults to `ADAPTER_`) */ - displayName?: string - createUser(profile: P): Promise - getUser(id: string): Promise - getUserByEmail(email: string | null): Promise - getUserByProviderAccountId( - providerId: string, - providerAccountId: string - ): Promise - updateUser(user: U): Promise - /** @todo Implement */ - deleteUser?(userId: string): Promise - linkAccount( - userId: string, - providerId: string, - providerType: string, - providerAccountId: string, - refreshToken?: string, - accessToken?: string, - accessTokenExpires?: null - ): Promise - /** @todo Implement */ - unlinkAccount?( - userId: string, - providerId: string, - providerAccountId: string - ): Promise - createSession(user: U): Promise - getSession(sessionToken: string): Promise - updateSession(session: S, force?: boolean): Promise - deleteSession(sessionToken: string): Promise - createVerificationRequest?( - identifier: string, - url: string, - token: string, - secret: string, - provider: EmailConfig & { maxAge: number; from: string } - ): Promise - getVerificationRequest?( - identifier: string, - verificationToken: string, - secret: string, - provider: Required - ): Promise<{ - id: string - identifier: string - token: string - expires: Date - } | null> - deleteVerificationRequest?( - identifier: string, - verificationToken: string, - secret: string, - provider: Required - ): Promise -} - -/** - * From an implementation perspective, an adapter in NextAuth.js is a function - * which returns an async `getAdapter()` method, which in turn returns a list of functions - * used to handle operations such as creating user, linking a user - * and an OAuth account or handling reading and writing sessions. - * - * It uses this approach to allow database connection logic to live in the `getAdapter()` method. - * By calling the function just before an action needs to happen, - * it is possible to check database connection status and handle connecting / reconnecting - * to a database as required. - * * **Required methods** * * _(These methods are required for all sign in flows)_ * - `createUser` * - `getUser` * - `getUserByEmail` - * - `getUserByProviderAccountId` + * - `getUserByAccount` * - `linkAccount` * - `createSession` - * - `getSession` + * - `getSessionAndUser` * - `updateSession` * - `deleteSession` * - `updateUser` * * _(Required to support email / passwordless sign in)_ * - * - `createVerificationRequest` - * - `getVerificationRequest` - * - `deleteVerificationRequest` + * - `createVerificationToken` + * - `useVerificationToken` * * **Unimplemented methods** * @@ -110,15 +56,53 @@ export interface AdapterInstance { * [Community adapters](https://github.com/nextauthjs/adapters) | * [Create a custom adapter](https://next-auth.js.org/tutorials/creating-a-database-adapter) */ -export type Adapter< - C = unknown, - O = Record, - U = unknown, - P = unknown, - S = unknown -> = ( - client: C, - options?: O -) => { - getAdapter(appOptions: InternalOptions): Promise> +export interface Adapter { + createUser(user: Omit): Awaitable + getUser(id: string): Awaitable + getUserByEmail(email: 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): Awaitable + /** @todo Implement */ + deleteUser?(userId: string): Awaitable + linkAccount( + account: Account + ): Promise | Awaitable + /** @todo Implement */ + unlinkAccount?( + providerAccountId: Pick + ): Promise | Awaitable + /** Creates a session for the user and returns it. */ + createSession(session: { + sessionToken: string + userId: string + expires: Date + }): Awaitable + getSessionAndUser( + sessionToken: string + ): Awaitable<{ session: AdapterSession; user: AdapterUser } | null> + updateSession( + session: Partial & Pick + ): Awaitable + /** + * Deletes a session from the database. + * It is preferred that this method also returns the session + * that is being deleted for logging purposes. + */ + deleteSession( + sessionToken: string + ): Awaitable + createVerificationToken?( + verificationToken: VerificationToken + ): Awaitable + /** + * Return verification token from the database + * and delete it so it cannot be used again. + */ + useVerificationToken?(params: { + identifier: string + token: string + }): Awaitable } diff --git a/types/index.d.ts b/types/index.d.ts index 59a4ac7755..f150ddba72 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -1,10 +1,6 @@ -// Minimum TypeScript Version: 3.6 - -/// - import { Adapter } from "./adapters" import { JWTOptions, JWT } from "./jwt" -import { AppProviders, Credentials } from "./providers" +import { Provider, Credentials, ProviderType } from "./providers" import { Awaitable, NextApiRequest, @@ -28,7 +24,7 @@ export interface NextAuthOptions { * * [Documentation](https://next-auth.js.org/configuration/options#providers) | [Providers documentation](https://next-auth.js.org/configuration/providers) */ - providers: AppProviders + providers: Provider[] /** * A random string used to hash tokens, sign cookies and generate cryptographic keys. * If not specified is uses a hash of all configuration options, including Client ID / Secrets for entropy. @@ -48,7 +44,7 @@ export interface NextAuthOptions { * * [Documentation](https://next-auth.js.org/configuration/options#session) */ - session?: SessionOptions + session?: Partial /** * JSON Web Tokens can be used for session tokens if enabled with the `session: { jwt: true }` option. * JSON Web Tokens are enabled by default if you have not specified a database. @@ -60,7 +56,7 @@ export interface NextAuthOptions { * * [Documentation](https://next-auth.js.org/configuration/options#jwt) */ - jwt?: JWTOptions + jwt?: Partial /** * Specify URLs to be used if you want to create custom sign in, sign out and error pages. * Pages specified will override the corresponding built-in page. @@ -80,7 +76,7 @@ export interface NextAuthOptions { * * [Documentation](https://next-auth.js.org/configuration/options#pages) | [Pages documentation](https://next-auth.js.org/configuration/pages) */ - pages?: PagesOptions + pages?: Partial /** * Callbacks are asynchronous functions you can use to control what happens when an action is performed. * Callbacks are *extremely powerful*, especially in scenarios involving JSON Web Tokens @@ -112,7 +108,7 @@ export interface NextAuthOptions { * [Documentation](https://next-auth.js.org/configuration/options#adapter) | * [Community adapters](https://github.com/nextauthjs/adapters) */ - adapter?: ReturnType + adapter?: Adapter /** * Set debug to true to enable debug messages for authentication and database operations. * * **Default value**: `false` @@ -154,7 +150,7 @@ export interface NextAuthOptions { * [Documentation](https://next-auth.js.org/configuration/options#logger) | * [Debug documentation](https://next-auth.js.org/configuration/options#debug) */ - logger?: LoggerInstance + logger?: Partial /** * Changes the theme of pages. * Set to `"light"` if you want to force pages to always be light. @@ -243,12 +239,24 @@ export type TokenSet = TokenSetParameters * Usually contains information about the provider being used * and also extends `TokenSet`, which is different tokens returned by OAuth Providers. */ -export interface Account extends Partial { - id: string +export interface DefaultAccount extends Partial { + /** + * This value depends on the type of the provider being used to create the account. + * - oauth: The OAuth account's id, returned from the `profile()` callback. + * - email: The user's email address. + * - credentials: `id` returned from the `authorize()` callback + */ + providerAccountId: string + /** id of the user this account belongs to. */ + userId: string + /** id of the provider used for this account */ provider: string - type: string + /** Provider's type for this account */ + type: ProviderType } +export interface Account extends Record, DefaultAccount {} + export interface DefaultProfile { sub?: string name?: string @@ -280,7 +288,7 @@ export interface CallbacksOptions< */ profile: P & Record /** - * If Email provider is used, it contains the email, and optionally on the first call a + * If Email provider is used, on the first call, it contains a * `verificationRequest: true` property to indicate it is being triggered in the verification request flow. * When the callback is invoked after a user has clicked on a sign in link, * this property will not be present. You can check for the `verificationRequest` property @@ -288,7 +296,6 @@ export interface CallbacksOptions< * for email address in an allow list. */ email: { - email: string | null verificationRequest?: boolean } /** If Credentials provider is used, it contains the user credentials */ @@ -397,10 +404,7 @@ export interface EventCallbacks { signOut(message: { session: Session; token: JWT }): Awaitable createUser(message: { user: User }): Awaitable updateUser(message: { user: User }): Awaitable - linkAccount(message: { - user: User - providerAccount: Record - }): Awaitable + linkAccount(message: { user: User; account: Account }): Awaitable /** * The message object will contain one of these depending on * if you use JWT or database persisted sessions: @@ -414,22 +418,24 @@ export type EventType = keyof EventCallbacks /** [Documentation](https://next-auth.js.org/configuration/pages) */ export interface PagesOptions { - signIn?: string - signOut?: string + signIn: string + signOut: string /** Error code passed in query string as ?error= */ - error?: string - verifyRequest?: string + error: string + verifyRequest: string /** If set, new users will be directed here on first sign in */ - newUser?: string + newUser: string } +export type ISODateString = string + export interface DefaultSession extends Record { user?: { name?: string | null email?: string | null image?: string | null } - expires?: string + expires: ISODateString } /** @@ -445,12 +451,22 @@ export interface Session extends Record, DefaultSession {} /** [Documentation](https://next-auth.js.org/configuration/options#session) */ export interface SessionOptions { - jwt?: boolean - maxAge?: number - updateAge?: number + jwt: boolean + /** + * Relative time from now in seconds when to expire the session + * @default 2592000 // 30 days + */ + maxAge: number + /** + * How often the session should be updated in seconds. + * If set to `0`, session is updated every time. + * @default 86400 // 1 day + */ + updateAge: number } export interface DefaultUser { + id: string name?: string | null email?: string | null image?: string | null diff --git a/types/internals/cookies.d.ts b/types/internals/cookies.d.ts new file mode 100644 index 0000000000..dc6e14aa94 --- /dev/null +++ b/types/internals/cookies.d.ts @@ -0,0 +1,9 @@ +// REVIEW: Is there any way to defer two types of strings? + +/** Stringified form of `JWT`. Extract the content with `jwt.decode` */ +export type JWTString = string + +/** If `options.session.jwt` is set to `true`, this is a stringified `JWT`. In case of a database persisted session, this is the `sessionToken` of the session in the database.. */ +export type SessionToken = T extends "jwt" + ? JWTString + : string diff --git a/types/internals/index.d.ts b/types/internals/index.d.ts index 4bcfffbbf9..35e8bd1c0a 100644 --- a/types/internals/index.d.ts +++ b/types/internals/index.d.ts @@ -8,12 +8,19 @@ import { SessionOptions, Theme, } from ".." -import { AppProvider } from "../providers" -import { JWTOptions } from "next-auth/jwt" -import { Adapter } from "next-auth/adapters" +import { Provider } from "../providers" +import { JWTOptions } from "../jwt" +import { Adapter } from "../adapters" -export interface InternalOptions { - providers: AppProvider[] +export type InternalProvider = Provider & { + signinUrl: string + callbackUrl: string +} + +export interface InternalOptions< + P extends InternalProvider = InternalProvider +> { + providers: InternalProvider[] baseUrl: string basePath: string action: @@ -25,7 +32,7 @@ export interface InternalOptions { | "callback" | "verify-request" | "error" - provider?: AppProvider + provider: P csrfToken?: string csrfTokenVerified?: boolean secret: string @@ -33,10 +40,10 @@ export interface InternalOptions { debug: boolean logger: LoggerInstance session: Required - pages: PagesOptions + pages: Partial jwt: JWTOptions - events: EventCallbacks - adapter: ReturnType + events: Partial + adapter: Adapter callbacks: CallbacksOptions cookies: CookiesOptions callbackUrl: string @@ -48,7 +55,7 @@ export interface NextAuthRequest extends NextApiRequest { export type NextAuthResponse = NextApiResponse -export type NextAuthApiHandler = ( +export type NextAuthApiHandler = ( req: NextAuthRequest, res: NextAuthResponse -) => Awaitable +) => Awaitable diff --git a/types/internals/oauth.d.ts b/types/internals/oauth.d.ts index 9fafd8959c..708d3e083f 100644 --- a/types/internals/oauth.d.ts +++ b/types/internals/oauth.d.ts @@ -1,10 +1,18 @@ -import { AppProviders } from "../providers" -import { Profile, LoggerInstance } from "../index" +import { OAuthConfig } from "../providers" +import { Profile, LoggerInstance, Account } from "../index" import { TokenSet } from "openid-client" export interface GetProfileParams { profile: Profile tokens: TokenSet - provider: AppProviders + provider: OAuthConfig logger: LoggerInstance } + +export interface GetProfileResult { + profile: ReturnType | null + account: Omit | null + OAuthProfile: Profile +} + +export type GetProfile = (params: GetProfileParams) => Promise diff --git a/types/jwt.d.ts b/types/jwt.d.ts index 81b188b13d..0841dc9473 100644 --- a/types/jwt.d.ts +++ b/types/jwt.d.ts @@ -41,7 +41,7 @@ export interface JWTDecodeParams { encryption?: boolean } -export function decode(params?: JWTDecodeParams): Promise +export function decode(params?: JWTDecodeParams): Promise export type GetTokenParams = { req: NextApiRequest @@ -58,12 +58,12 @@ export function getToken( ): Promise export interface JWTOptions { - secret?: string - maxAge?: number + secret: string + maxAge: number encryption?: boolean signingKey?: string encryptionKey?: string - encode?: typeof encode - decode?: typeof decode + encode: typeof encode + decode: typeof decode verificationOptions?: JoseJWT.VerifyOptions } diff --git a/types/providers/credentials.d.ts b/types/providers/credentials.d.ts index 42aa20f44d..5d7a996e42 100644 --- a/types/providers/credentials.d.ts +++ b/types/providers/credentials.d.ts @@ -18,7 +18,7 @@ export interface CredentialsConfig authorize( credentials: Record, req: NextApiRequest - ): Awaitable + ): Awaitable<(Omit | { id?: string }) | null> } export type CredentialsProvider = >( diff --git a/types/providers/email.d.ts b/types/providers/email.d.ts index 8cfa6eee38..facc166410 100644 --- a/types/providers/email.d.ts +++ b/types/providers/email.d.ts @@ -2,14 +2,6 @@ import { CommonProviderOptions } from "." import { Options as SMTPConnectionOptions } from "nodemailer/lib/smtp-connection" import { Awaitable } from "../internals/utils" -export type SendVerificationRequest = (params: { - identifier: string - url: string - baseUrl: string - token: string - provider: EmailConfig -}) => Awaitable - export interface EmailConfig extends CommonProviderOptions { type: "email" // TODO: Make use of https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html @@ -22,7 +14,27 @@ export interface EmailConfig extends CommonProviderOptions { * @default 86400 */ maxAge?: number - sendVerificationRequest: SendVerificationRequest + sendVerificationRequest(params: { + identifier: string + url: string + expires: Date + provider: EmailConfig + token: string + }): 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 + * ```js + * Providers.Email({ + * async generateVerificationToken() { + * return "ABC123" + * } + * }) + * ``` + * [Documentation](https://next-auth.js.org/providers/email#customising-the-verification-token) + */ + generateVerificationToken?(): Awaitable } export type EmailProvider = (options: Partial) => EmailConfig diff --git a/types/tests/jwt.test.ts b/types/tests/jwt.test.ts index fe453a69e1..af9b08cd0b 100644 --- a/types/tests/jwt.test.ts +++ b/types/tests/jwt.test.ts @@ -7,7 +7,7 @@ JWTType.encode({ secret: "secret", }) -// $ExpectType Promise +// $ExpectType Promise JWTType.decode({ token: "token", secret: "secret", diff --git a/types/tests/providers.test.ts b/types/tests/providers.test.ts index ac8c180a3e..d56de1770e 100644 --- a/types/tests/providers.test.ts +++ b/types/tests/providers.test.ts @@ -63,9 +63,7 @@ CredentialsProvider({ }, }, async authorize({ username, password }) { - return { - /* fetched user */ - } + return null }, }) diff --git a/types/tests/react.test.ts b/types/tests/react.test.ts index 2660adc248..b81ea6c73d 100644 --- a/types/tests/react.test.ts +++ b/types/tests/react.test.ts @@ -90,7 +90,7 @@ client.SessionProvider({ client.SessionProvider({ children: null, session: { - expires: "", + expires: "1234", }, baseUrl: "https://foo.com", basePath: "/", diff --git a/types/tests/server.test.ts b/types/tests/server.test.ts index 63f3c272bd..54cb497cc8 100644 --- a/types/tests/server.test.ts +++ b/types/tests/server.test.ts @@ -1,10 +1,9 @@ import { OAuthConfig } from "next-auth/providers" -import { Adapter } from "next-auth/adapters" +import { Adapter, AdapterSession, AdapterUser } from "next-auth/adapters" import NextAuth, * as NextAuthTypes from "next-auth" import { IncomingMessage, ServerResponse } from "http" import { Socket } from "net" import { NextApiRequest, NextApiResponse } from "internals/utils" -import { InternalOptions } from "internals" import GitHubProvider from "next-auth/providers/github" import TwitterProvider from "next-auth/providers/twitter" @@ -47,92 +46,70 @@ const simpleConfig = { ], } -const exampleUser: NextAuthTypes.User = { +const exampleUser: AdapterUser = { + id: "", + emailVerified: null, name: "", image: "", email: "", } -const exampleSession: NextAuthTypes.Session = { +const exampleSession: AdapterSession = { + sessionToken: "0000", + id: "", userId: "", - accessToken: "", - sessionToken: "", + expires: new Date(), } -const exampleVerificationRequest = { - id: "", - identifier: "", - token: "", - expires: new Date(), +interface Client { + c(): void + r(): void + u(): void + d(): void } -const MyAdapter: Adapter> = () => { +function MyAdapter(client: Client): Adapter { return { - async getAdapter(appOptions: InternalOptions) { - return { - async createUser(profile) { - return exampleUser - }, - async getUser(id) { - return exampleUser - }, - async getUserByEmail(email) { - return exampleUser - }, - async getUserByProviderAccountId(providerId, providerAccountId) { - return exampleUser - }, - async updateUser(user) { - return exampleUser - }, - async linkAccount( - userId, - providerId, - providerType, - providerAccountId, - refreshToken, - accessToken, - accessTokenExpires - ) { - return undefined - }, - async createSession(user) { - return exampleSession - }, - async getSession(sessionToken) { - return exampleSession - }, - async updateSession(session, force) { - return exampleSession - }, - async deleteSession(sessionToken) { - return undefined - }, - async createVerificationRequest(email, url, token, secret, provider) { - return undefined - }, - async getVerificationRequest( - email, - verificationToken, - secret, - provider - ) { - return exampleVerificationRequest - }, - async deleteVerificationRequest( - email, - verificationToken, - secret, - provider - ) { - return undefined - }, - } + async createUser(profile) { + return exampleUser + }, + async getUser(id) { + return exampleUser + }, + async getUserByEmail(email) { + return exampleUser + }, + async getUserByAccount(providerAccountId) { + return exampleUser + }, + async updateUser(user) { + return exampleUser + }, + async linkAccount({}) { + return undefined + }, + async createSession(user) { + return exampleSession + }, + async getSessionAndUser() { + return { session: exampleSession, user: exampleUser } + }, + async updateSession(session) { + return exampleSession + }, + async deleteSession(sessionToken) { + return exampleSession + }, + async createVerificationToken(params) { + return undefined + }, + async useVerificationToken(params) { + return null }, } } -const client = {} // Create a fake db client +const client = { c() {}, r() {}, u() {}, d() {} } // Create a fake db client const allConfig: NextAuthTypes.NextAuthOptions = { providers: [ @@ -143,6 +120,10 @@ const allConfig: NextAuthTypes.NextAuthOptions = { ], debug: true, secret: "my secret", + theme: "dark", + logger: { + debug: () => undefined, + }, session: { jwt: true, maxAge: 365, diff --git a/www/docs/adapters/models.md b/www/docs/adapters/models.md index 148a59e482..7dc7362244 100644 --- a/www/docs/adapters/models.md +++ b/www/docs/adapters/models.md @@ -3,60 +3,52 @@ id: models title: Models --- -Models in NextAuth.js are built for ANSI SQL but are polymorphic and are transformed to adapt to the database being used; there is some variance in specific data types (e.g. for datetime, text fields, etc) but they are functionally the same with as much parity in behaviour as possible. +NextAuth.js can be used with any database. Models tell you what structures NextAuth.js expects from your database. Models will vary slightly depending on which adapter you use, but in general will look something like this. -All table/collection names in the built in models are plural, and all table names and column names use `snake_case` when used with an SQL database and `camelCase` when used with Document database. +![v4 Schema](/img/nextauth_v4_schema.png) + +More information about each Model / Table can be found below. :::note -You can [extend the built in models](/tutorials/typeorm-custom-models) and even [create your own database adapter](/tutorials/creating-a-database-adapter) if you want to use NextAuth.js with a database that is not supported out of the box. +You can [create your own adapter](/tutorials/creating-a-database-adapter) if you want to use NextAuth.js with a database that is not supported out of the box, or you have to change fields on any of the models. ::: --- ## User -Table: `users` - -**Description:** - -The User model is for information such as the users name and email address. +The User model is for information such as the user's name and email address. -Email address are optional, but if one is specified for a User then it must be unique. +Email address is optional, but if one is specified for a User then it must be unique. :::note If a user first signs in with OAuth then their email address is automatically populated using the one from their OAuth profile, if the OAuth provider returns one. -This provides a way to contact users and for users to maintain access to their account and sign in using email in the event they are unable to sign in with the OAuth provider in future (if email sign in is configured). +This provides a way to contact users and for users to maintain access to their account and sign in using email in the event they are unable to sign in with the OAuth provider in future (if the [Email Provider](/providers/email) is configured). ::: ## Account -Table: `accounts` - -**Description:** - The Account model is for information about OAuth accounts associated with a User. -A single User can have multiple Accounts, each Account can only have one User. +A single User can have multiple Accounts, but each Account can only have one User. -## Session +Linking Accounts to Users happen automatically, only when they have the same e-mail address, and the user is currently signed in. Check the [FAQ](/faq#security) for more information why this is a requirement. -Table: `sessions` - -**Description:** +## Session -The Session model is used for database sessions. It is not used if JSON Web Tokens are enabled. +The Session model is used for database sessions. It is not used if JSON Web Tokens are enabled. Keep in mind, that you can use a database to persist Users and Accounts, and still use JWT for sessions. See the [`session.jwt`](/configuration/options#session) option. A single User can have multiple Sessions, each Session can only have one User. -## Verification Request - -Table: `verification_requests` +## Verification Token -**Description:** +The Verification Token model is used to store tokens for passwordless sign in. -The Verification Request model is used to store tokens for passwordless sign in emails. +A single User can have multiple open Verification Tokens (e.g. to sign in to different devices). -A single User can have multiple open Verification Requests (e.g. to sign in to different devices). +It has been designed to be extendable for other verification purposes in the future (e.g. 2FA / short codes). -It has been designed to be extendable for other verification purposes in future (e.g. 2FA / short codes). +:::note +NextAuth.js makes sure that every token is usable only once, and by default has a short lifetime. If your user did not manage to finish the sign-in flow in time (15 minutes by default), they will have to start the sign-in process again. +::: diff --git a/www/docs/adapters/overview.md b/www/docs/adapters/overview.md index 3effb1f474..5a76443cac 100644 --- a/www/docs/adapters/overview.md +++ b/www/docs/adapters/overview.md @@ -3,15 +3,18 @@ id: overview title: Overview --- -An **Adapter** in NextAuth.js connects your application to whatever database or backend system you want to use to store data for user accounts, sessions, etc. +An **Adapter** in NextAuth.js connects your application to whatever database or backend system you want to use to store data for users, their accounts, sessions, etc. Adapters are optional, unless you need to persist user information in your own database, or you wnt implement certain flows. The [Email Provider](/providers/email) requires an adapter to be able to save [Verification Tokens](/adapters/models#verification-token). -The adapters can be found in their own repository under [`nextauthjs/adapters`](https://github.com/nextauthjs/adapters). +:::tip +When using a database, you can still use JWT for session handling for fast access. See the [`session.jwt`](/configuration/options#session) option. Read about the trade-offs of JWT in this [FAQ section](/faq#json-web-tokens). +::: + +The official adapters can be found in their own repository under [`nextauthjs/adapters`](https://github.com/nextauthjs/adapters). There you can find the following adapters: - [`typeorm-legacy`](./typeorm/typeorm-overview) - [`prisma`](./prisma) -- [`prisma-legacy`](./prisma-legacy) - [`fauna`](./fauna) - [`dynamodb`](./dynamodb) - [`firebase`](./firebase) @@ -19,21 +22,21 @@ There you can find the following adapters: ## Custom Adapter -See the tutorial for [creating a database Adapter](/tutorials/creating-a-database-adapter) for more information on how to create a custom Adapter. Have a look at the [Adapter repository](https://github.com/nextauthjs/adapters) to see community maintained custom Adapter or add your own. +See the tutorial for [creating a database Adapter](/tutorials/creating-a-database-adapter) for more information on how to create a custom Adapter. + +:::tip +If you would like to see a new adapter in the official repository, please [open a PR](https://github.com/nextauthjs/adapters) and we will help you. +::: ### Editor integration When writing your own custom Adapter in plain JavaScript, note that you can use **JSDoc** to get helpful editor hints and auto-completion like so: ```js -/** @type { import("next-auth/adapters").Adapter } */ -const MyAdapter = () => { +/** @return { import("next-auth/adapters").Adapter } */ +function MyAdapter() { return { - async getAdapter() { - return { - // your adapter methods here - } - }, + // your adapter methods here } } ``` diff --git a/www/docs/adapters/prisma-legacy.md b/www/docs/adapters/prisma-legacy.md deleted file mode 100644 index 1cc9a40b30..0000000000 --- a/www/docs/adapters/prisma-legacy.md +++ /dev/null @@ -1,174 +0,0 @@ ---- -id: prisma-legacy -title: Prisma Adapter (Legacy) ---- - -# Prisma (Legacy) - -You can also use NextAuth.js with the built-in Adapter for [Prisma](https://www.prisma.io/docs/). This is included in the core `next-auth` package at the moment. The other adapter needs to be installed from its own additional package. - -:::info -You may have noticed there is a `prisma` and `prisma-legacy` adapter. This is due to historical reasons, but the code has mostly converged so that there is no longer much difference between the two. The legacy adapter, however, does have the ability to rename tables which the newer version does not. -::: - -To use this Adapter, you need to install Prisma Client and Prisma CLI: - -``` -npm install @prisma/client -npm install prisma --save-dev -``` - -Configure your NextAuth.js to use the Prisma Adapter: - -```javascript title="pages/api/auth/[...nextauth].js" -import NextAuth from "next-auth" -import Providers from "next-auth/providers" -import { PrismaLegacyAdapter } from "@next-auth/prisma-legacy-adapter" -import { PrismaClient } from "@prisma/client" - -const prisma = new PrismaClient() - -export default NextAuth({ - providers: [ - Providers.Google({ - clientId: process.env.GOOGLE_CLIENT_ID, - clientSecret: process.env.GOOGLE_CLIENT_SECRET, - }), - ], - adapter: PrismaLegacyAdapter({ prisma }), -}) -``` - -:::tip -While Prisma includes an experimental feature in the migration command that is able to generate SQL from a schema, creating tables and columns using the provided SQL is currently recommended instead as SQL schemas automatically generated by Prisma may differ from the recommended schemas. -::: -Schema for the Prisma Adapter - -## Setup - -Create a schema file in `prisma/schema.prisma` similar to this one: - -```json title="schema.prisma" -generator client { - provider = "prisma-client-js" -} - -datasource db { - provider = "sqlite" - url = "file:./dev.db" -} - -model Account { - id Int @id @default(autoincrement()) - compoundId String @unique @map(name: "compound_id") - userId Int @map(name: "user_id") - providerType String @map(name: "provider_type") - providerId String @map(name: "provider_id") - providerAccountId String @map(name: "provider_account_id") - refreshToken String? @map(name: "refresh_token") - accessToken String? @map(name: "access_token") - accessTokenExpires DateTime? @map(name: "access_token_expires") - createdAt DateTime @default(now()) @map(name: "created_at") - updatedAt DateTime @default(now()) @map(name: "updated_at") - - @@index([providerAccountId], name: "providerAccountId") - @@index([providerId], name: "providerId") - @@index([userId], name: "userId") - @@map(name: "accounts") -} - -model Session { - id Int @id @default(autoincrement()) - userId Int @map(name: "user_id") - expires DateTime - sessionToken String @unique @map(name: "session_token") - accessToken String @unique @map(name: "access_token") - createdAt DateTime @default(now()) @map(name: "created_at") - updatedAt DateTime @default(now()) @map(name: "updated_at") - - @@map(name: "sessions") -} - -model User { - id Int @id @default(autoincrement()) - name String? - email String? @unique - emailVerified DateTime? @map(name: "email_verified") - image String? - createdAt DateTime @default(now()) @map(name: "created_at") - updatedAt DateTime @default(now()) @map(name: "updated_at") - - @@map(name: "users") -} - -model VerificationRequest { - id Int @id @default(autoincrement()) - identifier String - token String @unique - expires DateTime - createdAt DateTime @default(now()) @map(name: "created_at") - updatedAt DateTime @default(now()) @map(name: "updated_at") - - @@map(name: "verification_requests") -} - - -``` - -### Generate Client - -Once you have saved your schema, use the Prisma CLI to generate the Prisma Client: - -``` -npx prisma generate -``` - -To configure you database to use the new schema (i.e. create tables and columns) use the `prisma migrate` command: - -``` -npx prisma migrate dev -``` - -To generate a schema in this way with the above example code, you will need to specify your database connection string in the environment variable `DATABASE_URL`. You can do this by setting it in a `.env` file at the root of your project. - -As this feature is experimental in Prisma, it is behind a feature flag. You should check your database schema manually after using this option. See the [Prisma documentation](https://www.prisma.io/docs/) for information on how to use `prisma migrate`. - -:::tip -If you experience issues with Prisma opening too many database connections in local development mode (e.g. due to Hot Module Reloading) you can use an approach like this when initalising the Prisma Client: - -```javascript title="pages/api/auth/[...nextauth].js" -let prisma - -if (process.env.NODE_ENV === "production") { - prisma = new PrismaClient() -} else { - if (!global.prisma) { - global.prisma = new PrismaClient() - } - prisma = global.prisma -} -``` - -::: - -### Custom Models - -You can add properties to the schema and map them to any database column names you wish, but you should not change the base properties or types defined in the example schema. - -The model names themselves can be changed with a configuration option, and the datasource can be changed to anything supported by Prisma. - -You can use custom model names by using the `modelMapping` option (shown here with default values). - -```javascript title="pages/api/auth/[...nextauth].js" -... -adapter: PrismaLegacyAdapter({ - prisma, - modelMapping: { - User: 'user', - Account: 'account', - Session: 'session', - VerificationRequest: 'verificationRequest' - } -}) -... -``` diff --git a/www/docs/adapters/prisma.md b/www/docs/adapters/prisma.md index 96b315cc45..8a25e8e6a4 100644 --- a/www/docs/adapters/prisma.md +++ b/www/docs/adapters/prisma.md @@ -5,12 +5,6 @@ title: Prisma Adapter # Prisma -You can also use NextAuth.js with the new experimental Adapter for [Prisma](https://www.prisma.io/docs/). This version of the Prisma Adapter is not included in the core `next-auth` package, and must be installed separately. - -:::info -You may have noticed there is a `prisma` and `prisma-legacy` adapter. This is due to historical reasons, but the code has mostly converged so that there is no longer much difference between the two. The legacy adapter, however, does have the ability to rename tables which the newer version does not. -::: - To use this Adapter, you need to install Prisma Client, Prisma CLI, and the separate `@next-auth/prisma-adapter` package: ``` @@ -46,43 +40,46 @@ Schema for the Prisma Adapter (`@next-auth/prisma-adapter`) ## Setup -Create a schema file in `prisma/schema.prisma` similar to this one: +You need to use at least Prisma 2.26.0. Create a schema file in `prisma/schema.prisma` similar to this one: ```json title="schema.prisma" -generator client { - provider = "prisma-client-js" -} - datasource db { provider = "sqlite" url = "file:./dev.db" } +generator client { + provider = "prisma-client-js" + previewFeatures = ["referentialActions"] +} + model Account { - id String @id @default(cuid()) + id String @id @default(cuid()) userId String - providerType String - providerId String + type String + provider String providerAccountId String - refreshToken String? - accessToken String? - accessTokenExpires DateTime? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - user User @relation(fields: [userId], references: [id]) - - @@unique([providerId, providerAccountId]) + refresh_token String? + access_token String? + expires_at Int? + token_type String? + scope String? + id_token String? + session_state String? + oauth_token_secret String? + oauth_token String? + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@unique([provider, providerAccountId]) } model Session { id String @id @default(cuid()) + sessionToken String @unique userId String expires DateTime - sessionToken String @unique - accessToken String @unique - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - user User @relation(fields: [userId], references: [id]) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) } model User { @@ -91,23 +88,17 @@ model User { email String? @unique emailVerified DateTime? image String? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt accounts Account[] sessions Session[] } -model VerificationRequest { - id String @id @default(cuid()) +model VerificationToken { identifier String token String @unique expires DateTime - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt @@unique([identifier, token]) } - ``` ### Generate Client @@ -126,93 +117,4 @@ npx prisma migrate dev To generate a schema in this way with the above example code, you will need to specify your database connection string in the environment variable `DATABASE_URL`. You can do this by setting it in a `.env` file at the root of your project. -As this feature is experimental in Prisma, it is behind a feature flag. You should check your database schema manually after using this option. See the [Prisma documentation](https://www.prisma.io/docs/) for information on how to use `prisma migrate`. - -## Schema History - -Changes from the original Prisma Adapter - -```diff - model Account { -- id Int @default(autoincrement()) @id -+ id String @id @default(cuid()) -- compoundId String @unique @map(name: "compound_id") -- userId Int @map(name: "user_id") -+ userId String -+ user User @relation(fields: [userId], references: [id]) -- providerType String @map(name: "provider_type") -+ providerType String -- providerId String @map(name: "provider_id") -+ providerId String -- providerAccountId String @map(name: "provider_account_id") -+ providerAccountId String -- refreshToken String? @map(name: "refresh_token") -+ refreshToken String? -- accessToken String? @map(name: "access_token") -+ accessToken String? -- accessTokenExpires DateTime? @map(name: "access_token_expires") -+ accessTokenExpires DateTime? -- createdAt DateTime @default(now()) @map(name: "created_at") -+ createdAt DateTime @default(now()) -- updatedAt DateTime @default(now()) @map(name: "updated_at") -+ updatedAt DateTime @updatedAt - -- @@index([providerAccountId], name: "providerAccountId") -- @@index([providerId], name: "providerId") -- @@index([userId], name: "userId") -- @@map(name: "accounts") -+ @@unique([providerId, providerAccountId]) - } - - model Session { -- id Int @default(autoincrement()) @id -+ id String @id @default(cuid()) -- userId Int @map(name: "user_id") -+ userId String -+ user User @relation(fields: [userId], references: [id]) - expires DateTime -- sessionToken String @unique @map(name: "session_token") -+ sessionToken String @unique -- accessToken String @unique @map(name: "access_token") -+ accessToken String @unique -- createdAt DateTime @default(now()) @map(name: "created_at") -+ createdAt DateTime @default(now()) -- updatedAt DateTime @default(now()) @map(name: "updated_at") -+ updatedAt DateTime @updatedAt -- -- @@map(name: "sessions") - } - - model User { -- id Int @default(autoincrement()) @id -+ id String @id @default(cuid()) - name String? - email String? @unique -- emailVerified DateTime? @map(name: "email_verified") -+ emailVerified DateTime? - image String? -+ accounts Account[] -+ sessions Session[] -- createdAt DateTime @default(now()) @map(name: "created_at") -+ createdAt DateTime @default(now()) -- updatedAt DateTime @default(now()) @map(name: "updated_at") -+ updatedAt DateTime @updatedAt - -- @@map(name: "users") - } - - model VerificationRequest { -- id Int @default(autoincrement()) @id -+ id String @id @default(cuid()) - identifier String - token String @unique - expires DateTime -- createdAt DateTime @default(now()) @map(name: "created_at") -+ createdAt DateTime @default(now()) -- updatedAt DateTime @default(now()) @map(name: "updated_at") -+ updatedAt DateTime @updatedAt - -- @@map(name: "verification_requests") -+ @@unique([identifier, token]) - } -``` +As this feature is experimental in Prisma, it is behind a feature flag. You should check your database schema manually after using this option. See the [Prisma documentation](https://www.prisma.io/docs/) for information on how to use `prisma migrate`. \ No newline at end of file diff --git a/www/docs/adapters/typeorm/overview.md b/www/docs/adapters/typeorm/overview.md index 733093b954..3c19a70cf6 100644 --- a/www/docs/adapters/typeorm/overview.md +++ b/www/docs/adapters/typeorm/overview.md @@ -5,8 +5,6 @@ title: Overview ## TypeORM Adapter -NextAuth.js comes with a default Adapter that uses [TypeORM](https://typeorm.io/) so that it can be used with many different databases without any further configuration, you simply add the node module for the database driver you want to use in your project and pass a database connection string to NextAuth.js. - ### Database Schemas Configure your database by creating the tables and columns to match the schema expected by NextAuth.js. @@ -16,32 +14,6 @@ Configure your database by creating the tables and columns to match the schema e - [Microsoft SQL Server Schema](./mssql) - [MongoDB](./mongodb) -The default Adapter is the TypeORM Adapter and the default database type for TypeORM is SQLite, the following configuration options are exactly equivalent. - -```javascript -database: { - type: 'sqlite', - database: ':memory:', - synchronize: true -} -``` - -```javascript -adapter: Adapters.Default({ - type: "sqlite", - database: ":memory:", - synchronize: true, -}) -``` - -```javascript -adapter: Adapters.TypeORM.Adapter({ - type: "sqlite", - database: ":memory:", - synchronize: true, -}) -``` - The tutorial [Custom models with TypeORM](/tutorials/typeorm-custom-models) explains how to extend the built in models and schemas used by the TypeORM Adapter. You can use these models in your own code. :::tip diff --git a/www/docs/configuration/databases.md b/www/docs/configuration/databases.md index a821df8448..94ef1d7c2a 100644 --- a/www/docs/configuration/databases.md +++ b/www/docs/configuration/databases.md @@ -7,7 +7,6 @@ NextAuth.js offers multiple database adapters: - [`typeorm-legacy`](./../adapters/typeorm/typeorm-overview) - [`prisma`](./../adapters/prisma) -- [`prisma-legacy`](./../adapters/prisma-legacy) - [`fauna`](./../adapters/fauna) - [`dynamodb`](./../adapters/dynamodb) - [`firebase`](./../adapters/firebase) diff --git a/www/docs/configuration/options.md b/www/docs/configuration/options.md index 24d78b0ee3..b8644849aa 100644 --- a/www/docs/configuration/options.md +++ b/www/docs/configuration/options.md @@ -169,12 +169,12 @@ An example JSON Web Token contains a payload like this: You can use the built-in `getToken()` helper method to verify and decrypt the token, like this: ```js -import jwt from "next-auth/jwt" +import { getToken } from "next-auth/jwt" const secret = process.env.JWT_SECRET export default async (req, res) => { - const token = await jwt.getToken({ req, secret }) + const token = await getToken({ req, secret }) console.log("JSON Web Token", token) res.end() } diff --git a/www/docs/getting-started/typescript.md b/www/docs/getting-started/typescript.md index 81c9773321..da4b79c693 100644 --- a/www/docs/getting-started/typescript.md +++ b/www/docs/getting-started/typescript.md @@ -17,13 +17,9 @@ If you're writing your own custom Adapter, you can take advantage of the types t ```ts import type { Adapter } from "next-auth/adapters" -const MyAdapter: Adapter = () => { +function MyAdapter(): Adapter { return { - async getAdapter() { - return { - // your adapter methods here - } - }, + // your adapter methods here } } ``` @@ -31,14 +27,10 @@ const MyAdapter: Adapter = () => { When writing your own custom Adapter in plain JavaScript, note that you can use **JSDoc** to get helpful editor hints and auto-completion like so: ```js -/** @type { import("next-auth/adapters").Adapter } */ -const MyAdapter = () => { +/** @return { import("next-auth/adapters").Adapter } */ +function MyAdapter() { return { - async getAdapter() { - return { - // your adapter methods here - } - }, + // your adapter methods here } } ``` diff --git a/www/docs/providers/credentials.md b/www/docs/providers/credentials.md index 9d85f3fea9..05a8f15d01 100644 --- a/www/docs/providers/credentials.md +++ b/www/docs/providers/credentials.md @@ -31,13 +31,9 @@ The Credentials provider is specified like other providers, except that you need If you return an object it will be persisted to the JSON Web Token and the user will be signed in, unless a custom `signIn()` callback is configured that subsequently rejects it. -2. Either `false` or `null`, which indicates failure. +2. If you return `null` then an error will be displayed advising the user to check their details. -If you return `false` or `null` then an error will be displayed advising the user to check their details. - -3. You can throw an Error or a URL (a string). - -If you throw an Error, the user will be sent to the error page with the error message as a query parameter. If throw a URL (a string), the user will be redirected to the URL. +3. If you throw an Error, the user will be sent to the error page with the error message as a query parameter. The Credentials provider's `authorize()` method also provides the request object as the second parameter (see example below). diff --git a/www/docs/providers/email.md b/www/docs/providers/email.md index 9440179dd5..2857dcbdce 100644 --- a/www/docs/providers/email.md +++ b/www/docs/providers/email.md @@ -31,14 +31,14 @@ You can override any of the options to suit your own use case. ## Configuration -1. You will need an SMTP account; ideally for one of the [services known to work with nodemailer](http://nodemailer.com/smtp/well-known/). +1. You will need an SMTP account; ideally for one of the [services known to work with `nodemailer`](http://nodemailer.com/smtp/well-known/). 2. There are two ways to configure the SMTP server connection. -You can either use a connection string or a nodemailer configuration object. +You can either use a connection string or a `nodemailer` configuration object. 2.1 **Using a connection string** -Create an .env file to the root of your project and add the connection string and email address. +Create an `.env` file to the root of your project and add the connection string and email address. ```js title=".env" {1} EMAIL_SERVER=smtp://username:password@smtp.example.com:587 @@ -90,9 +90,9 @@ providers: [ A user account (i.e. an entry in the Users table) will not be created for the user until the first time they verify their email address. If an email address is already associated with an account, the user will be signed in to that account when they use the link in the email. -## Customising emails +## Customizing emails -You can fully customise the sign in email that is sent by passing a custom function as the `sendVerificationRequest` option to `Providers.Email()`. +You can fully customize the sign in email that is sent by passing a custom function as the `sendVerificationRequest` option to `Providers.Email()`. e.g. @@ -202,7 +202,7 @@ const html = ({ url, site, email }) => { ` } -// Email text body – fallback for email clients that don't render HTML +// Email text body - fallback for email clients that don't render HTML const text = ({ url, site }) => `Sign in to ${site}\n${url}\n\n` ``` @@ -210,7 +210,7 @@ const text = ({ url, site }) => `Sign in to ${site}\n${url}\n\n` If you want to generate great looking email client compatible HTML with React, check out https://mjml.io ::: -## Customising the Verification Token +## Customizing the Verification Token By default, we are generating a random verification token. You can define a `generateVerificationToken` method in your provider options if you want to override it: diff --git a/www/docs/tutorials/creating-a-database-adapter.md b/www/docs/tutorials/creating-a-database-adapter.md index 5045111a5e..0a5371e36e 100644 --- a/www/docs/tutorials/creating-a-database-adapter.md +++ b/www/docs/tutorials/creating-a-database-adapter.md @@ -1,134 +1,91 @@ --- id: creating-a-database-adapter -title: Creating a database adapter +title: Create an adapter --- -Using a custom adapter you can connect to any database backend or even several different databases. Custom adapters created and maintained by our community can be found in the [adapters repository](https://github.com/nextauthjs/adapters). Feel free to add a custom adapter from your project to the repository, or even become a maintainer of a certain adapter. Custom adapters can still be created and used in a project without being added to the repository. - -Creating a custom adapter can be considerable undertaking and will require some trial and error and some reverse engineering using the built-in adapters for reference. +Using a custom adapter you can connect to any database back-end or even several different databases. Official adapters created and maintained by our community can be found in the [adapters repository](https://github.com/nextauthjs/adapters). Feel free to add a custom adapter from your project to the repository, or even become a maintainer of a certain adapter. Custom adapters can still be created and used in a project without being added to the repository. ## How to create an adapter -From an implementation perspective, an adapter in NextAuth.js is a function which returns an async `getAdapter()` method, which in turn returns a Promise with a list of functions used to handle operations such as creating user, linking a user and an OAuth account or handling reading and writing sessions. +_See the code below for practical example._ -It uses this approach to allow database connection logic to live in the `getAdapter()` method. By calling the function just before an action needs to happen, it is possible to check database connection status and handle connecting / reconnecting to a database as required. +### Example code + +```ts +/** @return { import("next-auth/adapters").Adapter } */ +export default function MyAdapter(client, options = {}) { + return { + async createUser(user) { + return + }, + async getUser(id) { + return + }, + async getUserByEmail(email) { + return + }, + async getUserByAccount({ provider, id }) { + return + }, + async updateUser(user) { + return + }, + async deleteUser(userId) { + return + }, + async linkAccount(account) { + return + }, + async unlinkAccount({ provider, id }) { + return + }, + async createSession({ sessionToken, userId, expires }) { + return + }, + async getSessionAndUser(sessionToken) { + return + }, + async updateSession({ sessionToken }) { + return + }, + async deleteSession(sessionToken) { + return + }, + async createVerificationToken({ identifier, expires, token }) { + return + }, + async useVerificationToken({ identifier, token }) { + return + }, + } +} +``` -_See the code below for practical example._ ### Required methods These methods are required for all sign in flows: -* createUser -* getUser -* getUserByEmail -* getUserByProviderAccountId -* linkAccount -* createSession -* getSession -* updateSession -* deleteSession -* updateUser +* `createUser` +* `getUser` +* `getUserByEmail` +* `getUserByAccount` +* `linkAccount` +* `createSession` +* `getSessionAndUser` +* `updateSession` +* `deleteSession` +* `updateUser` These methods are required to support email / passwordless sign in: -* createVerificationRequest -* getVerificationRequest -* deleteVerificationRequest +* `createVerificationToken` +* `useVerificationToken` ### Unimplemented methods These methods will be required in a future release, but are not yet invoked: -* deleteUser -* unlinkAccount - -### Example code +* `deleteUser` +* `unlinkAccount` -```js -export default function YourAdapter (config, options = {}) { - return { - async getAdapter (appOptions) { - async createUser (profile) { - return null - }, - async getUser (id) { - return null - }, - async getUserByEmail (email) { - return null - }, - async getUserByProviderAccountId ( - providerId, - providerAccountId - ) { - return null - }, - async updateUser (user) { - return null - }, - async deleteUser (userId) { - return null - }, - async linkAccount ( - userId, - providerId, - providerType, - providerAccountId, - refreshToken, - accessToken, - accessTokenExpires - ) { - return null - }, - async unlinkAccount ( - userId, - providerId, - providerAccountId - ) { - return null - }, - async createSession (user) { - return null - }, - async getSession (sessionToken) { - return null - }, - async updateSession ( - session, - force - ) { - return null - }, - async deleteSession (sessionToken) { - return null - }, - async createVerificationRequest ( - identifier, - url, - token, - secret, - provider - ) { - return null - }, - async getVerificationRequest ( - identifier, - token, - secret, - provider - ) { - return null - }, - async deleteVerificationRequest ( - identifier, - token, - secret, - provider - ) { - return null - } - } - } -} -``` diff --git a/www/docs/tutorials/securing-pages-and-api-routes.md b/www/docs/tutorials/securing-pages-and-api-routes.md index a6f642f4b5..e0bc0f3bb5 100644 --- a/www/docs/tutorials/securing-pages-and-api-routes.md +++ b/www/docs/tutorials/securing-pages-and-api-routes.md @@ -120,12 +120,12 @@ If you are using JSON Web Tokens you can use the `getToken()` helper to access t ```js title="pages/api/get-token-example.js" // This is an example of how to read a JSON Web Token from an API route -import jwt from "next-auth/jwt" +import { getToken } from "next-auth/jwt" const secret = process.env.SECRET export default async (req, res) => { - const token = await jwt.getToken({ req, secret }) + const token = await getToken({ req, secret }) if (token) { // Signed in console.log("JSON Web Token", JSON.stringify(token, null, 2)) diff --git a/www/sidebars.js b/www/sidebars.js index aaccda945e..0734af70e3 100644 --- a/www/sidebars.js +++ b/www/sidebars.js @@ -46,7 +46,6 @@ module.exports = { }, "adapters/fauna", "adapters/prisma", - "adapters/prisma-legacy", "adapters/dynamodb", "adapters/firebase", "adapters/pouchdb", diff --git a/www/static/img/nextauth_v4_schema.png b/www/static/img/nextauth_v4_schema.png new file mode 100644 index 0000000000..a41d3d8fc9 Binary files /dev/null and b/www/static/img/nextauth_v4_schema.png differ