Compare commits

...

33 Commits
v2.0 ... v2.2.0

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
Iain Collins
f899d7bb04 Update version to 2.1.0 2020-06-24 01:57:28 +01:00
Iain Collins
e36646ce7f Update docs formatting 2020-06-24 01:53:49 +01:00
Iain Collins
f3d36a74c9 Update provider documentation 2020-06-24 01:53:49 +01:00
Iain Collins
4e11c9c36e Fix linter errors 2020-06-24 01:26:07 +01:00
Iain Collins
0a7ac36584 Update client documentation 2020-06-24 01:26:07 +01:00
Iain Collins
fc4850f354 Update documentation for client and options 2020-06-24 01:26:07 +01:00
Iain Collins
6e9a8d2074 Update beta version 2020-06-24 01:26:07 +01:00
Iain Collins
c712d7da07 Remove base URL cookie; no longer needed 2020-06-24 01:26:07 +01:00
Iain Collins
5183181d1c Refactor client to allow provider to be passed options 2020-06-24 01:26:07 +01:00
Iain Collins
b024f89ba8 Update client max age documentation 2020-06-24 01:26:07 +01:00
Iain Collins
fbbe516b9a Refactor client to take configuraiton from _app.js 2020-06-24 01:26:07 +01:00
Iain Collins
d48a3fd948 Rename internal debug env var for consistancy
This is a naughty global used server side to more easily facilitate debugging.
2020-06-24 01:26:07 +01:00
ndo@$(hostname)
86f0c53bd3 add: npm publish workflow 2020-06-24 01:19:51 +01:00
Onur
7f6cc2048b Minor typo fix 2020-06-24 00:00:09 +01:00
Lori Karikari
b2829f6384 added missing comma to homepage example 2020-06-23 18:33:59 +02:00
Iain Collins
67c5041860 Remove console.log from auth0 provider 2020-06-23 13:00:25 +01:00
Lori Karikari
33df9e3132 updated Cognito (#307) 2020-06-23 11:40:18 +02:00
Lori Karikari
602bc28a45 Add new OAuth providers (#221) 2020-06-23 11:31:18 +02:00
jose-donato
5a7a494701 typo 2020-06-23 00:34:44 +01:00
Iain Collins
9fa82cedbd Fix Auth0 provider
Resolves #301
2020-06-23 00:13:55 +01:00
Iain Collins
b0408284b8 Tweak homepage CSS 2020-06-21 17:30:43 +01:00
50 changed files with 676 additions and 460 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

36
.github/workflows/npm-publish.yml vendored Normal file
View File

@@ -0,0 +1,36 @@
# Publishes module to registry when a new release is created.
#
# The following secrets need to be configured for this workflow:
#
# * NPM_TOKEN - Auth token from npmjs.com
name: Publish to NPM
on:
release:
types: [created]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: 12
- run: npm ci
- run: npm test
publish-npm:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: 12
registry-url: https://registry.npmjs.org/
- run: npm ci
- run: npm publish
env:
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}

2
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{
"name": "next-auth",
"version": "2.0.0",
"version": "2.1.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "next-auth",
"version": "2.0.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

@@ -15,4 +15,4 @@ const css = fs.readFileSync(pathToCss, 'utf8')
const cssWithEscapedQuotes = css.replace(/"/gm, '\\"')
const js = `module.exports = function() { return "${cssWithEscapedQuotes}" }`
fs.writeFileSync(pathToCssJs, js)
fs.writeFileSync(pathToCssJs, 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

@@ -3,41 +3,47 @@
import { useState, useEffect, useContext, createContext, createElement } from 'react'
import logger from '../lib/logger'
// Note: In calls to fetch() from universal methods, all cookies are passed
// through from the browser, when the server makes the HTTP request, so that
// it can authenticate as the browser.
const __NEXTAUTH = {
site: '',
basePath: '/api/auth',
clientMaxAge: 0 // e.g. 0 == disabled, 60 == 60 seconds
}
// These can be overridden with NEXTAUTH_ env vars in next.config.js
// e.g. process.env.NEXTAUTH_SITE
const NEXTAUTH_DEFAULT_BASE_URL_COOKIE_NAME = 'next-auth.base-url'
const NEXTAUTH_DEFAULT_SITE = ''
const NEXTAUTH_DEFAULT_BASE_PATH = '/api/auth'
const NEXTAUTH_DEFAULT_CLIENT_MAXAGE = 0 // e.g. 0 == disabled, 60 == 60 seconds
let __NEXTAUTH_EVENT_LISTENER_ADDED = false
let NEXTAUTH_EVENT_LISTENER_ADDED = false
// Method to set options. The documented way is to use the provider, but this
// method is being left in as an alternative, that will be helpful if/when we
// expose a vanilla JavaScript version that doesn't depend on React.
const setOptions = ({
site,
basePath,
clientMaxAge
} = {}) => {
if (site) { __NEXTAUTH.site = site }
if (basePath) { __NEXTAUTH.basePath = basePath }
if (clientMaxAge) { __NEXTAUTH.clientMaxAge = clientMaxAge }
}
// Universal method (client + server)
const getSession = async ({ req } = {}) => {
const baseUrl = _baseUrl({ req })
const baseUrl = _baseUrl()
const options = req ? { headers: { cookie: req.headers.cookie } } : {}
const session = await _fetchData(`${baseUrl}/session`, options)
_sendMessage({ event: 'session', data: { triggeredBy: 'getSession' } })
_sendMessage({ event: 'session', data: { trigger: 'getSession' } })
return session
}
// Universal method (client + server)
const getProviders = async ({ req } = {}) => {
const baseUrl = _baseUrl({ req })
const options = req ? { headers: { cookie: req.headers.cookie } } : {}
return _fetchData(`${baseUrl}/providers`, options)
const getProviders = async () => {
const baseUrl = _baseUrl()
return _fetchData(`${baseUrl}/providers`)
}
// Universal method (client + server)
const getCsrfToken = async ({ req } = {}) => {
const baseUrl = _baseUrl({ req })
const options = req ? { headers: { cookie: req.headers.cookie } } : {}
const data = await _fetchData(`${baseUrl}/csrf`, options)
return data.csrfToken
const getCsrfToken = async () => {
const baseUrl = _baseUrl()
const data = await _fetchData(`${baseUrl}/csrf`)
return data && data.csrfToken ? data.csrfToken : null
}
// Context to store session data globally
@@ -57,8 +63,7 @@ const useSession = (session) => {
// Internal hook for getting session from the api.
const useSessionData = (session) => {
const clientMaxAge = (process.env.NEXTAUTH_CLIENT_MAXAGE || NEXTAUTH_DEFAULT_CLIENT_MAXAGE) * 1000
const clientMaxAge = __NEXTAUTH.clientMaxAge * 1000
const [data, setData] = useState(session)
const [loading, setLoading] = useState(true)
const _getSession = async (sendEvent = true) => {
@@ -68,17 +73,17 @@ const useSessionData = (session) => {
// Send event to trigger other tabs to update (unless sendEvent is false)
if (sendEvent) {
_sendMessage({ event: 'session', data: { triggeredBy: 'useSessionData' } })
_sendMessage({ event: 'session', data: { trigger: 'useSessionData' } })
}
if (typeof window !== 'undefined' && NEXTAUTH_EVENT_LISTENER_ADDED === false) {
NEXTAUTH_EVENT_LISTENER_ADDED = true
if (typeof window !== 'undefined' && __NEXTAUTH_EVENT_LISTENER_ADDED === false) {
__NEXTAUTH_EVENT_LISTENER_ADDED = true
window.addEventListener('storage', async (event) => {
if (event.key === 'nextauth.message') {
const message = JSON.parse(event.newValue)
if (message.event && message.event === 'session' && message.data) {
// Fetch new session data but tell it not to fire an event to
// avoid an infinate loop.
// avoid an infinite loop.
//
// Note: We could pass session data through and do something like
// `setData(message.data)` but that causes problems depending on
@@ -159,18 +164,18 @@ const signout = async (args) => {
}
const res = await fetch(`${baseUrl}/signout`, options)
_sendMessage({ event: 'session', data: { triggeredBy: 'signout' } })
_sendMessage({ event: 'session', data: { trigger: 'signout' } })
window.location = res.url ? res.url : callbackUrl
}
// Provider to wrap the app in to make session data available globally
const Provider = ({ children, session }) => {
const value = useSession(session)
return createElement(SessionContext.Provider, { value }, children)
const Provider = ({ children, session, options }) => {
setOptions(options)
return createElement(SessionContext.Provider, { value: useSession(session) }, children)
}
const _fetchData = async (url, options) => {
const _fetchData = async (url, options = {}) => {
try {
const res = await fetch(url, options)
const data = await res.json()
@@ -181,44 +186,7 @@ const _fetchData = async (url, options) => {
}
}
const _baseUrl = ({ req } = {}) => {
if (req) {
// Server Side
// If we have a 'req' object are running sever side, so we should grab the
// base URL from cookie that is set by the API route - which is how config
// is shared automatically between the API route and the client.
const cookies = req ? _parseCookies(req.headers.cookie) : null
const baseUrlCookieName = process.env.NEXTAUTH_BASE_URL_COOKIE_NAME || NEXTAUTH_DEFAULT_BASE_URL_COOKIE_NAME
const cookieValue = cookies[`__Secure-${baseUrlCookieName}`] || cookies[baseUrlCookieName]
const [baseUrl] = cookieValue ? cookieValue.split('|') : [null]
return baseUrl
} else {
// Client Side
// Note: 'site' is empty by default; URL is normally relative.
const site = process.env.NEXTAUTH_SITE || NEXTAUTH_DEFAULT_SITE
const basePath = process.env.NEXTAUTH_BASE_PATH || NEXTAUTH_DEFAULT_BASE_PATH
return `${site}${basePath}`
}
}
// Adapted from https://github.com/felixfong227/simple-cookie-parser/blob/master/index.js
const _parseCookies = (string) => {
if (!string) { return {} }
try {
const object = {}
const a = string.split(';')
for (let i = 0; i < a.length; i++) {
const b = a[i].split('=')
if (b[0].length > 1 && b[1]) {
object[b[0].trim()] = decodeURIComponent(b[1])
}
}
return object
} catch (error) {
logger.error('CLIENT_COOKIE_PARSE_ERROR', error)
return {}
}
}
const _baseUrl = () => `${__NEXTAUTH.site}${__NEXTAUTH.basePath}`
const _encodedForm = (formData) => {
return Object.keys(formData).map((key) => {
@@ -233,6 +201,10 @@ const _sendMessage = (message) => {
}
export default {
// Call config() from _app.js to set options globally in the app.
// You need to set at least the site name to use server side calls.
options: setOptions,
setOptions,
// Some methods are exported with more than one name. This provides
// flexibility over how they can be invoked and compatibility with earlier
// releases (going back to v1 and earlier v2 beta releases).

View File

@@ -7,4 +7,4 @@ import path from 'path'
const pathToCss = path.join(__dirname, '/index.css')
const css = fs.readFileSync(pathToCss, 'utf8')
export default () => css
export default () => css

View File

@@ -11,7 +11,7 @@ const logger = {
}
},
debug: (debugCode, ...text) => {
if (process && process.env && process.env._NEXT_AUTH_DEBUG) {
if (process && process.env && process.env._NEXTAUTH_DEBUG) {
console.log(
`[next-auth][debug][${debugCode}]`,
text

View File

@@ -4,11 +4,11 @@ export default (options) => {
name: 'Auth0',
type: 'oauth',
version: '2.0',
params: { grant_type: 'authorization_code', response_type: 'code' },
params: { grant_type: 'authorization_code' },
scope: 'openid email profile',
accessTokenUrl: `https://${options.subdomain}.auth0/oauth/token`,
authorizationUrl: `https://${options.subdomain}.auth0.com/authorize?`,
profileUrl: `http://${options.subdomain}.auth0.com/userinfo`,
accessTokenUrl: `https://${options.domain}/oauth/token`,
authorizationUrl: `https://${options.domain}/authorize?response_type=code`,
profileUrl: `https://${options.domain}/userinfo`,
profile: (profile) => {
return {
id: profile.sub,

View File

@@ -0,0 +1,29 @@
export default (options) => {
const { region } = options
return {
id: 'battlenet',
name: 'Battle.net',
type: 'oauth',
version: '2.0',
scope: 'openid',
params: { grant_type: 'authorization_code' },
accessTokenUrl:
region === 'CN'
? 'https://www.battlenet.com.cn/oauth/token'
: `https://${region}.battle.net/oauth/token`,
authorizationUrl:
region === 'CN'
? 'https://www.battlenet.com.cn/oauth/authorize'
: `https://${region}.battle.net/oauth/authorize`,
profileUrl: 'https://us.battle.net/oauth/userinfo',
profile: (profile) => {
return {
id: profile.id,
name: profile.battletag,
email: null,
image: null
}
},
...options
}
}

23
src/providers/cognito.js Normal file
View File

@@ -0,0 +1,23 @@
export default (options) => {
const { domain } = options
return {
id: 'cognito',
name: 'Cognito',
type: 'oauth',
version: '2.0',
scope: 'openid profile email',
params: { grant_type: 'authorization_code' },
accessTokenUrl: `https://${domain}/oauth2/token`,
authorizationUrl: `https://${domain}/oauth2/authorize?response_type=code`,
profileUrl: `https://${domain}/oauth2/userInfo`,
profile: (profile) => {
return {
id: profile.sub,
name: profile.username,
email: profile.email,
image: null
}
},
...options
}
}

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

@@ -2,9 +2,11 @@ import Auth0 from './auth0'
import Apple from './apple'
import Box from './box'
import Credentials from './credentials'
import BattleNet from './battlenet'
import Cognito from './cognito'
import Discord from './discord'
import Email from './email'
import Facebook from './facebook' // @TODO
import Facebook from './facebook'
import GitHub from './github'
import GitLab from './gitlab'
import Google from './google'
@@ -21,6 +23,8 @@ export default {
Apple,
Box,
Credentials,
BattleNet,
Cognito,
Discord,
Email,
Facebook,

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 {
@@ -89,15 +89,6 @@ export default async (req, res, userSuppliedOptions) => {
secure: useSecureCookies
}
},
baseUrl: {
name: `${cookiePrefix}next-auth.base-url`,
options: {
httpOnly: true,
sameSite: 'lax',
path: '/',
secure: useSecureCookies
}
},
csrfToken: {
// Default to __Host- for CSRF token for additional protection if using useSecureCookies
// NB: The `__Host-` prefix is stricter than the `__Secure-` prefix.
@@ -180,24 +171,6 @@ export default async (req, res, userSuppliedOptions) => {
cookie.set(res, cookies.csrfToken.name, newCsrfTokenCookie, cookies.csrfToken.options)
}
// Set canonical site name + API route in a cookie to facilitate passing configuration
// to the NextAuth client. There are potential security considerations around this
// relating to trying to prevent attackers from exploiting this by setting this cookie
// on the client first if they can get control of a sub domain or exploit a XSS
// vulnerability, but this approach attempts to mitgate that by always verifying
// the cookie and updating it if fails the verification check.
let setUrlPrefixCookie = true
if (req.cookies[cookies.baseUrl.name]) {
const [baseUrlValue, baseUrlHash] = req.cookies[cookies.baseUrl.name].split('|')
// If the hash on the cookie is verified, then we must have set the cookie and don't need to update it
if (baseUrlValue === baseUrl && baseUrlHash === createHash('sha256').update(`${baseUrlValue}${secret}`).digest('hex')) { setUrlPrefixCookie = false }
}
// If the cookie is not set already (or if it is set, but failed verification) set header to update the cookie
if (setUrlPrefixCookie) {
const newUrlPrefixCookie = `${baseUrl}|${createHash('sha256').update(`${baseUrl}${secret}`).digest('hex')}`
cookie.set(res, cookies.baseUrl.name, newUrlPrefixCookie, cookies.baseUrl.options)
}
// User provided options are overriden by other options,
// except for the options with special handling above
const options = {
@@ -227,7 +200,7 @@ export default async (req, res, userSuppliedOptions) => {
}
// If debug enabled, set ENV VAR so that logger logs debug messages
if (options.debug === true) { process.env._NEXT_AUTH_DEBUG = true }
if (options.debug === true) { process.env._NEXTAUTH_DEBUG = true }
// Get / Set callback URL based on query param / cookie + validation
options.callbackUrl = await callbackUrlHandler(req, res, options)

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

@@ -1,5 +1,3 @@
import fs from 'fs'
import path from 'path'
import signin from './signin'
import signout from './signout'
import verifyRequest from './verify-request'
@@ -19,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

@@ -7,16 +7,16 @@ Callbacks are asynchronous functions you can use to control what happens when an
Callbacks are extremely powerful, especially in scenarios involving JSON Web Tokens as they allow you to implement access controls without a database and to integrate with external databases or APIs.
### Example
You can specify a handler for any of the callbacks below.
#### How to use the callback option
```js
```js title="pages/api/auth/[...nextauth.js]"
callbacks: {
signin: async (profile, account, metadata) => { },
redirect: async (url, baseUrl) => { },
session: async (session, token) => { },
jwt: async (token) => { }
jwt: async (token, oAuthProfile) => { }
}
```

View File

@@ -0,0 +1,23 @@
---
id: events
title: Events
---
Events are asynchronous functions that do not return a response, they are useful for audit logging.
### Example
You can specify a handler for any of these events below - e.g. for debugging or to create an audit log.
```js title="pages/api/auth/[...nextauth.js]"
events: {
signin: async (message) => { /* on successful sign in */ },
signout: async (message) => { /* on signout */ },
createUser: async (message) => { /* user created */ },
linkAccount: async (message) => { /* account linked to a user */ },
session: async (message) => { /* session is active */ },
error: async (message) => { /* error in authentication flow */ }
}
```
The content of the message object varies depending on the flow (e.g. OAuth or Email authentication flow, JWT or database sessions, etc), but typically contains a user object and/or contents of the JSON Web Token and other information relevent to the event.

View File

@@ -289,20 +289,16 @@ Advanced options are passed the same way as basic options, but may have complex
This option allows you to specify a different base path if you don't want to use `/api/auth` for some reason.
If you set this option you **must** also specify the same value in the `NEXTAUTH_BASE_PATH` environment variable in `next.config.js` so that the client knows how to contact the server:
If you set this option you **must** also configure it along with the `site` property in `pages/_app.js`
```js title="next.config.js"
module.exports = {
env: {
NEXTAUTH_BASE_PATH: '/api/my-custom-auth-route',
},
}
```js title="pages/_app.js"
import { config } from 'next-auth/client'
config({
site: process.env.SITE, // e.g. 'http://localhost:3000'
basePath: process.env.BASE_PATH // e.g. '/api/some-other-route-name'
})
```
This is required because the NextAuth.js API route is a separate codepath to the NextAuth.js Client.
As long as you also specify this option in an environment variable, the client will be able to pick up any subsequent configuration from the server, but if you do not set in both it the NextAuth.js Client will not work.
---
### adapter
@@ -379,15 +375,6 @@ cookies: {
secure: true
}
},
baseUrl: {
name: `__Secure-next-auth.base-url`,
options: {
httpOnly: true,
sameSite: 'lax',
path: '/',
secure: true
}
},
csrfToken: {
name: `__Host-next-auth.csrf-token`,
options: {
@@ -403,38 +390,3 @@ cookies: {
:::warning
Changing the cookie options may introduce security flaws into your application and may break NextAuth.js integration now or in a future update. Using this option is not recommended.
:::
---
### Client Max Age
* **Default value**: `0`
* **Required**: *No*
#### Description
By default the NextAuth.js client will use whatever cached session object it has and will not not re-check the current session if using the `useSession()` hook.
You can change this behaviour and force it to periodically sync the session state by setting a `NEXTAUTH_CLIENT_MAXAGE` environment variable.
```js title="next.config.js"
module.exports = {
env: {
NEXTAUTH_CLIENT_MAXAGE: 60, // Will re-check session every 60 seconds
},
}
```
If set to `0` (the default) sessions are not re-checked automatically, only when a new window or tab is opened or when `getSession()` is called.
If set to any other value, specifies how many seconds the window or tab should poll the server to update the session data.
When a session is checked this way (or using `getSession()`) it is active and extends the life of the current session.
It can be useful to use this option to prevent sessions from timing out if your application has a short session expiry time.
This option usually has cost implications as checking session status triggers a call to a server side route and/or a database.
:::note
In NextAuth.js session state is automatically synchronized across all open windows and tabs in the same browser. If you have session expiry times of 30 days or more (the default) you probably don't need to use this option, or can set it to a high value (e.g. every 24 hours).
:::

View File

@@ -7,11 +7,11 @@ NextAuth.js automatically creates simple, unbranded authentication pages for han
The options displayed on the sign up page are automatically generated based on the providers specified in the options passed to NextAuth.js.
## Using custom pages
### Example
To add a custom login page, for example. You can us the `pages` option:
```javascript title="/pages/api/auth/[...nextauth].js"
```javascript title="pages/api/auth/[...nextauth].js"
...
pages: {
signin: '/auth/signin',
@@ -29,7 +29,7 @@ To add a custom login page, for example. You can us the `pages` option:
In order to get the available authentication providers and the URLs to use for them, you can make a request to the API endpoint `/api/auth/providers`:
```jsx title="/pages/auth/signin"
```jsx title="pages/auth/signin"
import React from 'react'
import { providers, signin } from 'next-auth/client'
@@ -70,7 +70,7 @@ This is easier of if you use the build in `signin()` function, as it sets the CS
To create a sign in page that works on clients with and without client side JavaScript, you can use both the **signin()** method and the **csrfToken()** method
:::
```jsx title="/pages/auth/email-signin"
```jsx title="pages/auth/email-signin"
import React from 'react'
import { csrfToken, signin } from 'next-auth/client'

View File

@@ -23,7 +23,9 @@ NextAuth.js is designed to work with any OAuth service, it supports OAuth 1.0, 1
* [Apple](/providers/apple)
* [Auth0](/providers/auth0)
* [Battle.net](/providers/battlenet)
* [Box](/providers/box)
* [Amazon Cognito](/providers/cognito)
* [Discord](/providers/discord)
* [Facebook](/providers/facebook)
* [GitHub](/providers/github)
@@ -58,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({
@@ -108,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({
@@ -158,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,
@@ -181,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,22 @@ 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
When using any of the client API methods server side, [context](https://nextjs.org/docs/api-reference/data-fetching/getInitialProps#context-object) must be passed as an argument. The documentation for **getSession()** has an example.
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'
export default ({ Component, pageProps }) => {
const { session } = pageProps
return (
<Provider options={{ site: process.env.SITE }} session={session} >
<Component {...pageProps} />
</Provider>
)
}
```
Specify the same site name as used in the route. [Documentation for `<Provider>`](/getting-started/client#provider)
:::
## useSession()
@@ -64,9 +79,9 @@ You can call `getSession()` inside a function to check if a user is signed in, o
Note that because it exposed to the client it does not contain sensitive information such as the Session Token or OAuth service related tokens. It includes enough information (e.g name, email) to display information on a page about the user who is signed in, and an Access Token that can be used to identify the session without exposing the Session Token itself.
:::
Because it is a Universal method, you can use `getSession()` in both client and server side functions, such as `getInitialProps()` in Next.js:
Because it is a Universal method, you can use `getSession()` in both client and server side functions.
```jsx title="/pages/index.js"
```jsx title="pages/index.js"
import { getSession } from 'next-auth/client'
const Page = ({ session }) => (<p>
@@ -89,36 +104,8 @@ Page.getInitialProps = async (context) => {
export default Page
```
#### Using getSession() in API routes
You can also get the session object in Next.js API routes:
```js
import { getSession } from 'next-auth/client'
export default (req, res) => {
const session = await getSession({ req })
if (session) {
// Signed in
const { accessToken } = session.user
// Do something with accessToken (e.g. look up user in DB)
res.statusCode = 200
res.setHeader('Content-Type', 'application/json')
res.end(JSON.stringify({ /* data */ }))
} else {
// Not signed in
res.status(302).setHeader('Location', pages.newUser)
res.end()
}
}
```
:::note
When calling `getSession()` server side, you must pass the request object - e.g. `getSession({req})` - or you can the pass entire `context` object as it contains the `req` object.
When calling `getSession()` server side, you must pass the request object or you can the pass entire `context` object as it contains the `req` object. e.g. `getSession(context)` or `getSession({req})`
:::
---
@@ -200,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.
@@ -231,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.
---
@@ -239,17 +226,17 @@ The URL must be considered valid by the [redirect callback handler](http://local
Using the supplied React `<Provider>` allows instances of `useSession()` to share the session object across components, by using [React Context](https://reactjs.org/docs/context.html) under the hood.
This improves performance, reduces network calls and avoids page flicker when rendering.
This improves performance, reduces network calls and avoids page flicker when rendering. It is highly recommended and can be easily added to all pages in Next.js apps by using `pages/_app.js`.
It is highly recommended and can be easily added to all pages in Next.js apps by using `/pages/_app.js`.
It is also *required* if you want to use client methods like `getSession()` in server side functions like `getServerSideProp()` or `getInitialProps()`.
```jsx title="/pages/_app.js"
```jsx title="pages/_app.js"
import { Provider } from 'next-auth/client'
export default ({ Component, pageProps }) => {
const { session } = pageProps
return (
<Provider session={session}>
<Provider options={{ site: process.env.SITE }} session={session} >
<Component {...pageProps} />
</Provider>
)
@@ -258,6 +245,21 @@ export default ({ Component, pageProps }) => {
If you pass the `session` page prop to the `<Provider>` as in the example above you can avoid checking the session twice on pages that support both server and client side rendering.
### Options
* `site` (required) - The URL of the site (e.g. `http://localhost:3000`)
* `baseUrl` (optional) - The base URL for NextAuth.js (e.g. `/api/auth`)
* `clientMaxAge` (optional) - How often to refresh the session the background (in seconds)
When `clientMaxAge` is set to `0` (the default) sessions are not re-checked automatically, only when a new window or tab is opened or when `getSession()` is called. If set to any other value, specifies how many seconds the client should poll the server to check the session is valid and to keep it alive.
It can be useful to use this option to prevent sessions from timing out if your application has a short session expiry time. This option usually has cost implications as checking session status triggers a call to a server side route and/or a database.
:::tip
In NextAuth.js session state is automatically synchronized across all open windows and tabs in the same browser. If you have session expiry times of 30 days or more (the default) you probably don't need to use the `clientMaxAge` option, or can set it to a high value (e.g. every 24 hours).
:::
:::note
See [**the Next.js documentation**](https://nextjs.org/docs/advanced-features/custom-app) for more information on **_app.js** in Next.js applications.
:::

View File

@@ -17,7 +17,7 @@ You can find a live demo of the example project at [next-auth-example.now.sh](ht
To add NextAuth.js to a project, first create a file called `[...nextauth].js` in `pages/api/auth`.
```javascript title="/pages/api/auth/[...nextauth].js"
```javascript title="pages/api/auth/[...nextauth].js"
import NextAuth from 'next-auth'
import Providers from 'next-auth/providers'
@@ -49,7 +49,7 @@ See the [options documentation](/configuration/options) for how to configure pro
The `useSession()` React Hook in the NextAuth.js client is the easiest way to check if someone is signed in.
```jsx title="/pages/index.js"
```jsx title="pages/index.js"
import React from 'react'
import { useSession } from 'next-auth/client'

View File

@@ -27,7 +27,7 @@ NextAuth.js can be used with or without a database.
* An open source solution that allows you to keep control of your data
* Supports Bring Your Own Database (BYOD) and can be used with any database
* Built-in support for for [MySQL, MariaDB, Postgres, MongoDB and SQLite](/configuration/database)
* Built-in support for [MySQL, MariaDB, Postgres, MongoDB and SQLite](/configuration/database)
* Works great with databases from popular hosting providers
* Can also be used *without a database* (e.g. OAuth + JWT)

View File

@@ -3,16 +3,15 @@ id: apple
title: Apple
---
## API Documentation
## Documentation
https://developer.apple.com/sign-in-with-apple/get-started/
## App Configuration
## Configuration
https://developer.apple.com/account/resources/identifiers/list/serviceId
## Usage
## Example
There are two ways you can use the Sign in with Apple provider.

View File

@@ -3,15 +3,19 @@ id: auth0
title: Auth0
---
## API Documentation
## Documentation
https://auth0.com/docs/api/authentication#authorize-application
## App Configuration
## Configuration
https://manage.auth0.com/dashboard
## Usage
:::tip
Configure your application in Auth0 as a 'Regular Web Application' (not a 'Single Page App').
:::
## Example
```js
import Providers from `next-auth/providers`
@@ -20,8 +24,12 @@ providers: [
Providers.Auth0({
clientId: process.env.AUTH0_CLIENT_ID,
clientSecret: process.env.AUTH0_CLIENT_SECRET,
subdomain: process.env.AUTH0_SUBDOMAIN
domain: process.env.AUTH0_DOMAIN
})
}
...
```
```
:::note
`domain` should be the fully qualified domain  e.g. `dev-s6clz2lv.eu.auth0.com`
:::

View File

@@ -0,0 +1,27 @@
---
id: battle.net
title: Battle.net
---
## Documentation
https://develop.battle.net/documentation/guides/using-oauth
## Configuration
https://develop.battle.net/access/clients
## Example
```js
import Providers from `next-auth/providers`
...
providers: [
Providers.BattleNet({
clientId: process.env.BATTLENET_CLIENT_ID,
clientSecret: process.env.BATTLENET_CLIENT_SECRET,
region: process.env.BATTLENET_REGION
})
}
...
```

View File

@@ -3,15 +3,15 @@ id: box
title: Box
---
## API Documentation
## Documentation
https://developer.box.com/reference/
## App Configuration
## Configuration
https://developer.box.com/guides/sso-identities-and-app-users/connect-okta-to-app-users/configure-box/
## Usage
## Example
```js
import Providers from `next-auth/providers`

View File

@@ -0,0 +1,35 @@
---
id: cognito
title: Amazon Cognito
---
## Documentation
https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-userpools-server-contract-reference.html
## Configuration
https://console.aws.amazon.com/cognito/users/
You need to select your AWS region to go the the Cognito dashboard.
## Example
```js
import Providers from `next-auth/providers`
...
providers: [
Providers.Cognito({
clientId: process.env.COGNITO_CLIENT_ID,
clientSecret: process.env.COGNITO_CLIENT_SECRET,
domain: process.env.COGNITO_DOMAIN,
})
}
...
```
warning:::
Make sure you select all the appropriate client settings or the OAuth flow will not work.
:::
![cognito](https://user-images.githubusercontent.com/7902980/83951604-cd096e80-a832-11ea-8bd2-c496ec9a16cb.PNG)

View File

@@ -27,7 +27,7 @@ It comes with the constraint that users authenticated in this manner are not per
The functionality provided for credentials based authentication is intentionally limited to discourage use of passwords due to the inherent security risks associated with them and the additional complexity associated with supporting usernames and passwords.
:::
## Usage
## Example
The Credentials provider is specified like other providers, except that you need to define a handler for `authorize()` that accepts credentials input and returns either a `user` object or `false`.
@@ -35,7 +35,7 @@ If you return an object it will be persisted to the JSON Web Token and the user
If you return `false` or `null` then an error will be displayed advising the user to check their details.
```js title="/pages/api/auth/[...nextauth].js"
```js title="pages/api/auth/[...nextauth].js"
import Providers from `next-auth/providers`
...
providers: [
@@ -50,43 +50,22 @@ 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)
}
}
})
}
]
...
```
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

@@ -3,15 +3,15 @@ id: discord
title: Discord
---
## API Documentation
## Documentation
https://discord.com/developers/docs/topics/oauth2
## App Configuration
## Configuration
https://discord.com/developers/applications
## Usage
## Example
```js
import Providers from `next-auth/providers`

View File

@@ -39,7 +39,7 @@ The Email Provider can be used with both JSON Web Tokens and database sessions,
Now you can add the email provider like this:
```js {3} title="/pages/api/auth/[...nextauth].js"
```js {3} title="pages/api/auth/[...nextauth].js"
providers: [
Providers.Email({
server: process.env.EMAIL_SERVER,
@@ -61,7 +61,7 @@ The Email Provider can be used with both JSON Web Tokens and database sessions,
```
Now you can add the provider settings to the NextAuth options object in the Email Provider.
```js title="/pages/api/auth/[...nextauth].js"
```js title="pages/api/auth/[...nextauth].js"
providers: [
Providers.Email({
server: {
@@ -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
:::

View File

@@ -3,15 +3,15 @@ id: facebook
title: Facebook
---
## API Documentation
## Documentation
https://developers.facebook.com/docs/facebook-login/manually-build-a-login-flow/
## App Configuration
## Configuration
https://developers.facebook.com/apps/
## Usage
## Example
```js
import Providers from `next-auth/providers`

View File

@@ -3,15 +3,15 @@ id: github
title: GitHub
---
## API Documentation
## Documentation
https://developer.github.com/apps/building-oauth-apps/authorizing-oauth-apps
## App Configuration
## Configuration
https://github.com/settings/apps
## Usage
## Example
```js
import Providers from `next-auth/providers`
@@ -23,7 +23,8 @@ providers: [
})
}
...
```
:::warning
Only allows one callback URL. May not return email address if privacy enabled.
Only allows one callback URL per Client ID + Secret. May not return email address if privacy enabled.
:::

View File

@@ -3,15 +3,15 @@ id: gitlab
title: GitLab
---
## API Documentation
## Documentation
https://docs.gitlab.com/ee/api/oauth2.html
## App Configuration
## Configuration
https://gitlab.com/profile/applications
## Usage
## Example
```js
import Providers from `next-auth/providers`
@@ -24,6 +24,7 @@ providers: [
}
...
```
:::tip
Enable the *"read_user"* option in scope if you want to save the users email address on sign up.
:::

View File

@@ -3,15 +3,15 @@ id: google
title: Google
---
## API Documentation
## Documentation
https://developers.google.com/identity/protocols/oauth2
## App Configuration
## Configuration
https://console.developers.google.com/apis/credentials
## Usage
## Example
```js
import Providers from `next-auth/providers`

View File

@@ -3,15 +3,12 @@ id: identity-server4
title: IdentityServer4
---
## API Documentation
## Documentation
https://identityserver4.readthedocs.io/en/latest/
## App Configuration
## Usage
## Example
```js
import Providers from `next-auth/providers`

View File

@@ -3,15 +3,15 @@ id: mixer
title: Mixer
---
## API Documentation
## Documentation
https://dev.mixer.com/reference/oauth
## App Configuration
## Configuration
https://mixer.com/lab/oauth
## Usage
## Example
```js
import Providers from `next-auth/providers`

View File

@@ -3,15 +3,11 @@ id: okta
title: Okta
---
## API Documentation
## Documentation
https://developer.okta.com/docs/reference/api/oidc
## App Configuration
## Usage
## Example
```js
import Providers from `next-auth/providers`

View File

@@ -3,15 +3,15 @@ id: slack
title: Slack
---
## API Documentation
## Documentation
https://api.slack.com
## App Configuration
## Configuration
https://api.slack.com/apps
## Usage
## Example
```js
import Providers from `next-auth/providers`

View File

@@ -3,15 +3,15 @@ id: twitch
title: Twitch
---
## API Documentation
## Documentation
https://dev.twitch.tv/docs/authentication
## App Configuration
## Configuration
https://dev.twitch.tv/console/apps
## Usage
## Example
```js
import Providers from `next-auth/providers`

View File

@@ -3,15 +3,15 @@ id: twitter
title: Twitter
---
## API Documentation
## Documentation
https://developer.twitter.com
## App Configuration
## Configuration
https://developer.twitter.com/en/apps
## Usage
## Example
```js
import Providers from `next-auth/providers`

View File

@@ -3,15 +3,15 @@ id: yandex
title: Yandex
---
## API Documentation
## Documentation
https://tech.yandex.com/oauth/doc/dg/concepts/about-docpage/
## App Configuration
## Configuration
https://oauth.yandex.com/client/new
## Usage
## Example
```js
import Providers from `next-auth/providers`

View File

@@ -11,12 +11,15 @@ module.exports = {
'configuration/providers',
'configuration/database',
'configuration/pages',
'configuration/callbacks'
'configuration/callbacks',
'configuration/events'
],
'Authentication Providers': [
'providers/apple',
'providers/auth0',
'providers/battle.net',
'providers/box',
'providers/cognito',
'providers/discord',
'providers/email',
'providers/credentials',

View File

@@ -107,6 +107,10 @@ html[data-theme='dark'] .hero {
background: linear-gradient(0deg, rgba(222,222,222,0.075) 0%, rgba(255,255,255,0) 100%);
}
.hero .container {
margin-bottom: 2rem;
}
.hero .hero__title {
font-size: 3rem;
margin-bottom: 0.5rem;
@@ -128,10 +132,6 @@ html[data-theme='dark'] .hero {
.hero .hero__subtitle {
font-size: 1.5rem;
}
.hero .container {
margin-bottom: 2rem;
}
}
.home-subtitle {

View File

@@ -109,7 +109,7 @@ function Home () {
<section className={`section-features ${styles.features}`}>
<div className='container'>
<h2 className='text--center'>
Full stack open source authentication
Open Source Authentication
</h2>
<div className='row'>
{features.map((props, idx) => (
@@ -150,8 +150,8 @@ function Home () {
<p className='text--center'>
<Link
to='/getting-started/example'
className='button button--secondary button--ouline button--lg rounded-pill'
>Example Code
className='button button--primary button--ouline button--lg rounded-pill'
>View Example Code
</Link>
</p>
</div>
@@ -197,7 +197,7 @@ import NextAuth from 'next-auth'
import Providers from 'next-auth/providers'
const options = {
site: 'https://example.com'
site: 'https://example.com',
providers: [
// OAuth authentication providers
Providers.Apple({

View File

@@ -57,6 +57,7 @@
.features h2 {
font-size: 2rem;
line-height: 3rem;
margin: 2rem 0 4rem 0;
}