mirror of
https://github.com/SrIzan10/next-auth.git
synced 2026-05-01 10:55:20 +00:00
Initial commit of next-auth and example
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
.env
|
||||
node_modules
|
||||
13
example/.env.example
Normal file
13
example/.env.example
Normal 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
3
example/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
.env
|
||||
/.env.production
|
||||
node_modules
|
||||
6645
example/.next/bundles/pages/_document.js
Normal file
6645
example/.next/bundles/pages/_document.js
Normal file
File diff suppressed because one or more lines are too long
7564
example/.next/bundles/pages/_error.js
Normal file
7564
example/.next/bundles/pages/_error.js
Normal file
File diff suppressed because one or more lines are too long
12495
example/.next/bundles/pages/auth.js
Normal file
12495
example/.next/bundles/pages/auth.js
Normal file
File diff suppressed because one or more lines are too long
15406
example/.next/commons.js
Normal file
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
4
example/.next/dist/pages/_document.js
vendored
Normal 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
4
example/.next/dist/pages/_error.js
vendored
Normal 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
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
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
817
example/.next/manifest.js
Normal file
File diff suppressed because one or more lines are too long
43
example/index.js
Normal file
43
example/index.js
Normal 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)
|
||||
})
|
||||
66
example/next-auth.config.js
Normal file
66
example/next-auth.config.js
Normal 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
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
179
example/next-auth.functions.js
Normal file
179
example/next-auth.functions.js
Normal 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)
|
||||
}
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
108
example/next-auth.providers.js
Normal file
108
example/next-auth.providers.js
Normal 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
35
example/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
94
example/pages/auth/callback.js
Normal file
94
example/pages/auth/callback.js
Normal 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>
|
||||
)
|
||||
}
|
||||
}
|
||||
31
example/pages/auth/check-email.js
Normal file
31
example/pages/auth/check-email.js
Normal 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>
|
||||
)
|
||||
}
|
||||
}
|
||||
74
example/pages/auth/error.js
Normal file
74
example/pages/auth/error.js
Normal 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
176
example/pages/auth/index.js
Normal 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
73
example/pages/index.js
Normal 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
358
index.js
Normal 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
26
package.json
Normal 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
324
passport-strategies.js
Normal 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`)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user