Files
archived-next-auth/index.js
Richard Lewis ee10fcd37e Allow customisation of session cookie name and sameSite value.
Allow customisation of session cookie name and sameSite value.
2020-05-09 02:28:51 +01:00

479 lines
17 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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)
})
})
}