commit 689a81f6362b52f9cba417a4cd147a915565534e
parent a54cac9299d27fe714df1987e7511e450f4093cb
Author: Konstantin Tarkus <hello@tarkus.me>
Date: Fri, 26 Feb 2016 01:51:07 +0300
Add Passport.js authentication strategy sample
Diffstat:
5 files changed, 239 insertions(+), 3 deletions(-)
diff --git a/package.json b/package.json
@@ -8,17 +8,24 @@
"babel-polyfill": "^6.5.0",
"babel-runtime": "^6.5.0",
"bluebird": "3.3.1",
+ "body-parser": "1.15.0",
"classnames": "2.2.3",
+ "cookie-parser": "1.4.1",
"eventemitter3": "1.1.1",
"express": "4.13.4",
+ "express-jwt": "3.3.0",
"fastclick": "1.0.6",
"fbjs": "0.7.2",
"front-matter": "2.0.6",
"history": "2.0.0",
"isomorphic-style-loader": "0.0.10",
"jade": "1.11.0",
+ "jsonwebtoken": "5.7.0",
"node-fetch": "1.3.3",
"normalize.css": "3.0.3",
+ "passport": "0.3.2",
+ "passport-facebook": "2.1.0",
+ "pg": "4.4.6",
"react": "0.14.7",
"react-dom": "0.14.7",
"react-routing": "0.0.7",
diff --git a/src/config.js b/src/config.js
@@ -13,6 +13,8 @@
export const port = process.env.PORT || 5000;
export const host = process.env.WEBSITE_HOSTNAME || `localhost:${port}`;
+export const databaseUrl = process.env.DATABASE_URL || 'postgresql://demo:Lqk62xg6TBm5UhfR@demo.ctbl5itzitm4.us-east-1.rds.amazonaws.com:5432/membership01';
+
export const analytics = {
// https://analytics.google.com/
@@ -22,10 +24,12 @@ export const analytics = {
export const auth = {
+ jwt: { secret: 'React Starter Kit' },
+
// https://developers.facebook.com/
facebook: {
- id: process.env.FACEBOOK_ID || '183246425378777',
- secret: process.env.FACEBOOK_SECRET || 'cb2b201f0249d15454221cbf00d6ff99',
+ id: process.env.FACEBOOK_APP_ID || '186244551745631',
+ secret: process.env.FACEBOOK_APP_SECRET || 'a970ae3240ab4b9b8aae0f9f0661c6fc',
},
// https://cloud.google.com/console/project
diff --git a/src/core/db.js b/src/core/db.js
@@ -0,0 +1,81 @@
+/**
+ * React Starter Kit (https://www.reactstarterkit.com/)
+ *
+ * Copyright © 2014-2016 Kriasoft, LLC. All rights reserved.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE.txt file in the root directory of this source tree.
+ */
+
+import db from 'pg';
+import Promise from 'bluebird';
+import { databaseUrl } from '../config';
+
+// TODO: Customize database connection settings
+/* jscs:disable requireCamelCaseOrUpperCaseIdentifiers */
+db.defaults.ssl = true;
+db.defaults.poolSize = 2;
+db.defaults.application_name = 'RSK';
+/* jscs:enable requireCamelCaseOrUpperCaseIdentifiers */
+
+/**
+ * Promise-based wrapper for pg.Client
+ * https://github.com/brianc/node-postgres/wiki/Client
+ */
+function AsyncClient(client) {
+ this.client = client;
+ this.query = this.query.bind(this);
+ this.end = this.end.bind(this);
+}
+
+AsyncClient.prototype.query = function query(sql, ...args) {
+ return new Promise((resolve, reject) => {
+ if (args.length) {
+ this.client.query(sql, args, (err, result) => {
+ if (err) {
+ reject(err);
+ } else {
+ resolve(result);
+ }
+ });
+ } else {
+ this.client.query(sql, (err, result) => {
+ if (err) {
+ reject(err);
+ } else {
+ resolve(result);
+ }
+ });
+ }
+ });
+};
+
+AsyncClient.prototype.end = function end() {
+ this.client.end();
+};
+
+/**
+ * Promise-based wrapper for pg.connect()
+ * https://github.com/brianc/node-postgres/wiki/pg
+ */
+db.connect = (connect => callback => new Promise((resolve, reject) => {
+ connect.call(db, databaseUrl, (err, client, done) => {
+ if (err) {
+ if (client) {
+ done(client);
+ }
+
+ reject(err);
+ } else {
+ callback(new AsyncClient(client)).then(() => {
+ done();
+ resolve();
+ }).catch(error => {
+ done(client);
+ reject(error);
+ });
+ }
+ });
+}))(db.connect);
+
+export default db;
diff --git a/src/core/passport.js b/src/core/passport.js
@@ -0,0 +1,111 @@
+/**
+ * React Starter Kit (https://www.reactstarterkit.com/)
+ *
+ * Copyright © 2014-2016 Kriasoft, LLC. All rights reserved.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE.txt file in the root directory of this source tree.
+ */
+
+/**
+ * Passport.js reference implementation.
+ * The database schema used in this sample is available at
+ * https://github.com/membership/membership.db/tree/master/postgres
+ */
+
+import passport from 'passport';
+import { Strategy as FacebookStrategy } from 'passport-facebook';
+import db from './db';
+import { auth as config } from '../config';
+
+/**
+ * Sign in with Facebook.
+ */
+passport.use(new FacebookStrategy({
+ clientID: config.facebook.id,
+ clientSecret: config.facebook.secret,
+ callbackURL: `/login/facebook/return`,
+ profileFields: ['name', 'email', 'link', 'locale', 'timezone'],
+ passReqToCallback: true,
+}, (req, accessToken, refreshToken, profile, done) => {
+ const loginName = 'facebook';
+ db.connect(async ({ query }) => {
+ if (req.user) {
+ let result = await query(
+ 'SELECT 1 FROM user_login WHERE name = $1 AND key = $2',
+ loginName, profile.id
+ );
+ if (result.rowCount) {
+ // There is already a Facebook account that belongs to you.
+ // Sign in with that account or delete it, then link it with your current account.
+ done();
+ } else {
+ await query(`
+ INSERT INTO user_account (id, email) SELECT $1, $2::character
+ WHERE NOT EXISTS (SELECT 1 FROM user_account WHERE id = $1);`,
+ req.user.id, profile._json.email);
+ await query(`
+ INSERT INTO user_login (user_id, name, key) VALUES ($1, 'facebook', $2);`,
+ req.user.id, profile.id);
+ await query(`
+ INSERT INTO user_claim (user_id, type, value) VALUES
+ ($1, 'urn:facebook:access_token', $3);`,
+ req.user.id, profile.id);
+ await query(`
+ INSERT INTO user_profile (user_id) SELECT $1
+ WHERE NOT EXISTS (SELECT 1 FROM user_profile WHERE user_id = $1);`,
+ req.user.id);
+ await query(`
+ UPDATE user_profile SET
+ display_name = COALESCE(NULLIF(display_name, ''), $2),
+ gender = COALESCE(NULLIF(gender, ''), $3),
+ picture = COALESCE(NULLIF(picture, ''), $4),
+ WHERE user_id = $1;`,
+ req.user.id, profile.displayName, profile._json.gender,
+ `https://graph.facebook.com/${profile.id}/picture?type=large`);
+ result = await query(`
+ SELECT id, email FROM user_account WHERE id = $1;`,
+ req.user.id);
+ done(null, result.rows[0]);
+ }
+ } else {
+ let result = await query(`
+ SELECT u.id, u.email FROM user_account AS u
+ LEFT JOIN user_login AS l ON l.user_id = u.id
+ WHERE l.name = $1 AND l.key = $2`, loginName, profile.id);
+ if (result.rowCount) {
+ done(null, result.rows[0]);
+ } else {
+ result = await query(`SELECT 1 FROM user_account WHERE email = $1`, profile._json.email);
+ if (result.rowCount) {
+ // There is already an account using this email address. Sign in to
+ // that account and link it with Facebook manually from Account Settings.
+ done(null);
+ } else {
+ result = await query(`
+ INSERT INTO user_account (email) VALUES ($1) RETURNING (id)`,
+ profile._json.email
+ );
+ const userId = result.rows[0].id;
+ await query(`
+ INSERT INTO user_login (user_id, name, key) VALUES ($1, 'facebook', $2)`,
+ userId, profile.id);
+ await query(`
+ INSERT INTO user_claim (user_id, type, value) VALUES
+ ($1, 'urn:facebook:access_token', $2);`,
+ userId, accessToken);
+ await query(`
+ INSERT INTO user_profile (user_id, display_name, gender, picture)
+ VALUES ($1, $2, $3, $4);`,
+ userId, profile.displayName, profile._json.gender,
+ `https://graph.facebook.com/${profile.id}/picture?type=large`
+ );
+ result = await query(`SELECT id, email FROM user_account WHERE id = $1;`, userId);
+ done(null, result.rows[0]);
+ }
+ }
+ }
+ }).catch(done);
+}));
+
+export default passport;
diff --git a/src/server.js b/src/server.js
@@ -10,12 +10,17 @@
import 'babel-polyfill';
import path from 'path';
import express from 'express';
+import cookieParser from 'cookie-parser';
+import bodyParser from 'body-parser';
+import expressJwt from 'express-jwt';
+import jwt from 'jsonwebtoken';
import React from 'react';
import ReactDOM from 'react-dom/server';
+import passport from './core/passport';
import Router from './routes';
import Html from './components/Html';
import assets from './assets';
-import { port } from './config';
+import { port, auth } from './config';
const server = global.server = express();
@@ -30,6 +35,34 @@ global.navigator.userAgent = global.navigator.userAgent || 'all';
// Register Node.js middleware
// -----------------------------------------------------------------------------
server.use(express.static(path.join(__dirname, 'public')));
+server.use(cookieParser());
+server.use(bodyParser.urlencoded({ extended: true }));
+server.use(bodyParser.json());
+
+//
+// Authentication
+// -----------------------------------------------------------------------------
+server.use(expressJwt({
+ secret: auth.jwt.secret,
+ credentialsRequired: false,
+ /* jscs:disable requireCamelCaseOrUpperCaseIdentifiers */
+ getToken: req => req.cookies.id_token,
+ /* jscs:enable requireCamelCaseOrUpperCaseIdentifiers */
+}));
+server.use(passport.initialize());
+
+server.get('/login/facebook',
+ passport.authenticate('facebook', { scope: ['email', 'user_location'], session: false })
+);
+server.get('/login/facebook/return',
+ passport.authenticate('facebook', { failureRedirect: '/login', session: false }),
+ (req, res) => {
+ const expiresIn = 60 * 60 * 24 * 180; // 180 days
+ const token = jwt.sign(req.user, auth.jwt.secret, { expiresIn });
+ res.cookie('id_token', token, { maxAge: 1000 * expiresIn, httpOnly: true });
+ res.redirect('/');
+ }
+);
//
// Register API middleware