Skip to content

Commit

Permalink
fix(providers): add normalizeIdentifier to EmailProvider
Browse files Browse the repository at this point in the history
* fix(providers): add `normalizeIdentifier` to EmailProvider

* docs: document `normalizeIdentifier`

* fix: allow throwing error from normalizer

* test: add e-mail tests

* chore: log provider id

* test: merge client+config jest configs and add coverage report

* test: show coverage for untested files

* fix: only allow first domain in email. Add tests

* chore: add `coverage` to tsconfig exclude list

* cleanup

* revert

Co-authored-by: Thang Vu <thvu@hey.com>
  • Loading branch information
balazsorban44 and ThangHuuVu committed Aug 1, 2022
1 parent a21db89 commit afb1fcd
Show file tree
Hide file tree
Showing 12 changed files with 301 additions and 66 deletions.
28 changes: 28 additions & 0 deletions docs/docs/providers/email.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,3 +223,31 @@ providers: [
})
],
```

## Normalizing the email address

By default, NextAuth.js will normalize the email address. It treats values as case-insensitive (which is technically not compliant to the [RFC 2821 spec](https://datatracker.ietf.org/doc/html/rfc2821), but in practice this causes more problems than it solves, eg. when looking up users by e-mail from databases.) and also removes any secondary email address that was passed in as a comma-separated list. You can apply your own normalization via the `normalizeIdentifier` method on the `EmailProvider`. The following example shows the default behavior:
```ts
EmailProvider({
// ...
normalizeIdentifier(identifier: string): string {
// Get the first two elements only,
// separated by `@` from user input.
let [local, domain] = identifier.toLowerCase().trim().split("@")
// The part before "@" can contain a ","
// but we remove it on the domain part
domain = domain.split(",")[0]
return `${local}@${domain}`

// You can also throw an error, which will redirect the user
// to the error page with error=EmailSignin in the URL
// if (identifier.split("@").length > 2) {
// throw new Error("Only one email allowed")
// }
},
})
```

:::warning
Always make sure this returns a single e-mail address, even if multiple ones were passed in.
:::
16 changes: 0 additions & 16 deletions packages/next-auth/config/jest.client.config.js

This file was deleted.

34 changes: 34 additions & 0 deletions packages/next-auth/config/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/** @type {import('jest').Config} */
module.exports = {
projects: [
{
displayName: "core",
testMatch: ["**/*.test.ts"],
rootDir: ".",
setupFilesAfterEnv: ["./config/jest-setup.js"],
transform: {
"\\.(js|jsx|ts|tsx)$": ["@swc/jest", require("./swc.config")],
},
coveragePathIgnorePatterns: ["tests"],
},
{
displayName: "client",
testMatch: ["**/*.test.js"],
setupFilesAfterEnv: ["./config/jest-setup.js"],
rootDir: ".",
transform: {
"\\.(js|jsx|ts|tsx)$": ["@swc/jest", require("./swc.config")],
},
testEnvironment: "jsdom",
coveragePathIgnorePatterns: ["__tests__"],
},
],
watchPlugins: [
"jest-watch-typeahead/filename",
"jest-watch-typeahead/testname",
],
collectCoverage: true,
coverageDirectory: "../coverage",
coverageReporters: ["html", "text-summary"],
collectCoverageFrom: ["src/**/*.(js|jsx|ts|tsx)"],
}
13 changes: 0 additions & 13 deletions packages/next-auth/config/jest.core.config.js

This file was deleted.

4 changes: 1 addition & 3 deletions packages/next-auth/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,7 @@
"build:js": "pnpm clean && pnpm generate-providers && tsc && babel --config-file ./config/babel.config.js src --out-dir . --extensions \".tsx,.ts,.js,.jsx\"",
"build:css": "postcss --config config/postcss.config.js src/**/*.css --base src --dir . && node config/wrap-css.js",
"watch:css": "postcss --config config/postcss.config.js --watch src/**/*.css --base src --dir .",
"test:client": "jest --config ./config/jest.client.config.js",
"test:core": "jest --config ./config/jest.core.config.js",
"test": "pnpm test:core && pnpm test:client",
"test": "jest --config ./config/jest.config.js",
"prepublishOnly": "pnpm build",
"generate-providers": "node ./config/generate-providers.js",
"setup": "pnpm generate-providers",
Expand Down
35 changes: 18 additions & 17 deletions packages/next-auth/src/core/lib/email/signin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,27 +21,28 @@ export default async function email(
Date.now() + (provider.maxAge ?? ONE_DAY_IN_SECONDS) * 1000
)

// Save in database
// @ts-expect-error
await adapter.createVerificationToken({
identifier,
token: hashToken(token, options),
expires,
})

// Generate a link with email, unhashed token and callback url
const params = new URLSearchParams({ callbackUrl, token, email: identifier })
const _url = `${url}/callback/${provider.id}?${params}`

// Send to user
await provider.sendVerificationRequest({
identifier,
token,
expires,
url: _url,
provider,
theme,
})
await Promise.all([
// Send to user
provider.sendVerificationRequest({
identifier,
token,
expires,
url: _url,
provider,
theme,
}),
// Save in database
// @ts-expect-error // verified in `assertConfig`
adapter.createVerificationToken({
identifier,
token: hashToken(token, options),
expires,
}),
])

return `${url}/verify-request?${new URLSearchParams({
provider: provider.id,
Expand Down
33 changes: 20 additions & 13 deletions packages/next-auth/src/core/routes/signin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,16 +33,26 @@ export default async function signin(params: {
return { redirect: `${url}/error?error=OAuthSignin` }
}
} else if (provider.type === "email") {
/**
* @note Technically the part of the email address local mailbox element
* (everything before the @ symbol) should be treated as 'case sensitive'
* according to RFC 2821, but in practice this causes more problems than
* it solves. We treat email addresses as all lower case. If anyone
* complains about this we can make strict RFC 2821 compliance an option.
*/
const email = body?.email?.toLowerCase()

let email: string = body?.email
if (!email) return { redirect: `${url}/error?error=EmailSignin` }
const normalizer: (identifier: string) => string =
provider.normalizeIdentifier ??
((identifier) => {
// Get the first two elements only,
// separated by `@` from user input.
let [local, domain] = identifier.toLowerCase().trim().split("@")
// The part before "@" can contain a ","
// but we remove it on the domain part
domain = domain.split(",")[0]
return `${local}@${domain}`
})

try {
email = normalizer(body?.email)
} catch (error) {
logger.error("SIGNIN_EMAIL_ERROR", { error, providerId: provider.id })
return { redirect: `${url}/error?error=EmailSignin` }
}

// Verified in `assertConfig`
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
Expand Down Expand Up @@ -85,10 +95,7 @@ export default async function signin(params: {
const redirect = await emailSignin(email, options)
return { redirect }
} catch (error) {
logger.error("SIGNIN_EMAIL_ERROR", {
error: error as Error,
providerId: provider.id,
})
logger.error("SIGNIN_EMAIL_ERROR", { error, providerId: provider.id })
return { redirect: `${url}/error?error=EmailSignin` }
}
}
Expand Down
17 changes: 16 additions & 1 deletion packages/next-auth/src/providers/email.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,21 @@ export interface EmailConfig extends CommonProviderOptions {
generateVerificationToken?: () => Awaitable<string>
/** If defined, it is used to hash the verification token when saving to the database . */
secret?: string
/**
* Normalizes the user input before sending the verification request.
*
* ⚠️ Always make sure this method returns a single email address.
*
* @note Technically, the part of the email address local mailbox element
* (everything before the `@` symbol) should be treated as 'case sensitive'
* according to RFC 2821, but in practice this causes more problems than
* it solves, e.g.: when looking up users by e-mail from databases.
* By default, we treat email addresses as all lower case,
* but you can override this function to change this behavior.
*
* [Documentation](https://next-auth.js.org/providers/email#normalizing-the-e-mail-address) | [RFC 2821](https://tools.ietf.org/html/rfc2821) | [Email syntax](https://en.wikipedia.org/wiki/Email_address#Syntax)
*/
normalizeIdentifier?: (identifier: string) => string
options: EmailUserConfig
}

Expand Down Expand Up @@ -79,7 +94,7 @@ export default function Email(options: EmailUserConfig): EmailConfig {
})
const failed = result.rejected.concat(result.pending).filter(Boolean)
if (failed.length) {
throw new Error(`Email(s) (${failed.join(", ")}) could not be sent`)
throw new Error(`Email (${failed.join(", ")}) could not be sent`)
}
},
options,
Expand Down
Loading

1 comment on commit afb1fcd

@vercel
Copy link

@vercel vercel bot commented on afb1fcd Aug 1, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.