Initial commit of next-auth and example

This commit is contained in:
Iain Collins
2018-01-27 12:37:30 +00:00
parent cc0e2d9366
commit a37fc97a60
25 changed files with 64350 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
.env
node_modules

13
example/.env.example Normal file
View File

@@ -0,0 +1,13 @@
SERVER_URL=http://localhost:3000
MONGO_URI=mongodb://localhost:27017/my-database
FACEBOOK_ID=
FACEBOOK_SECRET=
GOOGLE_ID=
GOOGLE_SECRET=
TWITTER_KEY=
TWITTER_SECRET=
EMAIL_FROM=username@gmail.com
EMAIL_SERVER=smtp.gmail.com
EMAIL_PORT=465
EMAIL_USERNAME=username@gmail.com
EMAIL_PASSWORD=

3
example/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
.env
/.env.production
node_modules

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

15406
example/.next/commons.js Normal file

File diff suppressed because one or more lines are too long

4
example/.next/dist/pages/_document.js vendored Normal file
View File

@@ -0,0 +1,4 @@
'use strict';
module.exports = require('next/dist/server/document.js');
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIm5vZGVfbW9kdWxlcy9uZXh0L2Rpc3QvcGFnZXMvX2RvY3VtZW50LmpzIl0sIm5hbWVzIjpbIm1vZHVsZSIsImV4cG9ydHMiLCJyZXF1aXJlIl0sIm1hcHBpbmdzIjoiOztBQUFBLE9BQU8sQUFBUCxVQUFpQixBQUFqQiIsImZpbGUiOiJfZG9jdW1lbnQuanM/ZW50cnkiLCJzb3VyY2VSb290IjoiL1VzZXJzL2lhaW4vRGV2ZWxvcG1lbnQvbmV4dC1hdXRoL2V4YW1wbGUifQ==

4
example/.next/dist/pages/_error.js vendored Normal file
View File

@@ -0,0 +1,4 @@
'use strict';
module.exports = require('next/dist/lib/error.js');
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIm5vZGVfbW9kdWxlcy9uZXh0L2Rpc3QvcGFnZXMvX2Vycm9yLmpzIl0sIm5hbWVzIjpbIm1vZHVsZSIsImV4cG9ydHMiLCJyZXF1aXJlIl0sIm1hcHBpbmdzIjoiOztBQUFBLE9BQU8sQUFBUCxVQUFpQixBQUFqQiIsImZpbGUiOiJfZXJyb3IuanM/ZW50cnkiLCJzb3VyY2VSb290IjoiL1VzZXJzL2lhaW4vRGV2ZWxvcG1lbnQvbmV4dC1hdXRoL2V4YW1wbGUifQ==

451
example/.next/dist/pages/auth/index.js vendored Normal file

File diff suppressed because one or more lines are too long

19359
example/.next/main.js Normal file

File diff suppressed because one or more lines are too long

817
example/.next/manifest.js Normal file

File diff suppressed because one or more lines are too long

43
example/index.js Normal file
View File

@@ -0,0 +1,43 @@
/**
* Simple example of how to use the NextAuth module.
*
* To invoke next-auth you will need to define a configuration block for your
* site. You can create a next-auth.config.js file and put all your options
* in it and pass it to next-auth when calling init().
*
* A number of sample configuration files for various databases and
* authentication options are provided.
**/
// Include Next.js, Next Auth and a Next Auth config
const next = require('next')
const nextAuth = require('next-auth')
const nextAuthConfig = require('./next-auth.config')
// Load environment variables from .env
require('dotenv').load()
// Initialize Next.js
const nextApp = next({
dir: '.',
dev: (process.env.NODE_ENV === 'development')
})
// Add next-auth to next app
nextApp
.prepare()
.then(() => {
// Load configuration and return config object
return nextAuthConfig()
})
.then(nextAuthOptions => {
// Pass Next.js App instance and NextAuth options to NextAuth
return nextAuth(nextApp, nextAuthOptions)
})
.then((response) => {
console.log(`Ready on http://localhost:${process.env.PORT || 3000}`)
})
.catch(err => {
console.log('An error occurred, unable to start the server')
console.log(err)
})

View File

@@ -0,0 +1,66 @@
/**
* next-auth.config.js Example
*
* Environment variables for this example:
*
* PORT=3000
* SERVER_URL=http://localhost:3000
* MONGO_URI=mongodb://localhost:27017/my-database
* EMAIL_FROM=username@gmail.com
* EMAIL_SERVER=smtp.gmail.com
* EMAIL_PORT=465
* EMAIL_USERNAME=username@gmail.com
* EMAIL_PASSWORD=p4ssw0rd
*
* If you wish, you can put these in a `.env` to seperate your environment
* specific configuration from your code.
**/
// Load environment variables from a .env file if one exists
require('dotenv').load()
const nextAuthProviders = require('./next-auth.providers')
const nextAuthFunctions = require('./next-auth.functions')
// If we want to pass a custom session store then we also need to pass an
// instance of Express Session along with it.
const expressSession = require('express-session')
const MongoStore = require('connect-mongo')(expressSession)
// If no store set, NextAuth defaults to using Express Sessions in-memory
// session store (the fallback is intended as fallback for testing only).
let sessonStore
if (process.env.MONGO_URI) {
sessonStore = new MongoStore({
url: process.env.MONGO_URI,
autoRemove: 'interval',
autoRemoveInterval: 10, // Removes expired sessions every 10 minutes
collection: 'sessions',
stringify: false
})
}
module.exports = () => {
// We connect to the User DB before we define our functions.
// next-auth.functions.js returns an async method that does that and returns
// an object with the functions needed for authentication.
return nextAuthFunctions()
.then(functions => {
return new Promise((resolve, reject) => {
// This is the config block we return, ready to be passed to NextAuth
resolve({
// Define a port (if none passed, will not start Express)
port: process.env.PORT || 3000,
// Set canonical URL (optional)
serverUrl: process.env.SERVER_URL || null,
// Add an Express Session store.
expressSession: expressSession,
sessionStore: sessonStore,
// Define oAuth Providers
providers: nextAuthProviders(),
// Add functions for finding, inserting and updating users.
functions: functions
})
})
})
}

View File

@@ -0,0 +1,179 @@
/**
* next-auth.functions.js Example
*
* This file defines functions NextAuth to look up, add and update users.
*
* It returns a Promise with the functions matching these signatures:
*
* {
* find: ({
* id,
* email,
* emailToken,
* provider,
* poviderToken
* } = {}) => {},
* update: (user) => {},
* insert: (user) => {},
* serialize: (user) => {},
* deserialize: (id) => {}
* }
*
* Each function returns Promise.resolve() - or Promise.reject() on error.
*
* This specific example supports both MongoDB and NeDB, but can be refactored
* to work with any database.
*
* Environment variables for this example:
*
* MONGO_URI=mongodb://localhost:27017/my-database
*
* If you wish, you can put these in a `.env` to seperate your environment
* specific configuration from your code.
**/
// Load environment variables from a .env file if one exists
require('dotenv').load()
// This config file uses MongoDB for User accounts, as well as session storage.
// This config includes options for NeDB, which it defaults to if no DB URI
// is specified. NeDB is an in-memory only database intended here for testing.
const MongoClient = require('mongodb').MongoClient
const NeDB = require('nedb')
const MongoObjectId = (process.env.MONGO_URI) ? require('mongodb').ObjectId : (id) => { return id }
// Use Node Mailer SMTP Transport for email sign in
const nodemailer = require('nodemailer')
const nodemailerSmtpTransport = require('nodemailer-smtp-transport')
module.exports = () => {
return new Promise((resolve, reject) => {
if (process.env.MONGO_URI) {
// Connect to MongoDB Database and return user connection
MongoClient.connect(process.env.MONGO_URI, (err, mongoClient) => {
if (err) return reject(err)
const dbName = process.env.MONGO_URI.split('/').pop().split('?').shift()
const db = mongoClient.db(dbName)
return resolve(db.collection('users'))
})
} else {
// If no MongoDB URI string specified, use NeDB, an in-memory work-a-like.
// NeDB is not persistant and is intended for testing only.
let collection = new NeDB({ autoload: true })
collection.loadDatabase(err => {
if (err) return reject(err)
resolve(collection)
})
}
})
.then(usersCollection => {
return Promise.resolve({
// If a user is not found find() should return null (with no error).
find: ({id, email, emailToken, provider} = {}) => {
let query = {}
// Find needs to support looking up a user by ID, Email, Email Token,
// and Provider Name + Users ID for that Provider
if (id) {
query = { _id: MongoObjectId(id) }
} else if (email) {
query = { email: email }
} else if (emailToken) {
query = { emailToken: emailToken }
} else if (provider) {
query = { [`${provider.name}.id`]: provider.id }
}
return new Promise((resolve, reject) => {
usersCollection.findOne(query, (err, user) => {
if (err) return reject(err)
return resolve(user)
})
})
},
insert: (user) => {
return new Promise((resolve, reject) => {
usersCollection.insert(user, (err, response) => {
if (err) return reject(err)
// Mongo Client automatically adds an id to an inserted object, but
// if using a work-a-like we may need to add it from the response.
if (!user._id && response._id) user._id = response._id
return resolve(user)
})
})
},
update: (user) => {
return new Promise((resolve, reject) => {
usersCollection.update({_id: user._id}, user, {}, (err) => {
if (err) return reject(err)
return resolve(user)
})
})
},
remove: (id) => {
return new Promise((resolve, reject) => {
usersCollection.remove({_id: id}, (err) => {
if (err) return reject(err)
return resolve(true)
})
})
},
// Seralize turns the value of the ID key from a User object
serialize: (user) => {
return Promise.resolve(user._id)
},
// Deseralize turns a User ID into a normalized User object that is
// exported to clients. It should not return private/sensitive fields.
deserialize: (id) => {
return new Promise((resolve, reject) => {
usersCollection.findOne({ _id: MongoObjectId(id) }, (err, user) => {
if (err) return reject(err)
// If user not found (e.g. account deleted) return null object
if (!user) return resolve(null)
return resolve({
id: user._id,
name: user.name,
email: user.email,
emailVerified: user.emailVerified,
admin: user.admin || false
})
})
})
},
// Define method for sending links for signing in over email.
sendSignInEmail: ({
email = null,
url = null
} = {}) => {
nodemailer
.createTransport(nodemailerSmtpTransport({
host: process.env.EMAIL_SERVER,
port: process.env.EMAIL_PORT || 25,
secure: true,
auth: {
user: process.env.EMAIL_USERNAME,
pass: process.env.EMAIL_PASSWORD
}
}))
.sendMail({
to: email,
from: process.env.EMAIL_FROM,
subject: 'Sign in link',
text: `Use the link below to sign in:\n\n${url}\n\n`,
html: `<p>Use the link below to sign in:</p><p>${url}</p>`
}, (err) => {
if (err) {
console.error('Error sending email to ' + email, err)
}
})
if (process.env.NODE_ENV === 'development') {
console.log('Generated sign in link ' + url + ' for ' + email)
}
},
})
})
}

View File

@@ -0,0 +1,108 @@
/**
* next-auth.providers.js Example
*
* This file returns a simple array of oAuth Provider objects for NextAuth.
*
* This example returns an array based on what environment variables are set,
* with explicit support for Facebook, Google and Twitter, but it can be used
* to add strategies for other oAuth providers.
*
* Environment variables for this example:
*
* FACEBOOK_ID=
* FACEBOOK_SECRET=
* GOOGLE_ID=
* GOOGLE_SECRET=
* TWITTER_KEY=
* TWITTER_SECRET=
*
* If you wish, you can put these in a `.env` to seperate your environment
* specific configuration from your code.
**/
// Load environment variables from a .env file if one exists
require('dotenv').load()
module.exports = () => {
let providers = []
if (process.env.FACEBOOK_ID && process.env.FACEBOOK_SECRET) {
providers.push({
providerName: 'Facebook',
providerOptions: {
scope: ['email', 'public_profile']
},
Strategy: require('passport-facebook').Strategy,
strategyOptions: {
clientID: process.env.FACEBOOK_ID,
clientSecret: process.env.FACEBOOK_SECRET,
profileFields: ['id', 'displayName', 'email', 'link']
},
getProfile(profile) {
// Normalize profile into one with {id, name, email} keys
return {
id: profile.id,
name: profile.displayName,
email: profile._json.email
}
}
})
}
if (process.env.GOOGLE_ID && process.env.GOOGLE_SECRET) {
providers.push({
providerName: 'Google',
providerOptions: {
scope: ['profile', 'email']
},
Strategy: require('passport-google-oauth').OAuth2Strategy,
strategyOptions: {
clientID: process.env.GOOGLE_ID,
clientSecret: process.env.GOOGLE_SECRET
},
getProfile(profile) {
// Normalize profile into one with {id, name, email} keys
return {
id: profile.id,
name: profile.displayName,
email: profile.emails[0].value
}
}
})
}
/**
* Note: Twitter doesn't expose emails by default.
* If we don't get one, Passport-stategies.js will create a placeholder.
*
*
* To have your Twitter oAuth return emails go to apps.twitter.com and add
* links to your Terms and Conditions and Privacy Policy under the "Settings"
* tab, then check the "Request email addresses" from users box under the
* "Permissions" tab.
**/
if (process.env.TWITTER_KEY && process.env.TWITTER_SECRET) {
providers.push({
providerName: 'Twitter',
providerOptions: {
scope: []
},
Strategy: require('passport-twitter').Strategy,
strategyOptions: {
consumerKey: process.env.TWITTER_KEY,
consumerSecret: process.env.TWITTER_SECRET,
userProfileURL: 'https://api.twitter.com/1.1/account/verify_credentials.json?include_email=true'
},
getProfile(profile) {
// Normalize profile into one with {id, name, email} keys
return {
id: profile.id,
name: profile.displayName,
email: (profile.emails && profile.emails[0].value) ? profile.emails[0].value : ''
}
}
})
}
return providers
}

35
example/package.json Normal file
View File

@@ -0,0 +1,35 @@
{
"name": "next-auth-examples",
"version": "1.0.0",
"description": "An example of how to use next-auth",
"repository": "https://github.com/iaincollins/next-auth.git",
"main": "",
"scripts": {
"dev": "NODE_ENV=development node index.js",
"build": "next build",
"start": "NODE_ENV=production node index.js"
},
"author": "",
"license": "ISC",
"dependencies": {
"connect-mongo": "^2.0.1",
"dotenv": "^4.0.0",
"mongodb": "^3.0.1",
"nedb": "^1.8.0",
"next": "^4.2.3",
"next-auth": "^1.1.1",
"next-auth-client": "^1.1.1",
"nodemailer": "^4.4.2",
"nodemailer-smtp-transport": "^2.7.4",
"passport-facebook": "^2.1.1",
"passport-google-oauth": "^1.0.0",
"passport-twitter": "^1.0.4",
"react": "^16.2.0",
"react-dom": "^16.2.0",
"universal-cookie": "^2.1.2"
},
"now": {
"name": "next-auth-demo",
"alias": "next-auth-demo.now.sh"
}
}

View File

@@ -0,0 +1,94 @@
import React from 'react'
import Link from 'next/link'
import Router from 'next/router'
import Cookies from 'universal-cookie'
import { NextAuth } from 'next-auth-client'
export default class extends React.Component {
static async getInitialProps({req}) {
const session = await NextAuth.init({force: true, req: req})
const cookies = new Cookies((req && req.headers && req.headers.cookie) ? req.headers.cookie : null)
// If the user is signed in, we look for a redirect URL cookie and send
// them to that page, so that people signing in end up back on the page they
// were on before signing in. Defaults to '/'.
let redirectTo = '/'
if (session.user) {
// Read redirect URL to redirect to from cookies
redirectTo = cookies.get('redirect_url') || redirectTo
// Allow relative paths only - strip protocol/host/port if they exist.
redirectTo = redirectTo.replace( /^[a-zA-Z]{3,5}\:\/{2}[a-zA-Z0-9_.:-]+\//, '')
}
return {
session: session,
redirectTo: redirectTo
}
}
async componentDidMount() {
// Get latest session data after rendering on client then redirect.
// The ensures client state is always updated after signing in or out.
const session = await NextAuth.init({force: true})
Router.push(this.props.redirectTo)
}
render() {
// Provide a link for clients without JavaScript as a fallback.
return (
<React.Fragment>
<style jsx global>{`
body{
background-color: #fff;
}
.circle-loader {
position: absolute;
top: 50%;
left: 50%;
width: 50%;
z-index: 100;
text-align: center;
transform: translate(-50%, -50%);
}
.circle-loader .circle {
fill: transparent;
stroke: rgba(0,0,0,0.2);
stroke-width: 4px;
animation: dash 2s ease infinite, rotate 2s linear infinite;
}
@keyframes dash {
0% {
stroke-dasharray: 1,95;
stroke-dashoffset: 0;
}
50% {
stroke-dasharray: 85,95;
stroke-dashoffset: -25;
}
100% {
stroke-dasharray: 85,95;
stroke-dashoffset: -93;
}
}
@keyframes rotate {
0% {transform: rotate(0deg); }
100% {transform: rotate(360deg); }
}
`}</style>
<a href={this.props.redirectTo} className="circle-loader">
<svg className="circle" width="60" height="60" version="1.1" xmlns="http://www.w3.org/2000/svg">
<circle cx="30" cy="30" r="15"/>
</svg>
</a>
<script src="https://cdn.polyfill.io/v2/polyfill.min.js"/>
</React.Fragment>
)
}
}

View File

@@ -0,0 +1,31 @@
import React from 'react'
import Head from 'next/head'
import Link from 'next/link'
export default class extends React.Component {
static async getInitialProps({query}) {
return {
email: query.email
}
}
render() {
return(
<div className="container">
<Head>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossOrigin="anonymous"/>
</Head>
<div className="text-center">
<h1 className="display-4 mt-5 mb-3">Check your email</h1>
<p className="lead">
A sign in link has been sent to <span className="font-weight-bold">{this.props.email}</span>
</p>
<p>
<Link href="/"><a>Home</a></Link>
</p>
</div>
</div>
)
}
}

View File

@@ -0,0 +1,74 @@
import React from 'react'
import Head from 'next/head'
import Link from 'next/link'
export default class extends React.Component {
static async getInitialProps({query}) {
return {
action: query.action || null,
type: query.type || null,
service: query.service || null
}
}
render() {
if (this.props.action == 'signin' && this.props.type == 'oauth' && this.props.service) {
return(
<div className="container">
<Head>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossOrigin="anonymous"/>
</Head>
<div className="text-center">
<h1 className="display-4 mt-5 mb-3">Unable to sign in with {this.props.service}</h1>
<p className="lead">An account associated with your email address already exists.</p>
<p className="lead"><Link href="/auth"><a>Sign in with email or another service.</a></Link></p>
</div>
<div className="row">
<div className="col-sm-8 mr-auto ml-auto">
<div className="card m-3 text-muted">
<div className="card-body">
<h4>Why am I seeing this?</h4>
<p className="mb-1">
It looks like you might have already signed up using another service.
</p>
<p className="mb-0">
To sign in with {this.props.service}, first sign in using your email address then link accounts.
</p>
</div>
</div>
</div>
</div>
</div>
)
} else if (this.props.action == 'signin' && this.props.type == 'token-invalid') {
return(
<div className="container">
<Head>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossOrigin="anonymous"/>
</Head>
<div className="text-center">
<h1 className="display-4 mt-5 mb-2">Sign in link not valid</h1>
<p className="lead">The sign in link you used is no longer valid.</p>
<p className="lead"><Link href="/auth"><a>Get a new sign in link.</a></Link></p>
</div>
</div>
)
} else {
return(
<div className="container">
<Head>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossOrigin="anonymous"/>
</Head>
<div className="text-center">
<h1 className="display-4 mt-5">Error signing in</h1>
<p className="lead">An error occured while trying to sign in.</p>
<p>
<Link href="/"><a>Home</a></Link>
</p>
</div>
</div>
)
}
}
}

176
example/pages/auth/index.js Normal file
View File

@@ -0,0 +1,176 @@
import React from 'react'
import Head from 'next/head'
import Router from 'next/router'
import Link from 'next/link'
import Cookies from 'universal-cookie'
import { NextAuth } from 'next-auth-client'
export default class extends React.Component {
static async getInitialProps({req}) {
return {
session: await NextAuth.init({req}),
linkedAccounts: await NextAuth.linked({req}),
providers: await NextAuth.providers({req})
}
}
constructor(props) {
super(props)
this.state = {
email: '',
session: this.props.session
}
this.handleEmailChange = this.handleEmailChange.bind(this)
this.handleSignInSubmit = this.handleSignInSubmit.bind(this)
}
handleEmailChange(event) {
this.setState({
email: event.target.value
})
}
handleSignInSubmit(event) {
event.preventDefault()
if (!this.state.email) return
// Save current URL so user is redirected back here after signing in
const cookies = new Cookies()
cookies.set('redirect_url', window.location.pathname)
NextAuth.signin(this.state.email)
.then(() => {
Router.push(`/auth/check-email?email=${this.state.email}`)
})
.catch(err => {
Router.push(`/auth/error?action=signin&type=email&email=${this.state.email}`)
})
}
render() {
if (this.props.session.user) {
return (
<div className="container">
<Head>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossOrigin="anonymous"/>
<script src="https://cdn.polyfill.io/v2/polyfill.min.js"/>
</Head>
<div className="text-center">
<h1 className="display-4 mt-3">NextAuth Example</h1>
<p className="lead mt-3 mb-1">You are signed in as <span className="font-weight-bold">{this.props.session.user.name || this.props.session.user.email}</span>.</p>
</div>
<div className="row">
<div className="col-sm-5 mr-auto ml-auto">
<LinkAccounts
session={this.props.session}
linkedAccounts={this.props.linkedAccounts}
/>
</div>
</div>
<p className="text-center">
<Link href="/"><a>Home</a></Link>
</p>
</div>
)
} else {
return (
<div className="container">
<Head>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossOrigin="anonymous"/>
<script src="https://cdn.polyfill.io/v2/polyfill.min.js"/>
</Head>
<div className="text-center">
<h1 className="display-4 mt-3 mb-3">NextAuth Example</h1>
</div>
<div className="row">
<div className="col-sm-6 mr-auto ml-auto">
<div className="card mt-3 mb-3">
<h4 className="card-header">Sign In</h4>
<div className="card-body pb-0">
<SignInButtons providers={this.props.providers}/>
<form id="signin" method="post" action="/auth/email/signin" onSubmit={this.handleSignInSubmit}>
<input name="_csrf" type="hidden" value={this.state.session.csrfToken}/>
<p>
<label htmlFor="email">Email address</label><br/>
<input name="email" type="text" placeholder="j.smith@example.com" id="email" className="form-control" value={this.state.email} onChange={this.handleEmailChange}/>
</p>
<p className="text-right">
<button id="submitButton" type="submit" className="btn btn-outline-primary">Sign in with email</button>
</p>
</form>
</div>
</div>
</div>
</div>
<p className="text-center">
<Link href="/"><a>Home</a></Link>
</p>
</div>
)
}
}
}
export class LinkAccounts extends React.Component {
render() {
return (
<div className="card mt-3 mb-3">
<h4 className="card-header">Link Accounts</h4>
<div className="card-body pb-0">
{
Object.keys(this.props.linkedAccounts).map((provider, i) => {
return <LinkAccount key={i} provider={provider} session={this.props.session} linked={this.props.linkedAccounts[provider]}/>
})
}
</div>
</div>
)
}
}
export class LinkAccount extends React.Component {
render() {
if (this.props.linked === true) {
return (
<form method="post" action={`/auth/oauth/${this.props.provider.toLowerCase()}/unlink`}>
<input name="_csrf" type="hidden" value={this.props.session.csrfToken}/>
<p>
<button className="btn btn-block btn-outline-danger" type="submit">
Unlink from {this.props.provider}
</button>
</p>
</form>
)
} else {
return (
<p>
<a className="btn btn-block btn-outline-primary" href={`/auth/oauth/${this.props.provider.toLowerCase()}`}>
Link with {this.props.provider}
</a>
</p>
)
}
}
}
export class SignInButtons extends React.Component {
render() {
return (
<React.Fragment>
{
Object.keys(this.props.providers).map((provider, i) => {
return (
<p key={i}>
<a className="btn btn-block btn-outline-secondary" href={this.props.providers[provider].signin}>
Sign in with {provider}
</a>
</p>
)
})
}
</React.Fragment>
)
}
}

73
example/pages/index.js Normal file
View File

@@ -0,0 +1,73 @@
import React from 'react'
import Head from 'next/head'
import Router from 'next/router'
import Link from 'next/link'
import Cookies from 'universal-cookie'
import { NextAuth } from 'next-auth-client'
export default class extends React.Component {
static async getInitialProps({req}) {
return {
session: await NextAuth.init({req})
}
}
constructor(props) {
super(props)
this.handleSignOutSubmit = this.handleSignOutSubmit.bind(this)
}
handleSignOutSubmit(event) {
event.preventDefault()
// Save current URL so user is redirected back here after signing out
const cookies = new Cookies()
cookies.set('redirect_url', window.location.pathname)
NextAuth.signout()
.then(() => {
Router.push('/auth/callback')
})
.catch(err => {
Router.push('/auth/error?action=signout')
})
}
render() {
return (
<div className="container">
<Head>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossOrigin="anonymous"/>
<script src="https://cdn.polyfill.io/v2/polyfill.min.js"/>
</Head>
<div className="text-center">
<h1 className="display-4 mt-3 mb-3">NextAuth Example</h1>
<p className="lead mt-3 mb-3">An example of how to use the <a href="https://www.npmjs.com/package/next-auth">NextAuth</a> module.</p>
<SignInMessage {...this.props}/>
</div>
</div>
)
}
}
export class SignInMessage extends React.Component {
render() {
if (this.props.session.user) {
return (
<React.Fragment>
<p><Link href="/auth"><a className="btn btn-secondary">Manage Account</a></Link></p>
<form id="signout" method="post" action="/auth/signout" onSubmit={this.handleSignOutSubmit}>
<input name="_csrf" type="hidden" value={this.props.session.csrfToken}/>
<button type="submit" className="btn btn-outline-secondary">Sign out</button>
</form>
</React.Fragment>
)
} else {
return (
<React.Fragment>
<p><Link href="/auth"><a className="btn btn-primary">Sign in</a></Link></p>
</React.Fragment>
)
}
}
}

358
index.js Normal file
View File

@@ -0,0 +1,358 @@
'use strict'
const BodyParser = require('body-parser')
const CookieParser = require('cookie-parser')
const lusca = require('lusca')
const Express = require('express')
const ExpressSession = require('express-session')
const passportStrategies = require('./passport-strategies')
const uuid = require('uuid/v4')
module.exports = (nextApp, {
cookieParser = true,
bodyParser = 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(),
// Maximum Session Age in ms (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,
// Session Revalidation required after X ms (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,
// Absolute URL of the server (recommended but optional).
// 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,
// 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) => { Promise.resolve(user) },
insert: (user) => { Promise.resolve(user) },
serialize: (user) => { Promise.resolve(id) },
deserialize: (id) => { Promise.resolve(user) },
sendSignInEmail: ({
email = null,
url = null,
req = null
} = {}) => { Promise.resolve(true) }
}
} = {}) => {
if (typeof(nextApp) !== 'object') {
throw new Error('nextApp must be a next instance')
}
if (typeof(functions) !== 'object') {
throw new Error('functions must be a an object')
}
if (expressApp === null) {
expressApp = Express()
}
/**
* Set up cookie parsing, body parsing, express sessions and add Cross Site
* Site Request Forgery tokens to all POST requests.
*
* You can set cookieParser, bodyParser to false and pass an express instance
* with your own pre-configured cookie parsing and body parsing library if
* you wish, though this may cause compatiblity issues if they do not work
* in the same way.
**/
if (cookieParser === true) {
expressApp.use(CookieParser())
}
if (bodyParser === true) {
expressApp.use(BodyParser.json())
expressApp.use(BodyParser.urlencoded({
extended: true
}))
}
expressApp.use(expressSession({
secret: sessionSecret,
store: sessionStore,
resave: false,
rolling: true,
saveUninitialized: false,
httpOnly: true,
cookie: {
maxAge: sessionMaxAge
}
}))
if (csrf === true) {
expressApp.use(lusca.csrf())
}
/**
* 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
*/
expressApp.get(`${pathPrefix}/session`, (req, res) => {
let session = {
maxAge: sessionMaxAge,
revalidateAge: sessionRevalidateAge,
csrfToken: res.locals._csrf
}
// Add user object to session if logged in
if (req.user) {
// If logged in, export the API access token details to the client
// Note: This token is valid for the duration of this session only.
session.user = req.user
if (req.session && req.session.api) {
session.api = req.session.api
}
}
return res.json(session)
})
/**
* Return list of which accounts are already linked.
*
* 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.linked = () => {
return new Promise((resolve, reject) => {
if (!req.user) return resolve({})
functions.serialize(req.user)
.then(id => {
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()
})
// RESTful endpoint
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)
})
})
/**
* 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 || `http://${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
})
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) {
// Reset token and update email address as verified
user.emailToken = null
user.emailVerified = true
return functions.update(user)
} 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) throw err
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`)
})
})
/**
* If a port has been specified, then set Next.js as the default final
* route hanlder and start listening on the specified port.
**/
if (port) {
// Set Next.js to handle all other routes by default
expressApp.all('*', (req, res) => {
let nextRequestHandler = nextApp.getRequestHandler()
return nextRequestHandler(req, res)
})
// Start Express
return expressApp.listen(port, err => {
if (err) Promise.reject(err)
return Promise.resolve({
express: expressApp
})
})
} else {
return Promise.resolve({
express: expressApp
})
}
}

26
package.json Normal file
View File

@@ -0,0 +1,26 @@
{
"name": "next-auth",
"version": "1.1.1",
"description": "Add oAuth and/or email authentication to Next.js sites",
"repository": "https://github.com/iaincollins/next-auth.git",
"main": "index.js",
"scripts": {},
"author": "",
"license": "ISC",
"dependencies": {
"cookie-parser": "^1.4.3",
"express": "^4.16.2",
"express-session": "^1.15.6",
"lusca": "^1.5.2",
"nodemailer": "^4.4.2",
"nodemailer-direct-transport": "^3.3.2",
"passport": "^0.4.0",
"uuid": "^3.2.1"
},
"devDependencies": {
"next": "^4.2.3",
"next-auth-client": "^1.0.0",
"react": "^16.2.0",
"react-dom": "^16.2.0"
}
}

324
passport-strategies.js Normal file
View File

@@ -0,0 +1,324 @@
/**
* Configures Passport Strategies
**/
'use strict'
const passport = require('passport')
module.exports = ({
expressApp = null, // Express Server
pathPrefix = '/auth', // URL base path for authentication routes
providers = [],
serverUrl = null,
functions = {
find: ({
id,
email,
emailToken,
provider
} = {}) => {},
update: (user) => {},
insert: (user) => {},
serialize: (user) => {},
deserialize: (id) => {}
}
} = {}) => {
if (expressApp === null) {
throw new Error('expressApp must be an instance of an express server')
}
if (typeof(functions) !== 'object') {
throw new Error('functions must be a an object')
}
/**
* Return functions ID property from a functions object
**/
passport.serializeUser((user, next) => {
functions.serialize(user)
.then(id => {
next(null, id)
})
.catch(err => {
next(err, false)
})
})
/**
* Return functions from a functions ID
**/
passport.deserializeUser((id, next) => {
functions.deserialize(id)
.then(user => {
if (!user) return next(null, false)
next(null, user)
})
.catch(err => {
next(err, false)
})
})
// Define a Passport strategy for provider
providers.forEach(({
providerName,
Strategy,
strategyOptions,
getProfile
}) => {
strategyOptions.callbackURL = (serverUrl || '') + `${pathPrefix}/oauth/${providerName.toLowerCase()}/callback`
strategyOptions.passReqToCallback = true
passport.use(new Strategy(strategyOptions, (req, accessToken, refreshToken, profile, next) => {
try {
// Normalise the provider specific profile into a standard user object.
profile = getProfile(profile)
// Save the Access Token to the current session.
req.session[providerName.toLowerCase()] = {
accessToken: accessToken
}
// If we didn't get an email address from the oAuth provider then
// generate a unique one as placeholder, using Provider name and ID.
//
// If you want users to specify a valid email address after signing in,
// you can check for email addresses ending "@localhost.localdomain"
// and prompt those users to supply a valid address.
if (!profile.email) {
profile.email = `${providerName.toLowerCase()}-${profile.id}@localhost.localdomain`
}
// Look for a user in the database associated with this account.
functions.find({
provider: {
name: providerName.toLowerCase(),
id: profile.id
}
})
.then(user => {
if (req.user) {
// This section handles scenarios when a user is already signed in.
if (user) {
// This section handles if the user is already logged in
if (req.user.id === user.id) {
// This section handles if the user is already logged in and is
// already linked to local account they are signed in with.
// If they are, all we need to do is update the Refresh Token
// value if we got one.
if (refreshToken) {
user[providerName.toLowerCase()] = {
id: profile.id,
accessToken: accessToken,
refreshToken: refreshToken
}
functions.update(user)
.then(user => {
return next(null, user)
})
.catch(err => {
next(err)
})
} else {
return next(null, user)
}
} else {
// This section handles if a user is logged in but the oAuth
// account they are trying to link to is already linked to a
// different local account.
// This prevents users from linking an oAuth account to more
// than one local account at the same time.
return next(null, false)
}
} else {
// This secion handles if a user is already logged in and is
// trying to link a new account.
// Look up the current user.
// 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 => {
// This error should not happen, unless the currently signed in
// user has been deleted deleted from the database since
// signing in (or there is a problem talking to the database).
if (!user) return next(new Error('Unable to look up account for current user'), false)
// If we don't already have a name for the user, use value the
// name value specfied in their profile on the remote service.
user.name = user.name || profile.name
// If we don't have a real email address for the user, use the
// email value specified in their profile on the remote service.
if (user.email && user.email.match(/.*@localhost\.localdomain$/) &&
profile.email && !profile.email.match(/.*@localhost\.localdomain$/)) {
user.emailVerified = false
user.email = profile.email
}
// Save Profile ID, Access Token and Refresh Token values
// to the users local account, which links the accounts.
user[providerName.toLowerCase()] = {
id: profile.id,
accessToken: accessToken,
refreshToken: refreshToken
}
// Update details for the new provider for this user.
return functions.update(user)
.then(user => {
return next(null, user)
})
.catch(err => {
return next(err)
})
})
.catch(err => {
return next(err, false)
})
}
} else {
// This section handles scenarios when a user is not logged in.
if (user) {
// This section handles senarios where the user is not logged in
// but they seem to have an account already, so we sign them in
// as that user.
// Update Access and Refresh Tokens for the user if we got them.
if (accessToken || refreshToken) {
if (accessToken) user[providerName.toLowerCase()].accessToken = accessToken
if (refreshToken) user[providerName.toLowerCase()].refreshToken = refreshToken
return functions.update(user)
.then(user => {
return next(null, user)
})
.catch(err => {
return next(err, false)
})
} else {
return next(null, user)
}
} else {
// This section handles senarios where the user is not logged in
// and they don't have a local account already.
// First we check to see if a local account with the same email
// address as the one associated with their oAuth profile exists.
//
// This is so they can't accidentally end up with two accounts
// linked to the same email address.
return functions.find({email: profile.email})
.then(user => {
// If we already have a local account associated with their
// email address, the user should sign in with that account -
// and then they can link accounts if they wish.
//
// Note: Automatically linking them here could expose a
// potential security exploit allowing someone to pre-register
// or create an account elsewhere for another users email
// address then trying to sign in from it, so don't do that.
if (user) return next(null, false)
// If an account does not exist, create one for them and return
// a user object to passport, which will sign them in.
return functions.insert({
name: profile.name,
email: profile.email,
[providerName.toLowerCase()]: {
id: profile.id,
accessToken: accessToken,
refreshToken: refreshToken
}
})
.then(user => {
return next(null, user)
})
.catch(err => {
return next(err, false)
})
})
}
}
})
.catch(err => {
next(err, false)
})
} catch (err) {
return next(err, false)
}
}))
})
// Initialise Passport
expressApp.use(passport.initialize())
expressApp.use(passport.session())
// Add routes for each provider
providers.forEach(({
providerName,
providerOptions
}) => {
// Route to start sign in
expressApp.get(`${pathPrefix}/oauth/${providerName.toLowerCase()}`, passport.authenticate(providerName.toLowerCase(), providerOptions))
// Route to call back to after signing in
expressApp.get(`${pathPrefix}/oauth/${providerName.toLowerCase()}/callback`,
passport.authenticate(providerName.toLowerCase(), {
successRedirect: `${pathPrefix}/callback?action=signin&service=${providerName}`,
failureRedirect: `${pathPrefix}/error?action=signin&type=oauth&service=${providerName}`
})
)
// Route to post to unlink accounts
expressApp.post(`${pathPrefix}/oauth/${providerName.toLowerCase()}/unlink`, (req, res, next) => {
if (!req.user) {
return next(new Error('Not signed in'))
}
// 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 next(new Error('Unable to look up account for current user'))
// Remove connection between user account and oauth provider
if (user[providerName.toLowerCase()]) {
delete user[providerName.toLowerCase()]
}
return functions.update(user)
.then(user => {
return res.redirect(`${pathPrefix}/callback?action=unlink&service=${providerName.toLowerCase()}`)
})
.catch(err => {
return next(err, false)
})
})
})
})
// A catch all for providers that are not configured
expressApp.get(`${pathPrefix}/oauth/:provider`, (req, res) => {
return res.redirect(`${pathPrefix}/error?action=signin&type=unsupported`)
})
}