Compare commits

..

12 Commits

Author SHA1 Message Date
Iain Collins
a3479b3503 Bump version to 2.2.0 2020-06-25 22:50:50 +01:00
Iain Collins
740535a8f2 Add support for mongodb+srv:// URLs 2020-06-25 22:49:56 +01:00
Iain Collins
19ed684a52 Add HTTP status codes to error pages 2020-06-25 22:47:08 +01:00
Iain Collins
bd72949fa7 Fix typos in docs 2020-06-25 22:44:31 +01:00
Iain Collins
a277cd5b0c Fix linter errors 2020-06-25 22:38:07 +01:00
Iain Collins
fd6e7e94df Update version to 2.2.0-beta.0
*  New email template
*  New callback error handling

I anticipate adding more changes and a new beta before we release 2.2.0 but wanted to test these changes.
2020-06-25 18:04:35 +01:00
Iain Collins
2f6403478d Improve apperance of sign in email
* Prevents links from being turned into hyperlinks by email clients
* Improve UI with a primary action button and better font sizing and spacing in the template
* Adds email address to body to clear indicate who they will be signing in as

While not exactly a bug in NextAuth.js it does resolve #331
2020-06-25 17:43:35 +01:00
Iain Collins
a4372ffc61 Handle OAuth sign in cancellations gracefully
Currently if a user hits a cancel button after selecting the option to sign in with an OAuth provider an error is displayed.

This error is only triggered in production.

This update refactors error handling so that in both dev and prod modes, the user is directed back to the sign in page.

Not all OAuth providers have a cancel button on their sign in page (e.g. Twitter does, Google doesn't).

The oAuthCallback has been slightly refactored to make debugging easier. It is still pretty horrible, but i don't want to do major refactoring of it until we have tests we trust in place.
2020-06-25 17:37:17 +01:00
Iain Collins
d6ce92811e Update documentation for credentials provider
There was a typo in the documentation and some of the documentation was outdated.
2020-06-25 02:35:43 +01:00
Sreetam Das
e5aecdf315 fix link in client API docs (#323) 2020-06-24 12:33:27 +02:00
Iain Collins
6d1c457a75 Add action to run npm test on new pull requests 2020-06-24 02:23:48 +01:00
Iain Collins
6e16aec6d3 Update test to run linter
The action to publish to NPM fails as it can't run the DB test yet so removing that.

Changing the test to run the linter instead so it does something (e.g. catch the worst syntax errors).
2020-06-24 02:13:02 +01:00
13 changed files with 316 additions and 182 deletions

29
.github/workflows/node.js.yml vendored Normal file
View File

@@ -0,0 +1,29 @@
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
name: Node.js CI
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [10.x, 12.x, 14.x]
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
- run: npm run build --if-present
- run: npm test

View File

@@ -1,6 +1,6 @@
{
"name": "next-auth",
"version": "2.1.0",
"version": "2.2.0",
"description": "An authentication library for Next.js",
"repository": "https://github.com/iaincollins/next-auth.git",
"author": "Iain Collins <me@iaincollins.com>",
@@ -12,7 +12,7 @@
"watch": "npm run watch:js | npm run watch:css",
"watch:js": "babel --watch src --out-dir dist",
"watch:css": "postcss --watch src/**/*.css --base src --dir dist",
"test": "npm run test:db",
"test": "npm run lint",
"test:db": "npm run test:db:mysql && npm run test:db:postgres && npm run test:db:mongodb",
"test:db:mysql": "node test/mysql.js",
"test:db:postgres": "node test/postgres.js",

View File

@@ -7,19 +7,30 @@ const parseConnectionString = (configString) => {
// to make configuration easier (in most use cases).
//
// TypeORM accepts connection string as a 'url' option, but unfortunately
// not for all databases (e.g. SQLite) or options, so we handle parsing it
// in this function..
// not for all databases (e.g. SQLite) or for all options, so we handle
// parsing it in this function.
try {
const parsedUrl = new URL(configString)
const config = {}
// Remove : and convert strings like 'mongodb+srv' into 'mongodb'
config.type = parsedUrl.protocol.replace(/:$/, '').replace(/\+(.*)?$/, '')
config.host = parsedUrl.hostname
config.port = Number(parsedUrl.port)
config.username = parsedUrl.username
config.password = parsedUrl.password
config.database = parsedUrl.pathname.replace(/^\//, '')
if (parsedUrl.protocol.startsWith('mongodb+srv')) {
// Special case handling is required for mongodb+srv with TypeORM
config.type = 'mongodb'
config.url = configString.replace(/\?(.*)$/, '')
config.useNewUrlParser = true
} else {
config.type = parsedUrl.protocol.replace(/:$/, '')
config.host = parsedUrl.hostname
config.port = Number(parsedUrl.port)
config.username = parsedUrl.username
config.password = parsedUrl.password
config.database = parsedUrl.pathname.replace(/^\//, '').replace(/\?(.*)$/, '')
}
// This option is recommended by mongodb
if (config.type === 'mongodb') {
config.useUnifiedTopology = true
}
if (parsedUrl.search) {
parsedUrl.search.replace(/^\?/, '').split('&').forEach(keyValuePair => {

View File

@@ -22,22 +22,22 @@ export default (options) => {
}
}
const sendVerificationRequest = ({ identifier: emailAddress, url, token, site, provider }) => {
const sendVerificationRequest = ({ identifier: email, url, token, site, provider }) => {
return new Promise((resolve, reject) => {
const { server, from } = provider
const siteName = site.replace(/^https?:\/\//, '')
site = site.replace(/^https?:\/\//, '') // Strip protocol from site
nodemailer
.createTransport(server)
.sendMail({
to: emailAddress,
to: email,
from,
subject: `Sign in to ${siteName}`,
text: text({ url, siteName }),
html: html({ url, siteName })
subject: `Sign in to ${site}`,
text: text({ url, site, email }),
html: html({ url, site, email })
}, (error) => {
if (error) {
logger.error('SEND_VERIFICATION_EMAIL_ERROR', emailAddress, error)
logger.error('SEND_VERIFICATION_EMAIL_ERROR', email, error)
return reject(new Error('SEND_VERIFICATION_EMAIL_ERROR', error))
}
return resolve()
@@ -46,28 +46,55 @@ const sendVerificationRequest = ({ identifier: emailAddress, url, token, site, p
}
// Email HTML body
const html = ({ url, siteName }) => {
const buttonBackgroundColor = '#444444'
const html = ({ url, site, 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, '&#8203;.')}`
const escapedSite = `${site.replace(/\./g, '&#8203;.')}`
// Some simple styling options
const backgroundColor = '#f9f9f9'
const textColor = '#444444'
const mainBackgroundColor = '#ffffff'
const buttonBackgroundColor = '#346df1'
const buttonBorderColor = '#346df1'
const buttonTextColor = '#ffffff'
return `
<table width="100%" border="0" cellspacing="0" cellpadding="0">
<tr>
<td align="center" style="padding: 8px 0; font-size: 22px; font-family: Helvetica, Arial, sans-serif; color: #888888;">
${siteName}
</td>
</tr>
<tr>
<td align="center" style="padding: 16px 0;">
<table border="0" cellspacing="0" cellpadding="0">
<tr>
<td align="center" style="border-radius: 3px;" bgcolor="${buttonBackgroundColor}"><a href="${url}" target="_blank" style="font-size: 18px; font-family: Helvetica, Arial, sans-serif; color: ${buttonTextColor}; text-decoration: none; text-decoration: none;border-radius: 3px; padding: 12px 18px; border: 1px solid ${buttonBackgroundColor}; display: inline-block; font-weight: bold;">Sign in</a></td>
</tr>
</table>
</td>
</tr>
</table>
<body style="background: ${backgroundColor};">
<table width="100%" border="0" cellspacing="0" cellpadding="0">
<tr>
<td align="center" style="padding: 10px 0px 20px 0px; font-size: 22px; font-family: Helvetica, Arial, sans-serif; color: ${textColor};">
<strong>${escapedSite}</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>
</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; text-decoration: none;border-radius: 5px; padding: 10px 20px; border: 1px solid ${buttonBorderColor}; 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};">
If you did not request this email you can safely ignore it.
</td>
</tr>
</table>
</body>
`
}
// Email Text body (fallback for email clients that don't render HTML, e.g. feature phones)
const text = ({ url, siteName }) => `Sign in to ${siteName}\n${url}\n\n`
const text = ({ url, site }) => `Sign in to ${site}\n${url}\n\n`

View File

@@ -32,7 +32,7 @@ export default async (req, res, userSuppliedOptions) => {
nextauth,
action = nextauth[0],
provider = nextauth[1],
error
error = nextauth[1]
} = query
const {

View File

@@ -9,6 +9,10 @@ import logger from '../../../lib/logger'
// appropriate credit) to make it easier to maintain and address issues as they
// come up, as the node-oauth package does not seem to be actively maintained.
// @TODO Refactor to use promises and not callbacks
// @TODO Refactor to use jsonwebtoken instead of jwt-decode & remove dependancy
export default async (req, provider, callback) => {
let { oauth_token, oauth_verifier, code } = req.query // eslint-disable-line camelcase
const client = oAuthClient(provider)
@@ -46,7 +50,10 @@ export default async (req, provider, callback) => {
accessToken,
refreshToken,
results.id_token,
(error, profileData) => callback(error, _getProfile(error, profileData, accessToken, refreshToken, provider))
async (error, profileData) => {
const { profile, account, OAuthProfile } = await _getProfile(error, profileData, accessToken, refreshToken, provider)
callback(error, profile, account, OAuthProfile)
}
)
} else {
// Use custom get() method for oAuth2 flows
@@ -55,7 +62,10 @@ export default async (req, provider, callback) => {
client.get(
provider,
accessToken,
(error, profileData) => callback(error, _getProfile(error, profileData, accessToken, refreshToken, provider))
async (error, profileData) => {
const { profile, account, OAuthProfile } = await _getProfile(error, profileData, accessToken, refreshToken, provider)
callback(error, profile, account, OAuthProfile)
}
)
}
}
@@ -76,7 +86,10 @@ export default async (req, provider, callback) => {
provider.profileUrl,
accessToken,
refreshToken,
(error, profileData) => callback(error, _getProfile(error, profileData, accessToken, refreshToken, provider))
async (error, profileData) => {
const { profile, account, OAuthProfile } = await _getProfile(error, profileData, accessToken, refreshToken, provider)
callback(error, profile, account, OAuthProfile)
}
)
}
)
@@ -96,13 +109,23 @@ async function _getProfile (error, profileData, accessToken, refreshToken, provi
profile = await provider.profile(profileData)
} catch (exception) {
// @TODO Handle parsing error
logger.error('OAUTH_PARSE_PROFILE_ERROR', exception)
throw new Error('Failed to get OAuth profile')
// If we didn't get a response either there was a problem with the provider
// response *or* the user cancelled the action with the provider.
//
// Unfortuately, we can't tell which - at least not in a way that works for
// all providers, so we return an empty object; the user should then be
// redirected back to the sign up page. We log the error to help developers
// who might be trying to debug this when configuring a new provider.
logger.error('OAUTH_PARSE_PROFILE_ERROR', exception, profileData)
return {
profile: null,
account: null,
OAuthProfile: profileData
}
}
// Return profile, raw profile and auth provider details
return ({
return {
profile: {
name: profile.name,
email: profile.email ? profile.email.toLowerCase() : null,
@@ -116,8 +139,8 @@ async function _getProfile (error, profileData, accessToken, refreshToken, provi
accessToken,
accessTokenExpires: null
},
oAuthProfile: profileData
})
OAuthProfile: profileData
}
}
// Ported from https://github.com/ciaranj/node-oauth/blob/a7f8a1e21c362eb4ed2039431fb9ac2ae749f26a/lib/oauth2.js

View File

@@ -1,9 +1,10 @@
import { h } from 'preact' // eslint-disable-line no-unused-vars
import render from 'preact-render-to-string'
export default ({ site, error, baseUrl }) => {
export default ({ site, error, baseUrl, res }) => {
const signinPageUrl = `${baseUrl}/signin` // @TODO Make sign in URL configurable
let statusCode = 200
let heading = <h1>Error</h1>
let message = <p><a className='site' href={site}>{site.replace(/^https?:\/\//, '')}</a></p>
@@ -24,6 +25,7 @@ export default ({ site, error, baseUrl }) => {
</div>
break
case 'OAuthAccountNotLinked':
statusCode = 403
heading = <h1>Sign in failed</h1>
message =
<div>
@@ -48,6 +50,7 @@ export default ({ site, error, baseUrl }) => {
</div>
break
case 'CredentialsSignin':
statusCode = 403
heading = <h1>Sign in failed</h1>
message =
<div>
@@ -58,6 +61,7 @@ export default ({ site, error, baseUrl }) => {
</div>
break
case 'Configuration':
statusCode = 500
heading = <h1>Server error</h1>
message =
<div>
@@ -68,6 +72,7 @@ export default ({ site, error, baseUrl }) => {
</div>
break
case 'AccessDenied':
statusCode = 403
heading = <h1>Access Denied</h1>
message =
<div>
@@ -80,6 +85,7 @@ export default ({ site, error, baseUrl }) => {
case 'Verification':
// @TODO Check if user is signed in already with the same email address.
// If they are, no need to display this message, can just direct to callbackUrl
statusCode = 403
heading = <h1>Unable to sign in</h1>
message =
<div>
@@ -93,6 +99,8 @@ export default ({ site, error, baseUrl }) => {
default:
}
res.status(statusCode)
return render(
<div className='error'>
{heading}

View File

@@ -17,7 +17,7 @@ function render (req, res, page, props, done) {
html = verifyRequest(props)
break
case 'error':
html = error(props)
html = error({ ...props, res })
break
default:
html = error(props)

View File

@@ -29,79 +29,97 @@ export default async (req, res, options, done) => {
const sessionToken = req.cookies ? req.cookies[cookies.sessionToken.name] : null
if (type === 'oauth') {
oAuthCallback(req, provider, async (error, oauthAccount) => {
if (error) {
logger.error('CALLBACK_OAUTH_ERROR', error)
res.status(302).setHeader('Location', `${baseUrl}/error?error=oAuthCallback`)
res.end()
return done()
}
try {
const { profile, account, oAuthProfile } = await oauthAccount
try {
oAuthCallback(req, provider, async (error, profile, account, OAuthProfile) => {
try {
if (error) {
logger.error('CALLBACK_OAUTH_ERROR', error)
res.status(302).setHeader('Location', `${baseUrl}/error?error=oAuthCallback`)
res.end()
return done()
}
// Check if user is allowed to sign in
const signinCallbackResponse = await callbacks.signin(profile, account, oAuthProfile)
// Make it easier to debug when adding a new provider
logger.debug('OAUTH_CALLBACK_RESPONSE', { profile, account, OAuthProfile })
if (signinCallbackResponse === false) {
res.status(302).setHeader('Location', `${baseUrl}/error?error=AccessDenied`)
// If we don't have a profile object then either something went wrong
// or the user cancelled signin in. We don't know which, so we just
// direct the user to the signup page for now. We could do something
// else in future.
//
// Note: In oAuthCallback an error is logged with debug info, so it
// should at least be visible to developers what happened if it is an
// error with the provider.
if (!profile) {
res.status(302).setHeader('Location', `${baseUrl}/signin`)
res.end()
return done()
}
// Check if user is allowed to sign in
const signinCallbackResponse = await callbacks.signin(profile, account, OAuthProfile)
if (signinCallbackResponse === false) {
res.status(302).setHeader('Location', `${baseUrl}/error?error=AccessDenied`)
res.end()
return done()
}
// Sign user in
const { user, session, isNewUser } = await callbackHandler(sessionToken, profile, account, options)
if (useJwtSession) {
const defaultJwtPayload = { user, account, isNewUser }
const jwtPayload = await callbacks.jwt(defaultJwtPayload, OAuthProfile)
// Sign and encrypt token
const newEncodedJwt = await jwt.encode({ secret: jwt.secret, token: jwtPayload, maxAge: sessionMaxAge })
// Set cookie expiry date
const cookieExpires = new Date()
cookieExpires.setTime(cookieExpires.getTime() + (sessionMaxAge * 1000))
cookie.set(res, cookies.sessionToken.name, newEncodedJwt, { expires: cookieExpires.toISOString(), ...cookies.sessionToken.options })
} else {
// Save Session Token in cookie
cookie.set(res, cookies.sessionToken.name, session.sessionToken, { expires: session.expires || null, ...cookies.sessionToken.options })
}
await dispatchEvent(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
// Note that the callback URL is preserved, so the journey can still be resumed
if (isNewUser && pages.newUser) {
res.status(302).setHeader('Location', pages.newUser)
res.end()
return done()
}
// Callback URL is already verified at this point, so safe to use if specified
res.status(302).setHeader('Location', callbackUrl || site)
res.end()
return done()
} catch (error) {
if (error.name === 'AccountNotLinkedError') {
// If the email on the account is already linked, but nto with this oAuth account
res.status(302).setHeader('Location', `${baseUrl}/error?error=OAuthAccountNotLinked`)
} else if (error.name === 'CreateUserError') {
res.status(302).setHeader('Location', `${baseUrl}/error?error=OAuthCreateAccount`)
} else {
logger.error('OAUTH_CALLBACK_HANDLER_ERROR', error)
res.status(302).setHeader('Location', `${baseUrl}/error?error=Callback`)
}
res.end()
return done()
}
// Sign user in
const { user, session, isNewUser } = await callbackHandler(sessionToken, profile, account, options)
if (useJwtSession) {
const defaultJwtPayload = { user, account, isNewUser }
const jwtPayload = await callbacks.jwt(defaultJwtPayload, oAuthProfile)
// Sign and encrypt token
const newEncodedJwt = await jwt.encode({ secret: jwt.secret, token: jwtPayload, maxAge: sessionMaxAge })
// Set cookie expiry date
const cookieExpires = new Date()
cookieExpires.setTime(cookieExpires.getTime() + (sessionMaxAge * 1000))
cookie.set(res, cookies.sessionToken.name, newEncodedJwt, { expires: cookieExpires.toISOString(), ...cookies.sessionToken.options })
} else {
// Save Session Token in cookie
cookie.set(res, cookies.sessionToken.name, session.sessionToken, { expires: session.expires || null, ...cookies.sessionToken.options })
}
await dispatchEvent(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
// Note that the callback URL is preserved, so the journey can still be resumed
if (isNewUser && pages.newUser) {
res.status(302).setHeader('Location', pages.newUser)
res.end()
return done()
}
} catch (error) {
if (error.name === 'AccountNotLinkedError') {
// If the email on the account is already linked, but nto with this oAuth account
res.status(302).setHeader('Location', `${baseUrl}/error?error=OAuthAccountNotLinked`)
} else if (error.name === 'CreateUserError') {
res.status(302).setHeader('Location', `${baseUrl}/error?error=OAuthCreateAccount`)
} else {
logger.error('OAUTH_CALLBACK_HANDLER_ERROR', error)
res.status(302).setHeader('Location', `${baseUrl}/error?error=Callback`)
}
res.end()
return done()
}
// Callback URL is already verified at this point, so safe to use if specified
if (callbackUrl) {
res.status(302).setHeader('Location', callbackUrl)
res.end()
} else {
res.status(302).setHeader('Location', site)
res.end()
}
})
} catch (error) {
logger.error('OAUTH_CALLBACK_ERROR', error)
res.status(302).setHeader('Location', `${baseUrl}/error?error=Callback`)
res.end()
return done()
})
}
} else if (type === 'email') {
try {
if (!adapter) {
@@ -156,8 +174,6 @@ export default async (req, res, options, done) => {
cookie.set(res, cookies.sessionToken.name, newEncodedJwt, { expires: cookieExpires.toISOString(), ...cookies.sessionToken.options })
} else {
console.log('debug.cookies', cookies)
console.log('debug.session', session)
// Save Session Token in cookie
cookie.set(res, cookies.sessionToken.name, session.sessionToken, { expires: session.expires || null, ...cookies.sessionToken.options })
}

View File

@@ -60,7 +60,7 @@ NextAuth.js is designed to work with any OAuth service, it supports OAuth 1.0, 1
4. Now you can add the provider settings to the NextAuth options object. You can add as many OAuth providers as you like, as you can see `providers` is an array.
```js title="/pages/api/auth/[...nextauth].js"
```js title="pages/api/auth/[...nextauth].js"
...
providers: [
Providers.Twitter({
@@ -110,7 +110,7 @@ As an example of what this looks like, this is the the provider object returned
```
You can replace all the options in this JSON object with the ones from your custom provider  be sure to give it a unique ID and specify the correct OAuth version - and add it to the providers option:
```js title="/pages/api/auth/[...nextauth].js"
```js title="pages/api/auth/[...nextauth].js"
...
providers: [
Providers.Twitter({
@@ -160,7 +160,7 @@ Adding support for signing in via email in addition to one or more OAuth service
Configuration is similar to other providers, but the options are different:
```js title="/pages/api/auth/[...nextauth].js"
```js title="pages/api/auth/[...nextauth].js"
providers: [
Providers.Email({
server: process.env.EMAIL_SERVER,
@@ -183,7 +183,7 @@ The Credentials provider allows you to handle signing in with arbitrary credenti
It is intended to support use cases where you have an existing system you need to authenticate users against.
```js title="/pages/api/auth/[...nextauth].js"
```js title="pages/api/auth/[...nextauth].js"
import Providers from `next-auth/providers`
...
providers: [

View File

@@ -8,7 +8,7 @@ The NextAuth.js client library makes it easy to interact with sessions from Reac
Some of the methods can be called both client side and server side.
:::note
To use client methods server side in `getServerSideProp()` or `getInitialProps()` you should add the NextAuth.js `<Provider>` in `pages/apps.js`
To use client methods server side in `getServerSideProp()` or `getInitialProps()` you should add the NextAuth.js `<Provider>` in `pages/app.js`
```jsx title="pages/_app.js"
import { Provider } from 'next-auth/client'
@@ -187,7 +187,7 @@ e.g.
* `signin('google', { callbackUrl: 'http://localhost:3000/foo' })`
* `signin('email', { email, callbackUrl: 'http://localhost:3000/foo' })`
The URL must be considered valid by the [redirect callback handler](http://localhost:3000/configuration/callbacks#redirect). By default this means it must be an absolute URL at the same hostname (or else it will default to the homepage); you can define your own custom redirect callback to allow other URLs, including supporting relative URLs.
The URL must be considered valid by the [redirect callback handler](/configuration/callbacks#redirect). By default this means it must be an absolute URL at the same hostname (or else it will default to the homepage); you can define your own custom redirect callback to allow other URLs, including supporting relative URLs.
:::tip
To also support signing in from clients that do not have client side JavaScript, use a regular link, add an onClick handler to it and call **e.preventDefault()** before calling the **signin()** method.
@@ -218,7 +218,7 @@ As with the `signin()` function, you can specify a `callbackUrl` parameter by pa
e.g. `signout{ callbackUrl: 'http://localhost:3000/foo' })`
The URL must be considered valid by the [redirect callback handler](http://localhost:3000/configuration/callbacks#redirect). By default this means it must be an absolute URL at the same hostname (or else it will default to the homepage); you can define your own custom redirect callback to allow other URLs, including supporting relative URLs.
The URL must be considered valid by the [redirect callback handler](/configuration/callbacks#redirect). By default this means it must be an absolute URL at the same hostname (or else it will default to the homepage); you can define your own custom redirect callback to allow other URLs, including supporting relative URLs.
---

View File

@@ -50,17 +50,14 @@ providers: [
password: { label: "Password", type: "password" }
},
authorize: async (credentials) => {
const user = (credentials) => {
// You need to provide your own logic here that takes the credentials
// submitted and returns either a object representing a user or value
// that is false/null if the credentials are invalid.
// e.g. return { id: 1, name: 'J Smith', email: 'jsmith@example.com' }
return null
}
// Add logic here to look up the user from the credentials supplied
const user = { id: 1, name: 'J Smith', email: 'jsmith@example.com' }
if (user) {
// Any user object returned here will be saved in the JSON Web Token
// Any object returned will be saved in `user` property of the JWT
return Promise.resolve(user)
} else {
// If you return null or false then the credentials will be rejected
return Promise.resolve(null)
}
}
@@ -69,24 +66,6 @@ providers: [
...
```
To use your new credentials provider, you will need to create a form that posts back to `/api/auth/callback/credentials`.
All form parameters submitted will be passed as `credentials` to your `authorize` callback.
```js title="pages/signin"
import React from 'react'
export default () => {
return (
<form method='post' action='/api/auth/callback/credentials'>
<input name='email' type='text' defaultValue='' />
<input name='password' type='password' defaultValue='' />
<button type='submit'>Sign in</button>
</form>
)
}
```
As the JSON Web Token is encrypted, you can safely store user credentials in it and revalidate them whenever an action is performed. See the [callbacks documentation](/configuration/callbacks) for more information on how to interact with the token.
## With multiple providers
@@ -105,7 +84,7 @@ As with all providers, the order you specify them in, is the order they are disp
id: 'domain-login',
name: "Domain Account",
authorize: async (credentials) => {
const user = (credentials) => { /* add function to get user */ }
const user = { /* add function to get user */ }
return Promise.resolve(user)
},
credentials: {
@@ -118,7 +97,7 @@ As with all providers, the order you specify them in, is the order they are disp
id: 'intranet-credentials',
name: "Two Factor Auth",
authorize: async (credentials) => {
const user = (credentials) => { /* add function to get user */ }
const user = { /* add function to get user */ }
return Promise.resolve(user)
},
credentials: {

View File

@@ -84,26 +84,39 @@ The Email Provider can be used with both JSON Web Tokens and database sessions,
You can fully customise the sign in email that is sent by passing a custom function as the `sendVerificationRequest` option to `Providers.Email()`.
The following example shows the complete source for the built-in `sendVerificationRequest()` method.
e.g.
```js {3} title="pages/api/auth/[...nextauth].js"
providers: [
Providers.Email({
server: process.env.EMAIL_SERVER,
from: process.env.EMAIL_FROM,
sendVerificationRequest: ({ identifier: email, url, token, site, provider }) => { /* your function */ }
})
]
```
The following code shows the complete source for the built-in `sendVerificationRequest()` method:
```js
import nodemailer from 'nodemailer'
const sendVerificationRequest = ({ identifier: emailAddress, url, token, site, provider }) => {
const sendVerificationRequest = ({ identifier: email, url, token, site, provider }) => {
return new Promise((resolve, reject) => {
const { server, from } = provider
const siteName = site.replace(/^https?:\/\//, '')
site = site.replace(/^https?:\/\//, '') // Strip protocol from site
nodemailer
.createTransport(server)
.sendMail({
to: emailAddress,
to: email,
from,
subject: `Sign in to ${siteName}`,
text: text({ url, siteName }),
html: html({ url, siteName })
subject: `Sign in to ${site}`,
text: text({ url, site, email }),
html: html({ url, site, email })
}, (error) => {
if (error) {
console.error('SEND_VERIFICATION_EMAIL_ERROR', emailAddress, error)
logger.error('SEND_VERIFICATION_EMAIL_ERROR', email, error)
return reject(new Error('SEND_VERIFICATION_EMAIL_ERROR', error))
}
return resolve()
@@ -112,33 +125,61 @@ const sendVerificationRequest = ({ identifier: emailAddress, url, token, site, p
}
// Email HTML body
const html = ({ url, siteName }) => {
const buttonBackgroundColor = '#444444'
const html = ({ url, site, 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, '&#8203;.')}`
const escapedSite = `${site.replace(/\./g, '&#8203;.')}`
// Some simple styling options
const backgroundColor = '#f9f9f9'
const textColor = '#444444'
const mainBackgroundColor = '#ffffff'
const buttonBackgroundColor = '#346df1'
const buttonBorderColor = '#346df1'
const buttonTextColor = '#ffffff'
// Uses tables for layout and inline CSS due to email client limitations
return `
<table width="100%" border="0" cellspacing="0" cellpadding="0">
<tr>
<td align="center" style="padding: 8px 0; font-size: 22px; font-family: Helvetica, Arial, sans-serif; color: #888888;">
${siteName}
</td>
</tr>
<tr>
<td align="center" style="padding: 16px 0;">
<table border="0" cellspacing="0" cellpadding="0">
<tr>
<td align="center" style="border-radius: 3px;" bgcolor="${buttonBackgroundColor}"><a href="${url}" target="_blank" style="font-size: 18px; font-family: Helvetica, Arial, sans-serif; color: ${buttonTextColor}; text-decoration: none; text-decoration: none;border-radius: 3px; padding: 12px 18px; border: 1px solid ${buttonBackgroundColor}; display: inline-block; font-weight: bold;">Sign in</a></td>
</tr>
</table>
</td>
</tr>
</table>
<body style="background: ${backgroundColor};">
<table width="100%" border="0" cellspacing="0" cellpadding="0">
<tr>
<td align="center" style="padding: 10px 0px 20px 0px; font-size: 22px; font-family: Helvetica, Arial, sans-serif; color: ${textColor};">
<strong>${escapedSite}</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>
</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; text-decoration: none;border-radius: 5px; padding: 10px 20px; border: 1px solid ${buttonBorderColor}; 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};">
If you did not request this email you can safely ignore it.
</td>
</tr>
</table>
</body>
`
}
// Email Text body (fallback for email clients that don't render HTML, e.g. feature phones)
const text = ({ url, siteName }) => `Sign in to ${siteName}\n${url}\n\n`
// Email text body  fallback for email clients that don't render HTML
const text = ({ url, site }) => `Sign in to ${site}\n${url}\n\n`
```
:::tip
If you want to generate email-client compatible HTML from React, check out https://mjml.io
If you want to generate great looking email client compatible HTML with React, check out https://mjml.io
:::