mirror of
https://github.com/SrIzan10/next-auth.git
synced 2026-05-01 10:55:20 +00:00
Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a3479b3503 | ||
|
|
740535a8f2 | ||
|
|
19ed684a52 | ||
|
|
bd72949fa7 | ||
|
|
a277cd5b0c | ||
|
|
fd6e7e94df | ||
|
|
2f6403478d | ||
|
|
a4372ffc61 | ||
|
|
d6ce92811e | ||
|
|
e5aecdf315 | ||
|
|
6d1c457a75 | ||
|
|
6e16aec6d3 | ||
|
|
f899d7bb04 | ||
|
|
e36646ce7f | ||
|
|
f3d36a74c9 | ||
|
|
4e11c9c36e | ||
|
|
0a7ac36584 | ||
|
|
fc4850f354 | ||
|
|
6e9a8d2074 | ||
|
|
c712d7da07 | ||
|
|
5183181d1c | ||
|
|
b024f89ba8 | ||
|
|
fbbe516b9a | ||
|
|
d48a3fd948 | ||
|
|
86f0c53bd3 | ||
|
|
7f6cc2048b | ||
|
|
b2829f6384 | ||
|
|
67c5041860 | ||
|
|
33df9e3132 | ||
|
|
602bc28a45 | ||
|
|
5a7a494701 | ||
|
|
9fa82cedbd | ||
|
|
b0408284b8 |
29
.github/workflows/node.js.yml
vendored
Normal file
29
.github/workflows/node.js.yml
vendored
Normal 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
36
.github/workflows/npm-publish.yml
vendored
Normal 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
2
package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "next-auth",
|
||||
"version": "2.0.0",
|
||||
"version": "2.1.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
29
src/providers/battlenet.js
Normal file
29
src/providers/battlenet.js
Normal 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
23
src/providers/cognito.js
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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, '​.')}`
|
||||
const escapedSite = `${site.replace(/\./g, '​.')}`
|
||||
|
||||
// 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`
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
|
||||
@@ -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) => { }
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
23
www/docs/configuration/events.md
Normal file
23
www/docs/configuration/events.md
Normal 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.
|
||||
@@ -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).
|
||||
:::
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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.
|
||||
:::
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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`
|
||||
:::
|
||||
27
www/docs/providers/battlenet.md
Normal file
27
www/docs/providers/battlenet.md
Normal 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
|
||||
})
|
||||
}
|
||||
...
|
||||
```
|
||||
@@ -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`
|
||||
|
||||
35
www/docs/providers/cognito.md
Normal file
35
www/docs/providers/cognito.md
Normal 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.
|
||||
:::
|
||||
|
||||

|
||||
@@ -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: {
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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, '​.')}`
|
||||
const escapedSite = `${site.replace(/\./g, '​.')}`
|
||||
|
||||
// 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
|
||||
:::
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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.
|
||||
:::
|
||||
@@ -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.
|
||||
:::
|
||||
@@ -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`
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -57,6 +57,7 @@
|
||||
|
||||
.features h2 {
|
||||
font-size: 2rem;
|
||||
line-height: 3rem;
|
||||
margin: 2rem 0 4rem 0;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user