Implement social authentication with React + RESTful API

The Gist

Let’s roll

mkdir social-auth-example && cd social-auth-example
npm i -g create-react-app
create-react-app frontend && cd frontend
npm start
npm i --save react-scripts
npm i --save react-twitter-auth react-facebook-login react-google-login
import React, { Component } from 'react';
import TwitterLogin from 'react-twitter-auth';
import FacebookLogin from 'react-facebook-login';
import { GoogleLogin } from 'react-google-login';

class App extends Component {

constructor() {
super();
this.state = { isAuthenticated: false, user: null, token: ''};
}

logout = () => {
this.setState({isAuthenticated: false, token: '', user: null})
};

twitterResponse = (e) => {};

facebookResponse = (e) => {};

googleResponse = (e) => {};
onFailure = (error) => {
alert(error);
}
render() {
let content = !!this.state.isAuthenticated ?
(
<div>
<p>Authenticated</p>
<div>
{this.state.user.email}
</div>
<div>
<button onClick={this.logout} className="button">
Log out
</button>
</div>
</div>
) :
(
<div>
<TwitterLogin loginUrl="http://localhost:4000/api/v1/auth/twitter"
onFailure=
{this.twitterResponse} onSuccess={this.twitterResponse}
requestTokenUrl="http://localhost:4000/api/v1/auth/twitter/reverse"/>
<FacebookLogin
appId="XXXXXXXXXX"
autoLoad=
{false}
fields="name,email,picture"
callback=
{this.facebookResponse} />
<GoogleLogin
clientId="XXXXXXXXXX"
buttonText="Login"
onSuccess=
{this.googleResponse}
onFailure={this.googleResponse}
/>
</div>
);

return (
<div className="App">
{content}
</div>
);
}
}

export default App;
npm i

Twitter

Google

Figure 1
Figure 2

Facebook

touch src/config.json
{
"GOOGLE_CLIENT_ID": "XXXXX",
"FACEBOOK_APP_ID": "XXXXX"
}
import React, { Component } from 'react';
import TwitterLogin from 'react-twitter-auth';
import FacebookLogin from 'react-facebook-login';
import { GoogleLogin } from 'react-google-login';
import config from './config.json';

class App extends Component {

constructor() {
super();
this.state = { isAuthenticated: false, user: null, token: ''};
}

logout = () => {
this.setState({isAuthenticated: false, token: '', user: null})
};
onFailure = (error) => {
alert(error);
};
twitterResponse = (response) => {};

facebookResponse = (response) => {
console.log(response);
};

googleResponse = (response) => {
console.log(response);
};

render() {
let content = !!this.state.isAuthenticated ?
(
<div>
<p>Authenticated</p>
<div>
{this.state.user.email}
</div>
<div>
<button onClick={this.logout} className="button">
Log out
</button>
</div>
</div>
) :
(
<div>
<TwitterLogin loginUrl="http://localhost:4000/api/v1/auth/twitter"
onFailure=
{this.onFailure} onSuccess={this.twitterResponse}
requestTokenUrl="http://localhost:4000/api/v1/auth/twitter/reverse"/>
<FacebookLogin
appId=
{config.FACEBOOK_APP_ID}
autoLoad={false}
fields="name,email,picture"
callback=
{this.facebookResponse} />
<GoogleLogin
clientId=
{config.GOOGLE_CLIENT_ID}
buttonText="Login"
onSuccess=
{this.googleResponse}
onFailure={this.onFailure}
/>
</div>
);

return (
<div className="App">
{content}
</div>
);
}
}

export default App;
import React, { Component } from 'react';
import TwitterLogin from 'react-twitter-auth';
import FacebookLogin from 'react-facebook-login';
import { GoogleLogin } from 'react-google-login';
import config from './config.json';

class App extends Component {

constructor() {
super();
this.state = { isAuthenticated: false, user: null, token: ''};
}

logout = () => {
this.setState({isAuthenticated: false, token: '', user: null})
};

onFailure = (error) => {
alert(error);
};

twitterResponse = (response) => {
const token = response.headers.get('x-auth-token');
response.json().then(user => {
if (token) {
this.setState({isAuthenticated: true, user, token});
}
});
};

facebookResponse = (response) => {
const tokenBlob = new Blob([JSON.stringify({access_token: response.accessToken}, null, 2)], {type : 'application/json'});
const options = {
method: 'POST',
body: tokenBlob,
mode: 'cors',
cache: 'default'
};
fetch('http://localhost:4000/api/v1/auth/facebook', options).then(r => {
const token = r.headers.get('x-auth-token');
r.json().then(user => {
if (token) {
this.setState({isAuthenticated: true, user, token})
}
});
})
};

googleResponse = (response) => {
const tokenBlob = new Blob([JSON.stringify({access_token: response.accessToken}, null, 2)], {type : 'application/json'});
const options = {
method: 'POST',
body: tokenBlob,
mode: 'cors',
cache: 'default'
};
fetch('http://localhost:4000/api/v1/auth/google', options).then(r => {
const token = r.headers.get('x-auth-token');
r.json().then(user => {
if (token) {
this.setState({isAuthenticated: true, user, token})
}
});
})
};

render() {
let content = !!this.state.isAuthenticated ?
(
<div>
<p>Authenticated</p>
<div>
{this.state.user.email}
</div>
<div>
<button onClick={this.logout} className="button">
Log out
</button>
</div>
</div>
) :
(
<div>
<TwitterLogin loginUrl="http://localhost:4000/api/v1/auth/twitter"
onFailure=
{this.onFailure} onSuccess={this.twitterResponse}
requestTokenUrl="http://localhost:4000/api/v1/auth/twitter/reverse"/>
<FacebookLogin
appId=
{config.FACEBOOK_APP_ID}
autoLoad={false}
fields="name,email,picture"
callback=
{this.facebookResponse} />
<GoogleLogin
clientId=
{config.GOOGLE_CLIENT_ID}
buttonText="Login"
onSuccess=
{this.googleResponse}
onFailure={this.onFailure}
/>
</div>
);

return (
<div className="App">
{content}
</div>
);
}
}

export default App;

Backend

mkdir backend && cd backend
npm i express-generator -g
express && npm i
npm i --save express-server utils
#!/usr/bin/env node

var app = require('../app');
var debug = require('debug')('backend:server');
var http = require('http');

var port = normalizePort(process.env.PORT || '4000');
app.set('port', port);

var server = http.createServer(app);

function normalizePort(val) {
var port = parseInt(val, 10);

if (isNaN(port)) {
// named pipe
return val;
}

if (port >= 0) {
// port number
return port;
}

return false;
}

var expressServerUtils = require('express-server-utils')(server, port);
expressServerUtils.listen();
expressServerUtils.handleOnError();
expressServerUtils.handleOnListening();

const exitActions = [server.close];
expressServerUtils.handleShutDown(exitActions);
npm start

Brace yourself

mkdir utils && touch passport.js mongoose.js utils2/token.utils.js && npm i --save passport passport-twitter-token passport-facebook-token passport-google-token mongoose jsonwebtoken cors request
var express = require('express');
var path = require('path');
var favicon = require('serve-favicon');
var logger = require('morgan');
var cookieParser = require('cookie-parser');
var bodyParser = require('body-parser');
var cors = require('cors');

var index = require('./routes/index');

var app = express();

var corsOption = {
origin: true,
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
credentials: true,
exposedHeaders: ['x-auth-token']
};
app.use(cors(corsOption));

// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');

// uncomment after placing your favicon in /public
//app.use(favicon(path.join(__dirname, 'public', 'favicon.ico')));
app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

app.use('/api/v1/', index);

module.exports = app;
var createToken = function(auth) {
return jwt.sign({
id: auth.id
}, 'my-secret',
{
expiresIn: 60 * 120
});
};

module.exports = {
generateToken: function(req, res, next) {
req.token = createToken(req.auth);
return next();
},
sendToken: function(req, res) {
res.setHeader('x-auth-token', req.token);
return res.status(200).send(JSON.stringify(req.user));
}
};
'use strict';

var mongoose = require('mongoose');
var Schema = mongoose.Schema;

module.exports = function () {

var db = mongoose.connect('mongodb://localhost:27017/social-auth-example');

var UserSchema = new Schema({
email: {
type: String, required: true,
trim: true, unique: true,
match: /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/
},
facebookProvider: {
type: {
id: String,
token: String
},
select: false
},
twitterProvider: {
type: {
id: String,
token: String
},
select: false
},
googleProvider: {
type: {
id: String,
token: String
},
select: false
}
});

UserSchema.set('toJSON', {getters: true, virtuals: true});

UserSchema.statics.upsertTwitterUser = function(token, tokenSecret, profile, cb) {
var that = this;
return this.findOne({
'twitterProvider.id': profile.id
}, function(err, user) {
// no user was found, lets create a new one
if (!user) {
var newUser = new that({
email: profile.emails[0].value,
twitterProvider: {
id: profile.id,
token: token,
tokenSecret: tokenSecret
}
});

newUser.save(function(error, savedUser) {
if (error) {
console.log(error);
}
return cb(error, savedUser);
});
} else {
return cb(err, user);
}
});
};

UserSchema.statics.upsertFbUser = function(accessToken, refreshToken, profile, cb) {
var that = this;
return this.findOne({
'facebookProvider.id': profile.id
}, function(err, user) {
// no user was found, lets create a new one
if (!user) {
var newUser = new that({
fullName: profile.displayName,
email: profile.emails[0].value,
facebookProvider: {
id: profile.id,
token: accessToken
}
});

newUser.save(function(error, savedUser) {
if (error) {
console.log(error);
}
return cb(error, savedUser);
});
} else {
return cb(err, user);
}
});
};

UserSchema.statics.upsertGoogleUser = function(accessToken, refreshToken, profile, cb) {
var that = this;
return this.findOne({
'googleProvider.id': profile.id
}, function(err, user) {
// no user was found, lets create a new one
if (!user) {
var newUser = new that({
fullName: profile.displayName,
email: profile.emails[0].value,
googleProvider: {
id: profile.id,
token: accessToken
}
});

newUser.save(function(error, savedUser) {
if (error) {
console.log(error);
}
return cb(error, savedUser);
});
} else {
return cb(err, user);
}
});
};

mongoose.model('User', UserSchema);

return db;
};
module.exports = {
'facebookAuth' : {
'clientID' : 'your-clientID-here',
'clientSecret' : 'your-client-secret-here',
'callbackURL' : 'http://localhost:4000/api/auth/facebook/callback',
'profileURL': 'https://graph.facebook.com/v2.5/me?fields=first_name,last_name,email'

},

'twitterAuth' : {
'consumerKey' : 'your-consumer-key-here',
'consumerSecret' : 'your-client-secret-here',
'callbackURL' : 'http://localhost:4000/auth/twitter/callback'
},

'googleAuth' : {
'clientID' : 'your-clientID-here',
'clientSecret' : 'your-client-secret-here',
'callbackURL' : 'http://localhost:4000/auth/google/callback'
}
};
'use strict';

require('./mongoose')();
var passport = require('passport');
var TwitterTokenStrategy = require('passport-twitter-token');
var User = require('mongoose').model('User');
var FacebookTokenStrategy = require('passport-facebook-token');
var GoogleTokenStrategy = require('passport-google-token').Strategy;
var config = require('./config');

module.exports = function () {

passport.use(new TwitterTokenStrategy({
consumerKey: config.twitterAuth.consumerKey,
consumerSecret: config.twitterAuth.consumerSecret,
includeEmail: true
},
function (token, tokenSecret, profile, done) {
User.upsertTwitterUser(token, tokenSecret, profile, function(err, user) {
return done(err, user);
});
}));

passport.use(new FacebookTokenStrategy({
clientID: config.facebookAuth.clientID,
clientSecret: config.facebookAuth.clientSecret
},
function (accessToken, refreshToken, profile, done) {
User.upsertFbUser(accessToken, refreshToken, profile, function(err, user) {
return done(err, user);
});
}));

passport.use(new GoogleTokenStrategy({
clientID: config.googleAuth.clientID,
clientSecret: config.googleAuth.clientSecret
},
function (accessToken, refreshToken, profile, done) {
User.upsertGoogleUser(accessToken, refreshToken, profile, function(err, user) {
return done(err, user);
});
}));
};
var express = require('express');
var router = express.Router();
var { generateToken, sendToken } = require('../utils/token.utils');
var passport = require('passport');
var config = require('../config');
var request = require('request');
require('../passport')();

router.route('/auth/twitter/reverse')
.post(function(req, res) {
request.post({
url: 'https://api.twitter.com/oauth/request_token',
oauth: {
oauth_callback: "http%3A%2F%2Flocalhost%3A3000%2Ftwitter-callback",
consumer_key: config.twitterAuth.consumerKey,
consumer_secret: config.twitterAuth.consumerSecret
}
}, function (err, r, body) {
if (err) {
return res.send(500, { message: e.message });
}
var jsonStr = '{ "' + body.replace(/&/g, '", "').replace(/=/g, '": "') + '"}';
res.send(JSON.parse(jsonStr));
});
});

router.route('/auth/twitter')
.post((req, res, next) => {
request.post({
url: `https://api.twitter.com/oauth/access_token?oauth_verifier`,
oauth: {
consumer_key: config.twitterAuth.consumerKey,
consumer_secret: config.twitterAuth.consumerSecret,
token: req.query.oauth_token
},
form: { oauth_verifier: req.query.oauth_verifier }
}, function (err, r, body) {
if (err) {
return res.send(500, { message: err.message });
}

const bodyString = '{ "' + body.replace(/&/g, '", "').replace(/=/g, '": "') + '"}';
const parsedBody = JSON.parse(bodyString);

req.body['oauth_token'] = parsedBody.oauth_token;
req.body['oauth_token_secret'] = parsedBody.oauth_token_secret;
req.body['user_id'] = parsedBody.user_id;

next();
});
}, passport.authenticate('twitter-token', {session: false}), function(req, res, next) {
if (!req.user) {
return res.send(401, 'User Not Authenticated');
}
req.auth = {
id: req.user.id
};

return next();
}, generateToken, sendToken);

router.route('/auth/facebook')
.post(passport.authenticate('facebook-token', {session: false}), function(req, res, next) {
if (!req.user) {
return res.send(401, 'User Not Authenticated');
}
req.auth = {
id: req.user.id
};

next();
}, generateToken, sendToken);

router.route('/auth/google')
.post(passport.authenticate('google-token', {session: false}), function(req, res, next) {
if (!req.user) {
return res.send(401, 'User Not Authenticated');
}
req.auth = {
id: req.user.id
};

next();
}, generateToken, sendToken);

module.exports = router;
mongod
npm start
MongoError: E11000 duplicate key error collection:

--

--

I help developers with easy-to-understand technical articles encompassing System Design, Algorithms, Cutting-Edge tech, and misc.

Love podcasts or audiobooks? Learn on the go with our new app.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store