mirror of
https://github.com/SrIzan10/next-auth.git
synced 2026-05-01 10:55:20 +00:00
479 lines
17 KiB
JavaScript
479 lines
17 KiB
JavaScript
'use strict'
|
||
|
||
const BodyParser = require('body-parser')
|
||
const Express = require('express')
|
||
const ExpressSession = require('express-session')
|
||
const lusca = require('lusca')
|
||
const passportStrategies = require('./src/passport-strategies')
|
||
const uuid = require('uuid/v4')
|
||
|
||
module.exports = (nextApp, {
|
||
bodyParser = true,
|
||
// Optional, allows you to set e.g. 'limit' (maximum request body size, default 100kb)
|
||
bodyParserJsonOptions = {},
|
||
bodyParserUrlEncodedOptions = { extended: true },
|
||
csrf = true,
|
||
// URL base path for authentication routes (optional).
|
||
// Note: The prefix value of '/auth' is currently hard coded in
|
||
// next-auth-client so you should not change this unless you also modify it.
|
||
pathPrefix = '/auth',
|
||
// Express Server (optional).
|
||
expressApp = null,
|
||
// Express Session (optional).
|
||
expressSession = ExpressSession,
|
||
// Secret used to encrypt session data on the server.
|
||
sessionSecret = 'change-me',
|
||
// Session store for express-session.
|
||
// Defaults to an in memory store, which is not recommended for production.
|
||
sessionStore = expressSession.MemoryStore(),
|
||
// The name of the session ID cookie to set in the response (and read from in
|
||
// the request). The default value is 'connect.sid'.
|
||
sessionCookie = 'connect.sid',
|
||
// Maximum Session Age in ms (optional, default is 7 days).
|
||
// The expiry time for a session is reset every time a user revisits the site
|
||
// or revalidates their session token - this is the maximum idle time value.
|
||
sessionMaxAge = 60000 * 60 * 24 * 7,
|
||
// The session cookie name. Useful for adding cookie prefixes. E.g. setting
|
||
// '__HOST-' and '__SECURE-' prefixes on cookie names prevents them from being
|
||
// overwritten by insecure origins.
|
||
sessionName = null,
|
||
// Session Revalidation in X ms (optional, default is 60 seconds).
|
||
// Specifies how often a Single Page App should revalidate a session.
|
||
// Does not impact the session life on the server, but causes clients to
|
||
// refetch session info (even if it is in a local cache) after N seconds has
|
||
// elapsed since it was last checked so they always display state correctly.
|
||
// If set to 0 will revalidate a session before rendering every page.
|
||
sessionRevalidateAge = 60000,
|
||
// Force the session to be saved back to the session store, even if the
|
||
// session was not modified during the request.
|
||
// Note: If this is false, session expiry will not rotate and will expire
|
||
// after sessionMaxAge unless you write you own code to rotate the session.
|
||
// This is an option exposed for advanced use cases on people with specific
|
||
// databases that have session store drivers that do not work well with
|
||
// the express-session resave option.
|
||
// https://www.npmjs.com/package/express-session#resave
|
||
sessionResave = true,
|
||
// Force a session identifier cookie to be set on every response. The expire
|
||
// time is reset to the original maxAge, resetting the expiration time.
|
||
// Note When this option is set to true but the saveUninitialized option
|
||
// is set to false, the cookie will not be set on a response with an
|
||
// uninitialized session https://www.npmjs.com/package/express-session#rolling
|
||
sessionRolling = true,
|
||
// Prevent cookies from being sent cross-site, protecting against CSRF
|
||
// attacks.
|
||
sessionSameSite = null,
|
||
// Forces a session that is "uninitialized" to be saved to the store.
|
||
// A session is uninitialized when it is new but not modified. Choosing false
|
||
// is useful for implementing login sessions, reducing server storage usage,
|
||
// or complying with laws that require permission before setting a cookie.
|
||
//
|
||
// Choosing false will also help with race conditions where a client makes
|
||
// multiple parallel requests without a session.
|
||
//
|
||
// Note that if the build in CSRF protection is enabled (the default) then
|
||
// sessions will ALWAYS be 'initialized' as it saves to the session.
|
||
// https://www.npmjs.com/package/express-session#saveuninitialized
|
||
sessionSaveUninitialized = false,
|
||
// Canonical URL of the server (optional, but recommended).
|
||
// e.g. 'http://localhost:3000' or 'https://www.example.com'
|
||
// Used in callbak URLs and email sign in links. It will be auto generated
|
||
// if not specified, which may cause problems if your site uses multiple
|
||
// aliases (e.g. 'example.com and 'www.examples.com').
|
||
serverUrl = null,
|
||
// If we are behind a proxy server and it says we are running SSL, trust it.
|
||
// All this does is make sure we use HTTPS for callback URLs and email links.
|
||
// You should never need to turn this off.
|
||
trustProxy = true,
|
||
// An array of oAuth Provider config blocks (optional).
|
||
providers = [],
|
||
// Port to start listening on
|
||
port = null,
|
||
// Functions for find, update, insert, serialize and deserialize methods.
|
||
// They should all return a Promise with resolve() or reject().
|
||
// find() should return resolve(null) if no matching user found.
|
||
functions = {
|
||
find: ({
|
||
id,
|
||
email,
|
||
emailToken,
|
||
provider // provider = { name: 'twitter', id: '123456' }
|
||
} = {}) => { Promise.resolve(user) },
|
||
update: (user, profile) => { Promise.resolve(user) },
|
||
insert: (user, profile) => { Promise.resolve(user) },
|
||
remove: (id) => { Promise.resolve(id) },
|
||
serialize: (user) => { Promise.resolve(id) },
|
||
deserialize: (id) => { Promise.resolve(user) },
|
||
session: null,
|
||
sendSignInEmail: null, /* ({
|
||
email = null,
|
||
url = null,
|
||
req = null
|
||
} = {}) => { Promise.resolve(true) }
|
||
*/
|
||
signIn: null /* ({
|
||
email = null,
|
||
password = null
|
||
} = {}) => { Promise.resolve(user) }
|
||
*/
|
||
}
|
||
} = {}) => {
|
||
|
||
if (typeof(functions) !== 'object') {
|
||
throw new Error('functions must be a an object')
|
||
}
|
||
|
||
if (expressApp === null) {
|
||
expressApp = Express()
|
||
}
|
||
|
||
// If an instance of nextApp was passed, let all requests to /_next/* pass
|
||
// to it *before* Express Session and other middleware is called.
|
||
if (nextApp) {
|
||
expressApp.all('/_next/*', (req, res) => {
|
||
let nextRequestHandler = nextApp.getRequestHandler()
|
||
return nextRequestHandler(req, res)
|
||
})
|
||
}
|
||
|
||
/*
|
||
* Set up body parsing, express sessions and add CSRF tokens.
|
||
*
|
||
* You can set bodyParser to false and pass an Express instance if you want
|
||
* to customise how you invoke Body Parser.
|
||
*/
|
||
if (bodyParser === true) {
|
||
expressApp.use(BodyParser.json(bodyParserJsonOptions))
|
||
expressApp.use(BodyParser.urlencoded(bodyParserUrlEncodedOptions))
|
||
}
|
||
expressApp.use(expressSession({
|
||
name: sessionName,
|
||
secret: sessionSecret,
|
||
store: sessionStore,
|
||
resave: sessionResave,
|
||
rolling: sessionRolling,
|
||
saveUninitialized: sessionSaveUninitialized,
|
||
cookie: {
|
||
name: sessionCookie,
|
||
httpOnly: true,
|
||
secure: 'auto',
|
||
maxAge: sessionMaxAge,
|
||
sameSite: sessionSameSite,
|
||
}
|
||
}))
|
||
|
||
if (csrf === true) {
|
||
// If csrf is true (default) apply to all routes
|
||
expressApp.use(lusca.csrf())
|
||
} else if (csrf !== false) {
|
||
// If csrf is anything else (except false) then pass it as a config option
|
||
expressApp.use(lusca.csrf(csrf))
|
||
} // if csrf is explicitly set to false then doesn't apply CSRF at all
|
||
|
||
if (trustProxy === true) {
|
||
expressApp.set('trust proxy', 1)
|
||
}
|
||
|
||
/*
|
||
* With sessions configured we need to configure Passport and trigger
|
||
* passport.initialize() before we add any other routes.
|
||
*/
|
||
passportStrategies({
|
||
expressApp: expressApp,
|
||
serverUrl: serverUrl,
|
||
providers: providers,
|
||
functions: functions
|
||
})
|
||
|
||
/*
|
||
* Add route to get CSRF token via AJAX
|
||
*/
|
||
expressApp.get(`${pathPrefix}/csrf`, (req, res) => {
|
||
return res.json({
|
||
csrfToken: res.locals._csrf
|
||
})
|
||
})
|
||
|
||
/*
|
||
* Return session info to client
|
||
*
|
||
* Will be stored in local storage, so should not return sensitive data that
|
||
* could be captured in a Cross Site Scripting attack (i.e. so not the session
|
||
* token) – or anything you don't want users to see (like private IDs) but is
|
||
* is okay to return things like access tokens for acessing remote services.
|
||
*/
|
||
expressApp.get(`${pathPrefix}/session`, (req, res) => {
|
||
let session = {
|
||
maxAge: sessionMaxAge,
|
||
revalidateAge: sessionRevalidateAge,
|
||
csrfToken: res.locals._csrf
|
||
}
|
||
|
||
if (req.user)
|
||
session.user = req.user
|
||
|
||
if (functions.session)
|
||
session = functions.session(session, req)
|
||
|
||
return res.json(session)
|
||
})
|
||
|
||
// Server side function for returning list of accounts user has linked.
|
||
// Called when pages are rendered in on the server (instead of /auth/linked).
|
||
// Returns all accounts the user has linked (e.g. Twitter, Facebook, Google…)
|
||
expressApp.use((req, res, next) => {
|
||
req.linked = () => {
|
||
return new Promise((resolve, reject) => {
|
||
if (!req.user) return resolve({})
|
||
|
||
functions.serialize(req.user)
|
||
.then(id => {
|
||
if (!id) throw new Error("Unable to serialize user")
|
||
return functions.find({ id: id })
|
||
})
|
||
.then(user => {
|
||
if (!user) return resolve({})
|
||
|
||
let linkedAccounts = {}
|
||
providers.forEach(provider => {
|
||
linkedAccounts[provider.providerName] = (user[provider.providerName.toLowerCase()]) ? true : false
|
||
})
|
||
|
||
return resolve(linkedAccounts)
|
||
})
|
||
.catch(err => {
|
||
return reject(err)
|
||
})
|
||
})
|
||
}
|
||
next()
|
||
})
|
||
|
||
// Client side REST endpoint for returning list of accounts user has linked.
|
||
// Called when pages are rendered in the browser (instead of req.linked()).
|
||
// Returns all accounts the user has linked (e.g. Twitter, Facebook, Google…)
|
||
expressApp.get(`${pathPrefix}/linked`, (req, res) => {
|
||
if (!req.user) return res.json({})
|
||
|
||
// First get the User ID from the User, then look up the user details.
|
||
// Note: We don't use the User object in req.user directly as it is a
|
||
// a simplified set of properties set by functions.deserialize().
|
||
functions.serialize(req.user)
|
||
.then(id => {
|
||
return functions.find({ id: id })
|
||
})
|
||
.then(user => {
|
||
if (!user) return res.json({})
|
||
|
||
let linkedAccounts = {}
|
||
providers.forEach(provider => {
|
||
linkedAccounts[provider.providerName] = (user[provider.providerName.toLowerCase()]) ? true : false
|
||
})
|
||
|
||
return res.json(linkedAccounts)
|
||
})
|
||
.catch(err => {
|
||
return res.status(500).end()
|
||
})
|
||
})
|
||
|
||
/*
|
||
* Return list of configured oAuth Providers
|
||
*
|
||
* We define this both as a server side function and a RESTful endpoint so
|
||
* that it can be used rendering a page both server side and client side.
|
||
*/
|
||
// Server side function
|
||
expressApp.use((req, res, next) => {
|
||
req.providers = () => {
|
||
return new Promise((resolve, reject) => {
|
||
let configuredProviders = {}
|
||
providers.forEach(provider => {
|
||
configuredProviders[provider.providerName] = {
|
||
signin: (serverUrl || '') + `${pathPrefix}/oauth/${provider.providerName.toLowerCase()}`,
|
||
callback: (serverUrl || '') + `${pathPrefix}/oauth/${provider.providerName.toLowerCase()}/callback`
|
||
}
|
||
})
|
||
return resolve(configuredProviders)
|
||
})
|
||
}
|
||
next()
|
||
})
|
||
// RESTful endpoint
|
||
expressApp.get(`${pathPrefix}/providers`, (req, res) => {
|
||
return new Promise((resolve, reject) => {
|
||
let configuredProviders = {}
|
||
providers.forEach(provider => {
|
||
configuredProviders[provider.providerName] = {
|
||
signin: (serverUrl || '') + `${pathPrefix}/oauth/${provider.providerName.toLowerCase()}`,
|
||
callback: (serverUrl || '') + `${pathPrefix}/oauth/${provider.providerName.toLowerCase()}/callback`
|
||
}
|
||
})
|
||
return res.json(configuredProviders)
|
||
})
|
||
})
|
||
|
||
|
||
/*
|
||
* Enable /auth/signin routes if signIn() function is passed
|
||
*/
|
||
if (functions.signIn) {
|
||
expressApp.post(`${pathPrefix}/signin`, (req, res) => {
|
||
// Passes all supplied credentials to the signIn function
|
||
functions.signIn({
|
||
form: req.body,
|
||
req: req
|
||
})
|
||
.then(user => {
|
||
if (user) {
|
||
// If signIn() returns a user, sign in as them
|
||
req.logIn(user, (err) => {
|
||
if (err) return res.redirect(`${pathPrefix}/error?action=signin&type=credentials`)
|
||
if (req.xhr) {
|
||
// If AJAX request (from client with JS), return JSON response
|
||
return res.json({success: true})
|
||
} else {
|
||
// If normal form POST (from client without JS) return redirect
|
||
return res.redirect(`${pathPrefix}/callback?action=signin&service=credentials`)
|
||
}
|
||
})
|
||
} else {
|
||
// If no user object is returned, bounce back to the sign in page
|
||
return res.redirect(`${pathPrefix}`)
|
||
}
|
||
})
|
||
.catch(err => {
|
||
return res.redirect(`${pathPrefix}/error?action=signin&type=credentials`)
|
||
})
|
||
})
|
||
}
|
||
|
||
/*
|
||
* Enable /auth/email/signin routes if sendSignInEmail() function is passed
|
||
*/
|
||
if (functions.sendSignInEmail) {
|
||
/*
|
||
* Generate a one time use sign in link and email it to the user
|
||
*/
|
||
expressApp.post(`${pathPrefix}/email/signin`, (req, res) => {
|
||
const email = req.body.email || null
|
||
|
||
if (!email || email.trim() === '') {
|
||
res.redirect(`${pathPrefix}`)
|
||
}
|
||
|
||
const token = uuid()
|
||
const url = (serverUrl || `${req.protocol}://${req.headers.host}`) + `${pathPrefix}/email/signin/${token}`
|
||
|
||
// Create verification token save it to database
|
||
functions.find({ email: email })
|
||
.then(user => {
|
||
if (user) {
|
||
// If a user with that email address exists already, update token.
|
||
user.emailToken = token
|
||
return functions.update(user)
|
||
} else {
|
||
// If the user does not exist, create a new account with the token.
|
||
return functions.insert({
|
||
email: email,
|
||
emailToken: token
|
||
})
|
||
}
|
||
})
|
||
.then(user => {
|
||
functions.sendSignInEmail({
|
||
email: user.email,
|
||
url: url,
|
||
req: req
|
||
})
|
||
if (req.xhr) {
|
||
// If AJAX request (from client with JS), return JSON response
|
||
return res.json({success: true})
|
||
} else {
|
||
// If normal form POST (from client without JS) return redirect
|
||
return res.redirect(`${pathPrefix}/check-email?email=${email}`)
|
||
}
|
||
})
|
||
.catch(err => {
|
||
return res.redirect(`${pathPrefix}/error?action=signin&type=email&email=${email}`)
|
||
})
|
||
})
|
||
|
||
/*
|
||
* Verify token in callback URL for email sign in
|
||
*/
|
||
expressApp.get(`${pathPrefix}/email/signin/:token`, (req, res) => {
|
||
if (!req.params.token) {
|
||
return res.redirect(`${pathPrefix}/error?action=signin&type=token-missing`)
|
||
}
|
||
|
||
functions.find({ emailToken: req.params.token })
|
||
.then(user => {
|
||
if (user) {
|
||
// Delete current token so it cannot be used again
|
||
delete user.emailToken
|
||
// Mark email as verified now we know they have access to it
|
||
user.emailVerified = true
|
||
return functions.update(user, null, { delete: 'emailToken' })
|
||
} else {
|
||
return Promise.reject(new Error("Token not valid"))
|
||
}
|
||
})
|
||
.then(user => {
|
||
// If the user object is valid, sign the user in
|
||
req.logIn(user, (err) => {
|
||
if (err) return res.redirect(`${pathPrefix}/error?action=signin&type=token-invalid`)
|
||
if (req.xhr) {
|
||
// If AJAX request (from client with JS), return JSON response
|
||
return res.json({success: true})
|
||
} else {
|
||
// If normal form POST (from client without JS) return redirect
|
||
return res.redirect(`${pathPrefix}/callback?action=signin&service=email`)
|
||
}
|
||
})
|
||
})
|
||
.catch(err => {
|
||
return res.redirect(`${pathPrefix}/error?action=signin&type=token-invalid`)
|
||
})
|
||
})
|
||
}
|
||
|
||
/*
|
||
* Sign a user out
|
||
*/
|
||
expressApp.post(`${pathPrefix}/signout`, (req, res) => {
|
||
// Log user out with Passport and remove their Express session
|
||
req.logout()
|
||
req.session.destroy(() => {
|
||
return res.redirect(`${pathPrefix}/callback?action=signout`)
|
||
})
|
||
})
|
||
|
||
return new Promise((resolve, reject) => {
|
||
let response = {
|
||
next: nextApp,
|
||
express: Express,
|
||
expressApp: expressApp,
|
||
functions: functions,
|
||
providers: providers,
|
||
port: port
|
||
}
|
||
|
||
// If no port specified, don't start Express automatically
|
||
if (!port) return resolve(response)
|
||
|
||
// If an instance of nextApp was passed, have it handle all other routes
|
||
if (nextApp) {
|
||
expressApp.all('*', (req, res) => {
|
||
let nextRequestHandler = nextApp.getRequestHandler()
|
||
return nextRequestHandler(req, res)
|
||
})
|
||
}
|
||
|
||
// Start Express
|
||
return expressApp.listen(port, err => {
|
||
if (err) reject(err)
|
||
return resolve(response)
|
||
})
|
||
})
|
||
}
|