mirror of
https://github.com/SrIzan10/next-auth.git
synced 2026-05-01 10:55:20 +00:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a3479b3503 | ||
|
|
740535a8f2 | ||
|
|
19ed684a52 | ||
|
|
bd72949fa7 | ||
|
|
a277cd5b0c | ||
|
|
fd6e7e94df | ||
|
|
2f6403478d | ||
|
|
a4372ffc61 | ||
|
|
d6ce92811e | ||
|
|
e5aecdf315 | ||
|
|
6d1c457a75 | ||
|
|
6e16aec6d3 |
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
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "next-auth",
|
||||
"version": "2.1.0",
|
||||
"version": "2.2.0",
|
||||
"description": "An authentication library for Next.js",
|
||||
"repository": "https://github.com/iaincollins/next-auth.git",
|
||||
"author": "Iain Collins <me@iaincollins.com>",
|
||||
@@ -12,7 +12,7 @@
|
||||
"watch": "npm run watch:js | npm run watch:css",
|
||||
"watch:js": "babel --watch src --out-dir dist",
|
||||
"watch:css": "postcss --watch src/**/*.css --base src --dir dist",
|
||||
"test": "npm run test:db",
|
||||
"test": "npm run lint",
|
||||
"test:db": "npm run test:db:mysql && npm run test:db:postgres && npm run test:db:mongodb",
|
||||
"test:db:mysql": "node test/mysql.js",
|
||||
"test:db:postgres": "node test/postgres.js",
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -32,7 +32,7 @@ export default async (req, res, userSuppliedOptions) => {
|
||||
nextauth,
|
||||
action = nextauth[0],
|
||||
provider = nextauth[1],
|
||||
error
|
||||
error = nextauth[1]
|
||||
} = query
|
||||
|
||||
const {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -17,7 +17,7 @@ function render (req, res, page, props, done) {
|
||||
html = verifyRequest(props)
|
||||
break
|
||||
case 'error':
|
||||
html = error(props)
|
||||
html = error({ ...props, res })
|
||||
break
|
||||
default:
|
||||
html = error(props)
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ NextAuth.js is designed to work with any OAuth service, it supports OAuth 1.0, 1
|
||||
|
||||
4. Now you can add the provider settings to the NextAuth options object. You can add as many OAuth providers as you like, as you can see `providers` is an array.
|
||||
|
||||
```js title="/pages/api/auth/[...nextauth].js"
|
||||
```js title="pages/api/auth/[...nextauth].js"
|
||||
...
|
||||
providers: [
|
||||
Providers.Twitter({
|
||||
@@ -110,7 +110,7 @@ As an example of what this looks like, this is the the provider object returned
|
||||
```
|
||||
You can replace all the options in this JSON object with the ones from your custom provider – be sure to give it a unique ID and specify the correct OAuth version - and add it to the providers option:
|
||||
|
||||
```js title="/pages/api/auth/[...nextauth].js"
|
||||
```js title="pages/api/auth/[...nextauth].js"
|
||||
...
|
||||
providers: [
|
||||
Providers.Twitter({
|
||||
@@ -160,7 +160,7 @@ Adding support for signing in via email in addition to one or more OAuth service
|
||||
|
||||
Configuration is similar to other providers, but the options are different:
|
||||
|
||||
```js title="/pages/api/auth/[...nextauth].js"
|
||||
```js title="pages/api/auth/[...nextauth].js"
|
||||
providers: [
|
||||
Providers.Email({
|
||||
server: process.env.EMAIL_SERVER,
|
||||
@@ -183,7 +183,7 @@ The Credentials provider allows you to handle signing in with arbitrary credenti
|
||||
|
||||
It is intended to support use cases where you have an existing system you need to authenticate users against.
|
||||
|
||||
```js title="/pages/api/auth/[...nextauth].js"
|
||||
```js title="pages/api/auth/[...nextauth].js"
|
||||
import Providers from `next-auth/providers`
|
||||
...
|
||||
providers: [
|
||||
|
||||
@@ -8,7 +8,7 @@ The NextAuth.js client library makes it easy to interact with sessions from Reac
|
||||
Some of the methods can be called both client side and server side.
|
||||
|
||||
:::note
|
||||
To use client methods server side in `getServerSideProp()` or `getInitialProps()` you should add the NextAuth.js `<Provider>` in `pages/apps.js`
|
||||
To use client methods server side in `getServerSideProp()` or `getInitialProps()` you should add the NextAuth.js `<Provider>` in `pages/app.js`
|
||||
|
||||
```jsx title="pages/_app.js"
|
||||
import { Provider } from 'next-auth/client'
|
||||
@@ -187,7 +187,7 @@ e.g.
|
||||
* `signin('google', { callbackUrl: 'http://localhost:3000/foo' })`
|
||||
* `signin('email', { email, callbackUrl: 'http://localhost:3000/foo' })`
|
||||
|
||||
The URL must be considered valid by the [redirect callback handler](http://localhost:3000/configuration/callbacks#redirect). By default this means it must be an absolute URL at the same hostname (or else it will default to the homepage); you can define your own custom redirect callback to allow other URLs, including supporting relative URLs.
|
||||
The URL must be considered valid by the [redirect callback handler](/configuration/callbacks#redirect). By default this means it must be an absolute URL at the same hostname (or else it will default to the homepage); you can define your own custom redirect callback to allow other URLs, including supporting relative URLs.
|
||||
|
||||
:::tip
|
||||
To also support signing in from clients that do not have client side JavaScript, use a regular link, add an onClick handler to it and call **e.preventDefault()** before calling the **signin()** method.
|
||||
@@ -218,7 +218,7 @@ As with the `signin()` function, you can specify a `callbackUrl` parameter by pa
|
||||
|
||||
e.g. `signout{ callbackUrl: 'http://localhost:3000/foo' })`
|
||||
|
||||
The URL must be considered valid by the [redirect callback handler](http://localhost:3000/configuration/callbacks#redirect). By default this means it must be an absolute URL at the same hostname (or else it will default to the homepage); you can define your own custom redirect callback to allow other URLs, including supporting relative URLs.
|
||||
The URL must be considered valid by the [redirect callback handler](/configuration/callbacks#redirect). By default this means it must be an absolute URL at the same hostname (or else it will default to the homepage); you can define your own custom redirect callback to allow other URLs, including supporting relative URLs.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -50,17 +50,14 @@ providers: [
|
||||
password: { label: "Password", type: "password" }
|
||||
},
|
||||
authorize: async (credentials) => {
|
||||
const user = (credentials) => {
|
||||
// You need to provide your own logic here that takes the credentials
|
||||
// submitted and returns either a object representing a user or value
|
||||
// that is false/null if the credentials are invalid.
|
||||
// e.g. return { id: 1, name: 'J Smith', email: 'jsmith@example.com' }
|
||||
return null
|
||||
}
|
||||
// Add logic here to look up the user from the credentials supplied
|
||||
const user = { id: 1, name: 'J Smith', email: 'jsmith@example.com' }
|
||||
|
||||
if (user) {
|
||||
// Any user object returned here will be saved in the JSON Web Token
|
||||
// Any object returned will be saved in `user` property of the JWT
|
||||
return Promise.resolve(user)
|
||||
} else {
|
||||
// If you return null or false then the credentials will be rejected
|
||||
return Promise.resolve(null)
|
||||
}
|
||||
}
|
||||
@@ -69,24 +66,6 @@ providers: [
|
||||
...
|
||||
```
|
||||
|
||||
To use your new credentials provider, you will need to create a form that posts back to `/api/auth/callback/credentials`.
|
||||
|
||||
All form parameters submitted will be passed as `credentials` to your `authorize` callback.
|
||||
|
||||
```js title="pages/signin"
|
||||
import React from 'react'
|
||||
|
||||
export default () => {
|
||||
return (
|
||||
<form method='post' action='/api/auth/callback/credentials'>
|
||||
<input name='email' type='text' defaultValue='' />
|
||||
<input name='password' type='password' defaultValue='' />
|
||||
<button type='submit'>Sign in</button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
As the JSON Web Token is encrypted, you can safely store user credentials in it and revalidate them whenever an action is performed. See the [callbacks documentation](/configuration/callbacks) for more information on how to interact with the token.
|
||||
|
||||
## With multiple providers
|
||||
@@ -105,7 +84,7 @@ As with all providers, the order you specify them in, is the order they are disp
|
||||
id: 'domain-login',
|
||||
name: "Domain Account",
|
||||
authorize: async (credentials) => {
|
||||
const user = (credentials) => { /* add function to get user */ }
|
||||
const user = { /* add function to get user */ }
|
||||
return Promise.resolve(user)
|
||||
},
|
||||
credentials: {
|
||||
@@ -118,7 +97,7 @@ As with all providers, the order you specify them in, is the order they are disp
|
||||
id: 'intranet-credentials',
|
||||
name: "Two Factor Auth",
|
||||
authorize: async (credentials) => {
|
||||
const user = (credentials) => { /* add function to get user */ }
|
||||
const user = { /* add function to get user */ }
|
||||
return Promise.resolve(user)
|
||||
},
|
||||
credentials: {
|
||||
|
||||
@@ -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
|
||||
:::
|
||||
|
||||
Reference in New Issue
Block a user