diff --git a/apps/dev/pages/api/auth/[...nextauth].ts b/apps/dev/pages/api/auth/[...nextauth].ts
index 041ac8450c..45c5f8f8dc 100644
--- a/apps/dev/pages/api/auth/[...nextauth].ts
+++ b/apps/dev/pages/api/auth/[...nextauth].ts
@@ -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`
diff --git a/docs/docs/configuration/options.md b/docs/docs/configuration/options.md
index 02b0c2ea05..870472e0f6 100644
--- a/docs/docs/configuration/options.md
+++ b/docs/docs/configuration/options.md
@@ -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
}
```
diff --git a/docs/docs/providers/email.md b/docs/docs/providers/email.md
index 0551118ab4..6ddefe655f 100644
--- a/docs/docs/providers/email.md
+++ b/docs/docs/providers/email.md
@@ -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, ".")}`
-
- // Some simple styling options
- const backgroundColor = "#f9f9f9"
- const textColor = "#444444"
- const mainBackgroundColor = "#ffffff"
- const buttonBackgroundColor = "#346df1"
- const buttonBorderColor = "#346df1"
- const buttonTextColor = "#ffffff"
+/**
+ * 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
+
+ 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 `
-
-
+
+
-
-
-
- Sign in as ${escapedEmail}
+ |
+ Sign in to ${escapedHost}
|
|
-
+ |
If you did not request this email you can safely ignore it.
|
@@ -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`
}
```
diff --git a/packages/next-auth/src/core/init.ts b/packages/next-auth/src/core/init.ts
index 5d2e489529..38a38acc53 100644
--- a/packages/next-auth/src/core/init.ts
+++ b/packages/next-auth/src/core/init.ts
@@ -62,6 +62,7 @@ export async function init({
colorScheme: "auto",
logo: "",
brandColor: "",
+ buttonText: "",
},
// Custom options override defaults
...userOptions,
diff --git a/packages/next-auth/src/core/lib/email/signin.ts b/packages/next-auth/src/core/lib/email/signin.ts
index ae4fa831ed..cd58f9262e 100644
--- a/packages/next-auth/src/core/lib/email/signin.ts
+++ b/packages/next-auth/src/core/lib/email/signin.ts
@@ -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", {
diff --git a/packages/next-auth/src/core/routes/signin.ts b/packages/next-auth/src/core/routes/signin.ts
index 0bd48731f7..cd1547015e 100644
--- a/packages/next-auth/src/core/routes/signin.ts
+++ b/packages/next-auth/src/core/routes/signin.ts
@@ -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!
diff --git a/packages/next-auth/src/core/types.ts b/packages/next-auth/src/core/types.ts
index 5f7590b45f..a66800a73b 100644
--- a/packages/next-auth/src/core/types.ts
+++ b/packages/next-auth/src/core/types.ts
@@ -217,6 +217,7 @@ export interface Theme {
colorScheme: "auto" | "dark" | "light"
logo?: string
brandColor?: string
+ buttonText?: string
}
/**
diff --git a/packages/next-auth/src/providers/email.ts b/packages/next-auth/src/providers/email.ts
index 185fcb67f0..89aca9d5ab 100644
--- a/packages/next-auth/src/providers/email.ts
+++ b/packages/next-auth/src/providers/email.ts
@@ -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
+ /** [Documentation](https://next-auth.js.org/providers/email#customizing-emails) */
+ sendVerificationRequest: (
+ params: SendVerificationRequestParams
+ ) => Awaitable
/**
* By default, we are generating a random verification token.
* You can make it predictable or modify it as you like with this method.
@@ -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 ",
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
+
+ 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"
- // Some simple styling options
- const backgroundColor = "#f9f9f9"
- const textColor = "#444444"
- const mainBackgroundColor = "#ffffff"
- const buttonBackgroundColor = "#346df1"
- const buttonBorderColor = "#346df1"
- const buttonTextColor = "#ffffff"
+ const color = {
+ background: "#f9f9f9",
+ text: "#444",
+ mainBackground: "#fff",
+ buttonBackground: brandColor,
+ buttonBorder: brandColor,
+ buttonText,
+ }
return `
-
-
-
-
- ${escapedHost}
- |
-
-
-
+
+
-
- Sign in as ${escapedEmail}
+ |
+ Sign in to ${escapedHost}
|
|
-
+ |
If you did not request this email you can safely ignore it.
|
@@ -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`
}
diff --git a/packages/next-auth/tests/email.test.ts b/packages/next-auth/tests/email.test.ts
deleted file mode 100644
index 43c3973fb5..0000000000
--- a/packages/next-auth/tests/email.test.ts
+++ /dev/null
@@ -1,59 +0,0 @@
-import { createCSRF, handler } from "./lib"
-import EmailProvider from "../src/providers/email"
-
-const originalEmail = "balazs@email.com"
-
-test.each([
- [originalEmail, `,Click here!`],
- [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)
- }
-})