mirror of
https://github.com/SrIzan10/next-auth.git
synced 2026-05-01 10:55:20 +00:00
Compare commits
7 Commits
next-auth@
...
next-auth@
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2adfadefdc | ||
|
|
32fa01f939 | ||
|
|
ae834f1e08 | ||
|
|
4d4c276627 | ||
|
|
f4c0d5ab5d | ||
|
|
01cd6b0f7b | ||
|
|
993c0f46b0 |
1
.github/workflows/release.yml
vendored
1
.github/workflows/release.yml
vendored
@@ -70,6 +70,7 @@ jobs:
|
||||
pnpm release
|
||||
env:
|
||||
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||
NPM_TOKEN_PKG: ${{ secrets.NPM_TOKEN_PKG }}
|
||||
NPM_TOKEN_ORG: ${{ secrets.NPM_TOKEN_ORG }}
|
||||
release-pr:
|
||||
|
||||
@@ -46,7 +46,10 @@ import BoxyHQSAMLProvider from "next-auth/providers/boxyhq-saml"
|
||||
// })
|
||||
// const adapter = FaunaAdapter(client)
|
||||
export const authOptions: NextAuthOptions = {
|
||||
// adapter,
|
||||
// adapter: {
|
||||
// getUserByEmail: (email) => ({ id: "1", email, emailVerified: null }),
|
||||
// createVerificationToken: (token) => token,
|
||||
// } as any,
|
||||
providers: [
|
||||
// E-mail
|
||||
// Start fake e-mail server with `npm run start:email`
|
||||
|
||||
@@ -11,7 +11,7 @@ This is the Dgraph Adapter for [`next-auth`](https://next-auth.js.org).
|
||||
|
||||
1. Install the necessary packages
|
||||
|
||||
```bash npm2yarn
|
||||
```bash npm2yarn2pnpm
|
||||
npm install next-auth @next-auth/dgraph-adapter
|
||||
```
|
||||
|
||||
@@ -226,22 +226,22 @@ database you must customize next-auth `encode` and `decode` functions, as the de
|
||||
further customize the jwt with roles if you want to implement [`RBAC logic`](https://dgraph.io/docs/graphql/authorization/directive/#role-based-access-control).
|
||||
|
||||
```js
|
||||
import * as jwt from "jsonwebtoken";
|
||||
import * as jwt from "jsonwebtoken"
|
||||
export default NextAuth({
|
||||
session: {
|
||||
strategy: "jwt"
|
||||
strategy: "jwt",
|
||||
},
|
||||
jwt: {
|
||||
secret: process.env.SECRET,
|
||||
encode: async ({ secret, token }) => {
|
||||
return jwt.sign({...token, userId: token.id}, secret, {
|
||||
return jwt.sign({ ...token, userId: token.id }, secret, {
|
||||
algorithm: "HS256",
|
||||
expiresIn: 30 * 24 * 60 * 60, // 30 days
|
||||
});
|
||||
})
|
||||
},
|
||||
decode: async ({ secret, token }) => {
|
||||
return jwt.verify(token, secret, { algorithms: ["HS256"] });
|
||||
}
|
||||
return jwt.verify(token, secret, { algorithms: ["HS256"] })
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
@@ -15,7 +15,7 @@ You can find the full schema in the table structure section below.
|
||||
|
||||
1. Install `next-auth` and `@next-auth/dynamodb-adapter`
|
||||
|
||||
```bash npm2yarn
|
||||
```bash npm2yarn2pnpm
|
||||
npm install next-auth @next-auth/dynamodb-adapter
|
||||
```
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ You can find the Fauna schema and seed information in the docs at [next-auth.js.
|
||||
|
||||
1. Install the necessary packages
|
||||
|
||||
```bash npm2yarn
|
||||
```bash npm2yarn2pnpm
|
||||
npm install next-auth @next-auth/fauna-adapter faunadb
|
||||
```
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ This is the Firebase Adapter for [`next-auth`](https://next-auth.js.org). This p
|
||||
|
||||
1. Install the necessary packages
|
||||
|
||||
```bash npm2yarn
|
||||
```bash npm2yarn2pnpm
|
||||
npm install next-auth @next-auth/firebase-adapter@experimental
|
||||
```
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ title: MikroORM
|
||||
|
||||
To use this Adapter, you need to install Mikro ORM, the driver that suits your database, and the separate `@next-auth/mikro-orm-adapter` package:
|
||||
|
||||
```bash npm2yarn
|
||||
```bash npm2yarn2pnpm
|
||||
npm install next-auth @next-auth/mikro-orm-adapter @mikro-orm/core @mikro-orm/[YOUR DRIVER]
|
||||
```
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ The MongoDB adapter does not handle connections automatically, so you will have
|
||||
|
||||
1. Install the necessary packages
|
||||
|
||||
```bash npm2yarn
|
||||
```bash npm2yarn2pnpm
|
||||
npm install next-auth @next-auth/mongodb-adapter mongodb
|
||||
```
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ This is the Neo4j Adapter for [`next-auth`](https://next-auth.js.org). This pack
|
||||
|
||||
1. Install the necessary packages
|
||||
|
||||
```bash npm2yarn
|
||||
```bash npm2yarn2pnpm
|
||||
npm install next-auth @next-auth/neo4j-adapter neo4j-driver
|
||||
```
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ Depending on your architecture you can use PouchDB's http adapter to reach any d
|
||||
|
||||
1. Install `next-auth` and `@next-auth/pouchdb-adapter`
|
||||
|
||||
```bash npm2yarn
|
||||
```bash npm2yarn2pnpm
|
||||
npm install next-auth @next-auth/pouchdb-adapter
|
||||
```
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ title: Prisma
|
||||
|
||||
To use this Adapter, you need to install Prisma Client, Prisma CLI, and the separate `@next-auth/prisma-adapter` package:
|
||||
|
||||
```bash npm2yarn
|
||||
```bash npm2yarn2pnpm
|
||||
npm install next-auth @prisma/client @next-auth/prisma-adapter
|
||||
npm install prisma --save-dev
|
||||
```
|
||||
|
||||
@@ -11,7 +11,7 @@ This is the Sequelize Adapter for [`next-auth`](https://next-auth.js.org).
|
||||
|
||||
1. Install the necessary packages
|
||||
|
||||
```bash npm2yarn
|
||||
```bash npm2yarn2pnpm
|
||||
npm install next-auth @next-auth/sequelize-adapter sequelize
|
||||
```
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ In the future, we might split up this adapter to support single flavors of SQL f
|
||||
|
||||
To use this Adapter, you need to install the following packages:
|
||||
|
||||
```bash npm2yarn
|
||||
```bash npm2yarn2pnpm
|
||||
npm install next-auth @next-auth/typeorm-legacy-adapter typeorm
|
||||
```
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ title: Upstash Redis
|
||||
|
||||
To use this Adapter, you need to install `@upstash/redis` and `@next-auth/upstash-redis-adapter` package:
|
||||
|
||||
```bash npm2yarn
|
||||
```bash npm2yarn2pnpm
|
||||
npm install @upstash/redis @next-auth/upstash-redis-adapter
|
||||
```
|
||||
|
||||
|
||||
@@ -366,11 +366,14 @@ Changes the color scheme theme of [pages](/configuration/pages) as well as allow
|
||||
|
||||
In addition, you can define a logo URL in `theme.logo` which will be rendered above the main card in the default signin/signout/error/verify-request pages, as well as a `theme.brandColor` which will affect the accent color of these pages.
|
||||
|
||||
The sign-in button's background color will match the `brandColor` and defaults to `"#346df1"`. The text color is `#fff` by default, but if your brand color gives a weak contrast, correct it with the `buttonText` color option.
|
||||
|
||||
```js
|
||||
theme: {
|
||||
colorScheme: "auto", // "auto" | "dark" | "light"
|
||||
brandColor: "", // Hex color code
|
||||
logo: "" // Absolute URL to image
|
||||
logo: "", // Absolute URL to image
|
||||
buttonText: "" // Hex color code
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -427,13 +427,14 @@ This only works on pages where you provide the correct `pageProps`, however. Thi
|
||||
|
||||
```js title="pages/index.js"
|
||||
import { unstable_getServerSession } from "next-auth/next"
|
||||
import { authOptions } from './api/auth/[...nextauth]'
|
||||
|
||||
...
|
||||
|
||||
export async function getServerSideProps(ctx) {
|
||||
export async function getServerSideProps({ req, res }) {
|
||||
return {
|
||||
props: {
|
||||
session: await unstable_getServerSession(ctx)
|
||||
session: await unstable_getServerSession(req, res, authOptions)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,6 +97,7 @@ To protect an API Route, you can use the [`unstable_getServerSession()`](/config
|
||||
|
||||
```javascript title="pages/api/restricted.js" showLineNumbers
|
||||
import { unstable_getServerSession } from "next-auth/next"
|
||||
import { authOptions } from "./api/auth/[...nextauth]"
|
||||
|
||||
export default async (req, res) => {
|
||||
const session = await unstable_getServerSession(req, res, authOptions)
|
||||
|
||||
@@ -13,7 +13,7 @@ We encourage users to try it out and report any and all issues they come across.
|
||||
|
||||
You can upgrade to the new version by running:
|
||||
|
||||
```bash npm2yarn
|
||||
```bash npm2yarn2pnpm
|
||||
npm install next-auth
|
||||
```
|
||||
|
||||
|
||||
@@ -124,67 +124,74 @@ providers: [
|
||||
The following code shows the complete source for the built-in `sendVerificationRequest()` method:
|
||||
|
||||
```js
|
||||
import nodemailer from "nodemailer"
|
||||
import { createTransport } from "nodemailer"
|
||||
|
||||
async function sendVerificationRequest({
|
||||
identifier: email,
|
||||
url,
|
||||
provider: { server, from },
|
||||
}) {
|
||||
async function sendVerificationRequest(params) {
|
||||
const { identifier, url, provider, theme } = params
|
||||
const { host } = new URL(url)
|
||||
const transport = nodemailer.createTransport(server)
|
||||
await transport.sendMail({
|
||||
to: email,
|
||||
from,
|
||||
// NOTE: You are not required to use `nodemailer`, use whatever you want.
|
||||
const transport = createTransport(provider.server)
|
||||
const result = await transport.sendMail({
|
||||
to: identifier,
|
||||
from: provider.from,
|
||||
subject: `Sign in to ${host}`,
|
||||
text: text({ url, host }),
|
||||
html: html({ url, host, email }),
|
||||
html: html({ url, host, theme }),
|
||||
})
|
||||
const failed = result.rejected.concat(result.pending).filter(Boolean)
|
||||
if (failed.length) {
|
||||
throw new Error(`Email(s) (${failed.join(", ")}) could not be sent`)
|
||||
}
|
||||
}
|
||||
|
||||
// Email HTML body
|
||||
function html({ url, host, email }: Record<"url" | "host" | "email", string>) {
|
||||
// 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 escapedHost = `${host.replace(/\./g, "​.")}`
|
||||
/**
|
||||
* Email HTML body
|
||||
* Insert invisible space into domains 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 it to sign in.
|
||||
*
|
||||
* @note We don't add the email address to avoid needing to escape it, if you do, remember to sanitize it!
|
||||
*/
|
||||
function html(params: { url: string; host: string; theme: Theme }) {
|
||||
const { url, host, theme } = params
|
||||
|
||||
// Some simple styling options
|
||||
const backgroundColor = "#f9f9f9"
|
||||
const textColor = "#444444"
|
||||
const mainBackgroundColor = "#ffffff"
|
||||
const buttonBackgroundColor = "#346df1"
|
||||
const buttonBorderColor = "#346df1"
|
||||
const buttonTextColor = "#ffffff"
|
||||
const escapedHost = host.replace(/\./g, "​.")
|
||||
|
||||
const brandColor = theme.brandColor || "#346df1"
|
||||
const color = {
|
||||
background: "#f9f9f9",
|
||||
text: "#444",
|
||||
mainBackground: "#fff",
|
||||
buttonBackground: brandColor,
|
||||
buttonBorder: brandColor,
|
||||
buttonText: theme.buttonText || "#fff",
|
||||
}
|
||||
|
||||
return `
|
||||
<body style="background: ${backgroundColor};">
|
||||
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
||||
<body style="background: ${color.background};">
|
||||
<table width="100%" border="0" cellspacing="20" cellpadding="0"
|
||||
style="background: ${color.mainBackground}; max-width: 600px; margin: auto; border-radius: 10px;">
|
||||
<tr>
|
||||
<td align="center" style="padding: 10px 0px 20px 0px; font-size: 22px; font-family: Helvetica, Arial, sans-serif; color: ${textColor};">
|
||||
<strong>${escapedHost}</strong>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table width="100%" border="0" cellspacing="20" cellpadding="0" style="background: ${mainBackgroundColor}; max-width: 600px; margin: auto; border-radius: 10px;">
|
||||
<tr>
|
||||
<td align="center" style="padding: 10px 0px 0px 0px; font-size: 18px; font-family: Helvetica, Arial, sans-serif; color: ${textColor};">
|
||||
Sign in as <strong>${escapedEmail}</strong>
|
||||
<td align="center"
|
||||
style="padding: 10px 0px; font-size: 22px; font-family: Helvetica, Arial, sans-serif; color: ${color.text};">
|
||||
Sign in to <strong>${escapedHost}</strong>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" style="padding: 20px 0;">
|
||||
<table border="0" cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td align="center" style="border-radius: 5px;" bgcolor="${buttonBackgroundColor}"><a href="${url}" target="_blank" style="font-size: 18px; font-family: Helvetica, Arial, sans-serif; color: ${buttonTextColor}; text-decoration: none; border-radius: 5px; padding: 10px 20px; border: 1px solid ${buttonBorderColor}; display: inline-block; font-weight: bold;">Sign in</a></td>
|
||||
<td align="center" style="border-radius: 5px;" bgcolor="${color.buttonBackground}"><a href="${url}"
|
||||
target="_blank"
|
||||
style="font-size: 18px; font-family: Helvetica, Arial, sans-serif; color: ${color.buttonText}; text-decoration: none; border-radius: 5px; padding: 10px 20px; border: 1px solid ${color.buttonBorder}; display: inline-block; font-weight: bold;">Sign
|
||||
in</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" style="padding: 0px 0px 10px 0px; font-size: 16px; line-height: 22px; font-family: Helvetica, Arial, sans-serif; color: ${textColor};">
|
||||
<td align="center"
|
||||
style="padding: 0px 0px 10px 0px; font-size: 16px; line-height: 22px; font-family: Helvetica, Arial, sans-serif; color: ${color.text};">
|
||||
If you did not request this email you can safely ignore it.
|
||||
</td>
|
||||
</tr>
|
||||
@@ -193,8 +200,8 @@ function html({ url, host, email }: Record<"url" | "host" | "email", string>) {
|
||||
`
|
||||
}
|
||||
|
||||
// Email Text body (fallback for email clients that don't render HTML, e.g. feature phones)
|
||||
function text({ url, host }: Record<"url" | "host", string>) {
|
||||
/** Email Text body (fallback for email clients that don't render HTML, e.g. feature phones) */
|
||||
function text({ url, host }: { url: string; host: string }) {
|
||||
return `Sign in to ${host}\n${url}\n\n`
|
||||
}
|
||||
```
|
||||
|
||||
@@ -5,7 +5,7 @@ title: Overview
|
||||
|
||||
Authentication Providers in **NextAuth.js** are services that can be used to sign in a user.
|
||||
|
||||
There's four ways a user can be signed in:
|
||||
There are four ways a user can be signed in:
|
||||
|
||||
- [Using a built-in OAuth Provider](/configuration/providers/oauth) (e.g Github, Twitter, Google, etc...)
|
||||
- [Using a custom OAuth Provider](/configuration/providers/oauth#using-a-custom-provider)
|
||||
|
||||
@@ -7,7 +7,7 @@ NextAuth.js provides the ability to setup a [custom Credential provider](/config
|
||||
|
||||
You will need an additional dependency, `ldapjs`, which you can install by running
|
||||
|
||||
```bash npm2yarn
|
||||
```bash npm2yarn2pnpm
|
||||
npm install ldapjs
|
||||
```
|
||||
|
||||
|
||||
@@ -62,6 +62,7 @@ You need to add this to every server rendered page you want to protect. Be aware
|
||||
|
||||
```js title="pages/server-side-example.js"
|
||||
import { useSession, unstable_getServerSession } from "next-auth/next"
|
||||
import { authOptions } from "./api/auth/[...nextauth]"
|
||||
|
||||
export default function Page() {
|
||||
const { data: session } = useSession()
|
||||
@@ -120,6 +121,7 @@ You can protect API routes using the `unstable_getServerSession()` method.
|
||||
|
||||
```js title="pages/api/get-session-example.js"
|
||||
import { unstable_getServerSession } from "next-auth/next"
|
||||
import { authOptions } from "./api/auth/[...nextauth]"
|
||||
|
||||
export default async (req, res) => {
|
||||
const session = await unstable_getServerSession(req, res, authOptions)
|
||||
|
||||
@@ -9,7 +9,7 @@ To test an implementation of NextAuth.js, we encourage you to use [Cypress](http
|
||||
|
||||
To get started, install the dependencies:
|
||||
|
||||
```bash npm2yarn
|
||||
```bash npm2yarn2pnpm
|
||||
npm install --save-dev cypress cypress-social-logins @testing-library/cypress
|
||||
```
|
||||
|
||||
|
||||
@@ -155,9 +155,9 @@ module.exports = {
|
||||
showLastUpdateAuthor: true,
|
||||
showLastUpdateTime: true,
|
||||
remarkPlugins: [
|
||||
require("@sapphire/docusaurus-plugin-npm2yarn2pnpm").npm2yarn2pnpm,
|
||||
require("remark-github"),
|
||||
require("mdx-mermaid"),
|
||||
[require("@docusaurus/remark-plugin-npm2yarn"), { sync: true }],
|
||||
],
|
||||
versions: {
|
||||
current: {
|
||||
|
||||
@@ -21,9 +21,9 @@
|
||||
"dependencies": {
|
||||
"@docusaurus/core": "^2.0.0-beta.21",
|
||||
"@docusaurus/preset-classic": "^2.0.0-beta.21",
|
||||
"@docusaurus/remark-plugin-npm2yarn": "^2.0.0-beta.21",
|
||||
"@docusaurus/theme-common": "2.0.0-beta.21",
|
||||
"@mdx-js/react": "1.6.22",
|
||||
"@sapphire/docusaurus-plugin-npm2yarn2pnpm": "^1.1.0",
|
||||
"classnames": "^2.3.1",
|
||||
"mdx-mermaid": "^1.2.2",
|
||||
"mermaid": "^9.0.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "next-auth",
|
||||
"version": "4.8.0",
|
||||
"version": "4.9.0",
|
||||
"description": "Authentication for Next.js",
|
||||
"homepage": "https://next-auth.js.org",
|
||||
"repository": "https://github.com/nextauthjs/next-auth.git",
|
||||
|
||||
@@ -62,6 +62,7 @@ export async function init({
|
||||
colorScheme: "auto",
|
||||
logo: "",
|
||||
brandColor: "",
|
||||
buttonText: "",
|
||||
},
|
||||
// Custom options override defaults
|
||||
...userOptions,
|
||||
|
||||
@@ -10,7 +10,7 @@ export default async function email(
|
||||
identifier: string,
|
||||
options: InternalOptions<"email">
|
||||
) {
|
||||
const { url, adapter, provider, logger, callbackUrl } = options
|
||||
const { url, adapter, provider, logger, callbackUrl, theme } = options
|
||||
|
||||
// Generate token
|
||||
const token =
|
||||
@@ -42,6 +42,7 @@ export default async function email(
|
||||
expires,
|
||||
url: _url,
|
||||
provider,
|
||||
theme,
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error("SEND_VERIFICATION_EMAIL_ERROR", {
|
||||
|
||||
@@ -37,19 +37,10 @@ export default async function signin(params: {
|
||||
* it solves. We treat email addresses as all lower case. If anyone
|
||||
* complains about this we can make strict RFC 2821 compliance an option.
|
||||
*/
|
||||
let email = body?.email?.toLowerCase()
|
||||
const email = body?.email?.toLowerCase()
|
||||
|
||||
if (!email) return { redirect: `${url}/error?error=EmailSignin` }
|
||||
|
||||
email = email
|
||||
.split(",")[0]
|
||||
.trim()
|
||||
.replaceAll("&", "&")
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">")
|
||||
.replaceAll('"', """)
|
||||
.replaceAll("'", "'")
|
||||
|
||||
// Verified in `assertConfig`
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const { getUserByEmail } = adapter!
|
||||
|
||||
@@ -217,6 +217,7 @@ export interface Theme {
|
||||
colorScheme: "auto" | "dark" | "light"
|
||||
logo?: string
|
||||
brandColor?: string
|
||||
buttonText?: string
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -83,20 +83,28 @@ function NextAuth(
|
||||
|
||||
export default NextAuth
|
||||
|
||||
let experimentalWarningShown = false
|
||||
export async function unstable_getServerSession(
|
||||
...args:
|
||||
| [GetServerSidePropsContext['req'], GetServerSidePropsContext['res'], NextAuthOptions]
|
||||
| [
|
||||
GetServerSidePropsContext["req"],
|
||||
GetServerSidePropsContext["res"],
|
||||
NextAuthOptions
|
||||
]
|
||||
| [NextApiRequest, NextApiResponse, NextAuthOptions]
|
||||
): Promise<Session | null> {
|
||||
console.warn(
|
||||
"[next-auth][warn][EXPERIMENTAL_API]",
|
||||
"\n`unstable_getServerSession` is experimental and may be removed or changed in the future, as the name suggested.",
|
||||
`\nhttps://next-auth.js.org/configuration/nextjs#unstable_getServerSession}`,
|
||||
`\nhttps://next-auth.js.org/warnings#EXPERIMENTAL_API`
|
||||
if (!experimentalWarningShown && process.env.NODE_ENV !== "production") {
|
||||
console.warn(
|
||||
"[next-auth][warn][EXPERIMENTAL_API]",
|
||||
"\n`unstable_getServerSession` is experimental and may be removed or changed in the future, as the name suggested.",
|
||||
`\nhttps://next-auth.js.org/configuration/nextjs#unstable_getServerSession}`,
|
||||
`\nhttps://next-auth.js.org/warnings#EXPERIMENTAL_API`
|
||||
)
|
||||
experimentalWarningShown = true
|
||||
}
|
||||
|
||||
const [req, res, options] = args
|
||||
|
||||
const [req, res, options] = args;
|
||||
|
||||
options.secret = options.secret ?? process.env.NEXTAUTH_SECRET
|
||||
|
||||
const session = await NextAuthHandler<Session | {}>({
|
||||
|
||||
@@ -3,6 +3,16 @@ import { createTransport } from "nodemailer"
|
||||
import type { CommonProviderOptions } from "."
|
||||
import type { Options as SMTPConnectionOptions } from "nodemailer/lib/smtp-connection"
|
||||
import type { Awaitable } from ".."
|
||||
import type { Theme } from "../core/types"
|
||||
|
||||
export interface SendVerificationRequestParams {
|
||||
identifier: string
|
||||
url: string
|
||||
expires: Date
|
||||
provider: EmailConfig
|
||||
token: string
|
||||
theme: Theme
|
||||
}
|
||||
|
||||
export interface EmailConfig extends CommonProviderOptions {
|
||||
type: "email"
|
||||
@@ -16,13 +26,10 @@ export interface EmailConfig extends CommonProviderOptions {
|
||||
* @default 86400
|
||||
*/
|
||||
maxAge?: number
|
||||
sendVerificationRequest: (params: {
|
||||
identifier: string
|
||||
url: string
|
||||
expires: Date
|
||||
provider: EmailConfig
|
||||
token: string
|
||||
}) => Awaitable<void>
|
||||
/** [Documentation](https://next-auth.js.org/providers/email#customizing-emails) */
|
||||
sendVerificationRequest: (
|
||||
params: SendVerificationRequestParams
|
||||
) => Awaitable<void>
|
||||
/**
|
||||
* By default, we are generating a random verification token.
|
||||
* You can make it predictable or modify it as you like with this method.
|
||||
@@ -56,78 +63,81 @@ export default function Email(options: EmailUserConfig): EmailConfig {
|
||||
type: "email",
|
||||
name: "Email",
|
||||
// Server can be an SMTP connection string or a nodemailer config object
|
||||
server: {
|
||||
host: "localhost",
|
||||
port: 25,
|
||||
auth: {
|
||||
user: "",
|
||||
pass: "",
|
||||
},
|
||||
},
|
||||
server: { host: "localhost", port: 25, auth: { user: "", pass: "" } },
|
||||
from: "NextAuth <no-reply@example.com>",
|
||||
maxAge: 24 * 60 * 60,
|
||||
async sendVerificationRequest({
|
||||
identifier: email,
|
||||
url,
|
||||
provider: { server, from },
|
||||
}) {
|
||||
async sendVerificationRequest(params) {
|
||||
const { identifier, url, provider, theme } = params
|
||||
const { host } = new URL(url)
|
||||
const transport = createTransport(server)
|
||||
await transport.sendMail({
|
||||
to: email,
|
||||
from,
|
||||
const transport = createTransport(provider.server)
|
||||
const result = await transport.sendMail({
|
||||
to: identifier,
|
||||
from: provider.from,
|
||||
subject: `Sign in to ${host}`,
|
||||
text: text({ url, host }),
|
||||
html: html({ url, host, email }),
|
||||
html: html({ url, host, theme }),
|
||||
})
|
||||
const failed = result.rejected.concat(result.pending).filter(Boolean)
|
||||
if (failed.length) {
|
||||
throw new Error(`Email(s) (${failed.join(", ")}) could not be sent`)
|
||||
}
|
||||
},
|
||||
options,
|
||||
}
|
||||
}
|
||||
|
||||
// Email HTML body
|
||||
function html({ url, host, email }: Record<"url" | "host" | "email", string>) {
|
||||
// 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 escapedHost = `${host.replace(/\./g, "​.")}`
|
||||
/**
|
||||
* Email HTML body
|
||||
* Insert invisible space into domains 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 it to sign in.
|
||||
*
|
||||
* @note We don't add the email address to avoid needing to escape it, if you do, remember to sanitize it!
|
||||
*/
|
||||
function html(params: { url: string; host: string; theme: Theme }) {
|
||||
const { url, host, theme } = params
|
||||
|
||||
// Some simple styling options
|
||||
const backgroundColor = "#f9f9f9"
|
||||
const textColor = "#444444"
|
||||
const mainBackgroundColor = "#ffffff"
|
||||
const buttonBackgroundColor = "#346df1"
|
||||
const buttonBorderColor = "#346df1"
|
||||
const buttonTextColor = "#ffffff"
|
||||
const escapedHost = host.replace(/\./g, "​.")
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
||||
const brandColor = theme.brandColor || "#346df1"
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
||||
const buttonText = theme.buttonText || "#fff"
|
||||
|
||||
const color = {
|
||||
background: "#f9f9f9",
|
||||
text: "#444",
|
||||
mainBackground: "#fff",
|
||||
buttonBackground: brandColor,
|
||||
buttonBorder: brandColor,
|
||||
buttonText,
|
||||
}
|
||||
|
||||
return `
|
||||
<body style="background: ${backgroundColor};">
|
||||
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
||||
<body style="background: ${color.background};">
|
||||
<table width="100%" border="0" cellspacing="20" cellpadding="0"
|
||||
style="background: ${color.mainBackground}; max-width: 600px; margin: auto; border-radius: 10px;">
|
||||
<tr>
|
||||
<td align="center" style="padding: 10px 0px 20px 0px; font-size: 22px; font-family: Helvetica, Arial, sans-serif; color: ${textColor};">
|
||||
<strong>${escapedHost}</strong>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table width="100%" border="0" cellspacing="20" cellpadding="0" style="background: ${mainBackgroundColor}; max-width: 600px; margin: auto; border-radius: 10px;">
|
||||
<tr>
|
||||
<td align="center" style="padding: 10px 0px 0px 0px; font-size: 18px; font-family: Helvetica, Arial, sans-serif; color: ${textColor};">
|
||||
Sign in as <strong>${escapedEmail}</strong>
|
||||
<td align="center"
|
||||
style="padding: 10px 0px; font-size: 22px; font-family: Helvetica, Arial, sans-serif; color: ${color.text};">
|
||||
Sign in to <strong>${escapedHost}</strong>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" style="padding: 20px 0;">
|
||||
<table border="0" cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td align="center" style="border-radius: 5px;" bgcolor="${buttonBackgroundColor}"><a href="${url}" target="_blank" style="font-size: 18px; font-family: Helvetica, Arial, sans-serif; color: ${buttonTextColor}; text-decoration: none; border-radius: 5px; padding: 10px 20px; border: 1px solid ${buttonBorderColor}; display: inline-block; font-weight: bold;">Sign in</a></td>
|
||||
<td align="center" style="border-radius: 5px;" bgcolor="${color.buttonBackground}"><a href="${url}"
|
||||
target="_blank"
|
||||
style="font-size: 18px; font-family: Helvetica, Arial, sans-serif; color: ${color.buttonText}; text-decoration: none; border-radius: 5px; padding: 10px 20px; border: 1px solid ${color.buttonBorder}; display: inline-block; font-weight: bold;">Sign
|
||||
in</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" style="padding: 0px 0px 10px 0px; font-size: 16px; line-height: 22px; font-family: Helvetica, Arial, sans-serif; color: ${textColor};">
|
||||
<td align="center"
|
||||
style="padding: 0px 0px 10px 0px; font-size: 16px; line-height: 22px; font-family: Helvetica, Arial, sans-serif; color: ${color.text};">
|
||||
If you did not request this email you can safely ignore it.
|
||||
</td>
|
||||
</tr>
|
||||
@@ -136,7 +146,7 @@ function html({ url, host, email }: Record<"url" | "host" | "email", string>) {
|
||||
`
|
||||
}
|
||||
|
||||
// Email Text body (fallback for email clients that don't render HTML, e.g. feature phones)
|
||||
function text({ url, host }: Record<"url" | "host", string>) {
|
||||
/** Email Text body (fallback for email clients that don't render HTML, e.g. feature phones) */
|
||||
function text({ url, host }: { url: string; host: string }) {
|
||||
return `Sign in to ${host}\n${url}\n\n`
|
||||
}
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
import { createCSRF, handler } from "./lib"
|
||||
import EmailProvider from "../src/providers/email"
|
||||
|
||||
const originalEmail = "balazs@email.com"
|
||||
|
||||
test.each([
|
||||
[originalEmail, `,<a href="example.com">Click here!</a>`],
|
||||
[originalEmail, ""],
|
||||
])("Sanitize email", async (emailOriginal, emailCompromised) => {
|
||||
const sendEmail = jest.fn()
|
||||
|
||||
const { secret, csrf } = createCSRF()
|
||||
|
||||
const email = {
|
||||
original: emailOriginal,
|
||||
compromised: `${emailOriginal}${emailCompromised}`,
|
||||
}
|
||||
|
||||
const { res } = await handler(
|
||||
{
|
||||
providers: [EmailProvider({ sendVerificationRequest: sendEmail })],
|
||||
adapter: {
|
||||
getUserByEmail: (email) => ({ id: "1", email, emailVerified: null }),
|
||||
createVerificationToken: (token) => token,
|
||||
} as any,
|
||||
secret,
|
||||
},
|
||||
{
|
||||
prod: true,
|
||||
path: "signin/email",
|
||||
requestInit: {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
email: email.compromised,
|
||||
csrfToken: csrf.value,
|
||||
}),
|
||||
headers: { "Content-Type": "application/json", Cookie: csrf.cookie },
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
if (!emailCompromised) {
|
||||
expect(res.redirect).toBe(
|
||||
"http://localhost:3000/api/auth/verify-request?provider=email&type=email"
|
||||
)
|
||||
expect(sendEmail).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
identifier: email.original,
|
||||
token: expect.any(String),
|
||||
})
|
||||
)
|
||||
} else {
|
||||
expect(res.redirect).not.toContain("error=EmailSignin")
|
||||
|
||||
const emailTo = sendEmail.mock.calls[0][0].identifier
|
||||
expect(emailTo).not.toBe(email.compromised)
|
||||
expect(emailTo).toBe(email.original)
|
||||
}
|
||||
})
|
||||
@@ -50,4 +50,19 @@ describe("Treat secret correctly", () => {
|
||||
expect(logger.error).toBeCalledTimes(1)
|
||||
expect(logger.error).toBeCalledWith("NO_SECRET", expect.any(MissingSecret))
|
||||
})
|
||||
|
||||
it("Only logs warning once and in development", async () => {
|
||||
// Expect console.warn to NOT be called due to NODE_ENV=production
|
||||
await unstable_getServerSession(req, res, { providers: [], logger })
|
||||
expect(console.warn).toBeCalledTimes(0)
|
||||
|
||||
// Expect console.warn to be called ONCE due to NODE_ENV=development
|
||||
process.env.NODE_ENV = "development"
|
||||
await unstable_getServerSession(req, res, { providers: [], logger })
|
||||
expect(console.warn).toBeCalledTimes(1)
|
||||
|
||||
// Expect console.warn to be still only be called ONCE
|
||||
await unstable_getServerSession(req, res, { providers: [], logger })
|
||||
expect(console.warn).toBeCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
21
pnpm-lock.yaml
generated
21
pnpm-lock.yaml
generated
@@ -88,9 +88,9 @@ importers:
|
||||
'@docusaurus/core': ^2.0.0-beta.21
|
||||
'@docusaurus/module-type-aliases': 2.0.0-beta.20
|
||||
'@docusaurus/preset-classic': ^2.0.0-beta.21
|
||||
'@docusaurus/remark-plugin-npm2yarn': ^2.0.0-beta.21
|
||||
'@docusaurus/theme-common': 2.0.0-beta.21
|
||||
'@mdx-js/react': 1.6.22
|
||||
'@sapphire/docusaurus-plugin-npm2yarn2pnpm': ^1.1.0
|
||||
classnames: ^2.3.1
|
||||
mdx-mermaid: ^1.2.2
|
||||
mermaid: ^9.0.1
|
||||
@@ -104,9 +104,9 @@ importers:
|
||||
dependencies:
|
||||
'@docusaurus/core': 2.0.0-beta.21_biqbaboplfbrettd7655fr4n2y
|
||||
'@docusaurus/preset-classic': 2.0.0-beta.21_biqbaboplfbrettd7655fr4n2y
|
||||
'@docusaurus/remark-plugin-npm2yarn': 2.0.0-beta.21
|
||||
'@docusaurus/theme-common': 2.0.0-beta.21_biqbaboplfbrettd7655fr4n2y
|
||||
'@mdx-js/react': 1.6.22_react@18.2.0
|
||||
'@sapphire/docusaurus-plugin-npm2yarn2pnpm': 1.1.0
|
||||
classnames: 2.3.1
|
||||
mdx-mermaid: 1.2.3_mermaid@9.1.2+react@18.2.0
|
||||
mermaid: 9.1.2
|
||||
@@ -4155,15 +4155,6 @@ packages:
|
||||
react: 18.2.0
|
||||
dev: false
|
||||
|
||||
/@docusaurus/remark-plugin-npm2yarn/2.0.0-beta.21:
|
||||
resolution: {integrity: sha512-CqvmoFEj05NzaQBKxnsfI90aM8KHJZWyCzED/Qg5odUD9VtR9zNQJ1Nu/X1ctqCN7FBIxBYk2tz1Xb1+zCP8gg==}
|
||||
engines: {node: '>=16.14'}
|
||||
dependencies:
|
||||
npm-to-yarn: 1.0.1
|
||||
tslib: 2.4.0
|
||||
unist-util-visit: 2.0.3
|
||||
dev: false
|
||||
|
||||
/@docusaurus/theme-classic/2.0.0-beta.21_biqbaboplfbrettd7655fr4n2y:
|
||||
resolution: {integrity: sha512-Ge0WNdTefD0VDQfaIMRRWa8tWMG9+8/OlBRd5MK88/TZfqdBq7b/gnCSaalQlvZwwkj6notkKhHx72+MKwWUJA==}
|
||||
engines: {node: '>=16.14'}
|
||||
@@ -5807,6 +5798,14 @@ packages:
|
||||
resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==}
|
||||
dev: true
|
||||
|
||||
/@sapphire/docusaurus-plugin-npm2yarn2pnpm/1.1.0:
|
||||
resolution: {integrity: sha512-AQGsbaxxJEFGWbLfzoXndT5waPNSQ708qvOz31aKwNilsftV22aBW1NMizxjLCQBooRr4dEWWBLxTGl9y4vKLg==}
|
||||
engines: {node: '>=v16.6.0', npm: '>=7.0.0'}
|
||||
dependencies:
|
||||
npm-to-yarn: 1.0.1
|
||||
unist-util-visit: 2.0.3
|
||||
dev: false
|
||||
|
||||
/@shelf/jest-dynamodb/2.2.4_qsruu6yolbxs4rh6ixjhkibvwu:
|
||||
resolution: {integrity: sha512-OAnkP5sPcIoqL+q/tpp54psuK1gssm+nZLOHRy0S1eyAZGmuqiYAUzyAvmH5AhyqvDPSEHFkIkfbqlp1+KpHgw==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
Reference in New Issue
Block a user