Implement social authentication with React + RESTful API

Alexander Leon
Nov 4, 2017 · 14 min read

I have found it difficult to find an example that demonstrates how to implement social authentication when using a RESTful API, so I figured I would write about it to save you the peril (it’s really not so bad).

We’re going to implement Facebook, Google, and Twitter authentication using a React frontend and Node backend.

If all you need is Twitter authentication, simply follow this awesome tutorial: https://medium.com/@robince885/how-to-do-twitter-authentication-with-react-and-restful-api-e525f30c62bb.

If you’d rather see an example with an Angular frontend, check this one out: https://github.com/GenFirst/angular2-node-fb-login

The Gist

Once you get the gist of how to go about implementing social login, the sky will be the limit as to which framework, language, or database you choose to use in the end. So here goes:

Step 1: On the frontend, get the 3rd party app’s login popup to appear.

Step 2: (Still on the frontend) Grab the authentication token the 3rd party app returns after agreeing to login.

Step 3: (Yep, still frontend) Send that token to the backend as part of the body of your request. (I like to use Blobs, but that’s just me.)

Step 4: On the backend, verify the token.

Step 5: If the token is authentic, you will receive the user as part of the verification response (at least that’s the case with Passport.js, which we’ll be using).

Step 6: Save the user’s data to your database.

Step 7: Return a JWT token to the frontend. What you do with that token is out of scope for this tutorial, but it should probably be used to authenticate each of the logged in user’s actions.

There you have it, the skeleton for creating social login.

By the way, before I forget, handling that JWT token properly is a non-trivial problem. Since you’re not using Rails that has security precautions for things you didn’t know were possible, you’ll have to devise procedures to at least avoid:

CSRF attacks: a malicious internet user grabbing your JWT token and pretending to be you.

XSS attacks: remember that cute kitten picture from that article You won’t believe what this adorable cat did next? Well, while you were fawning over it and showing it to your friends, that image had an IIFE javascript script that ran queries on your app, and your app didn’t stop it because technically you were logged in and that JWT token was readily available.

OK, rant over.

Let’s roll

We’ll implement Facebook, Google, and Twitter authentications simultaneously so we’re not constantly retracing our steps. Let’s add a directory that will be home to the frontend and backend. Run the following:

mkdir social-auth-example && cd social-auth-example

Let’s use create-react-app for our frontend.

Run the following if you don’t have the module globally yet:

npm i -g create-react-app

and then run

create-react-app frontend && cd frontend

You can double-check that everything installed correctly by running the following:

npm start

and visiting the URL where the new React app is being hosted. It’s also a good idea to get a local copy of react-scripts (so as to not always rely on the global one), so let’s install that.

npm i --save react-scripts

That’s pretty much it for configuring the frontend. Looking back at our cookbook for authentication, the first step is to get those login popups to appear. If you want to implement that yourself, have at it. I’m going to use some open-source solutions for this.

Run the following:

npm i --save react-twitter-auth react-facebook-login react-google-login

Each of these modules has pretty documentation, so I recommend checking that out if something goes awry. This is what our skeleton should generally look like:

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;

If you visit the page now, you should see three hideous looking buttons. If the app failed to compile, installing your node_modules again is probably the solution:

npm i

If that worked for you, let’s take another glance at the code. We have three button components that will respond to our requests. If the response is successful we’ll update the local component state. isAuthenticated will become true, user will hold some of your user data, and token will be your JWT token.

Note: In a real-life application, you’ll probably want to store that data in a storage system like Redis or LocalStorage that’s persistent across sessions.

OK, this is the point where I have to send you to do some errands before continuing with the tutorial. This is a fairly tedious process. You’ll need to fetch authentication IDs, secrets, and permitted redirect URLs.

Fine, I can help out a bit:

Twitter

For Twitter, you can get that info here: https://apps.twitter.com

Step 1: Click the Create New App button

Step 2: Go to permissions and select the checkbox about ‘Requesting emails from users.’ We’ll want the email addresses.

Step 3: Copy down the Consumer Key and Consumer Secrets values somewhere private.

Step 4: Under the Settings tab, fill out the required fields. If you’ve made a Github account for this tutorial, use that for Website. Otherwise, just use this tutorial’s repo: https://github.com/alien35/social-auth-example

Google

Step 1: Go to the developer console: https://console.developers.google.com/

Step 2: Look up ‘oauth credentials’ in the search bar, and click the single option that pops up.

Step 3: Try to find the ‘Create credentials’ button. If you find it, go ahead and click on it. Choose ‘Oauth Client Id’ (figure 1).

Figure 1

For application type, choose web application. Authorized origins and redirect URLs are a bit confusing, so I’ve just copy pasted a bunch of different options (figure 2) that have worked for me in the past. For production settings, you’ll probably want to be a bit more specific.

Figure 2

Step 4: Click save, and copy down the Client Id and Client Secret values that are hiding somewhere on the same page.

Facebook

Step 1: Go to https://developers.facebook.com/apps/ and select ‘Add a new app.’

Step 2: Give your app a name and complete the security question.

Step 3: If you see an option to select products, choose ‘Facebook Login.’

Step 3: Go to Settings and under App Domains type ‘localhost.’

Step 4: Click Facebook Login under Products in the side bar, and add the following redirect url: http://localhost:3000/api/auth/facebook/callback

That should do it. If you come across redirect URI issues when trying to use social login, you’re definitely not the first, and you’ll find plenty of documentation online (though no more from this tutorial).

From the root of the frontend app, let’s run a command to create a config file for our newly created credentials:

touch src/config.json

Add something like the following to this file:

{
"GOOGLE_CLIENT_ID": "XXXXX",
"FACEBOOK_APP_ID": "XXXXX"
}

Replace the X’s with your Google Client Id and Facebook App Id. If you wish, add this file to your .gitignore file. Regardless, this data is publicly available, so whatever you do, don’t add any of your secret credentials anywhere in the .

Let’s use these values in App.js. That file should now look something like the following:

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;

Give the Google and Facebook buttons a click, and check out your console for responses. Hopefully you see a bunch of data including an ‘authToken’ value. As you may have noticed, Twitter authentication has a slightly different flow. While for Google and Facebook we request the token in the frontend and verify it in the backend, for Twitter we will both request a token and verify it in the backend. We’re doing this just because of how that button was designed.

HERE’S A CHALLENGE: If you wish to write an open-source react-twitter button that obtains the authToken in the frontend based on the Twitter consumer key, I’d happily use that instead.

Let me give you the rest of the frontend logic:

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;

For Facebook and Google, based on the response of the login popup, we will submit the access token as part of the body of our HTTP request to the backend. The backend will do something with it (hint hint: verify the token and then send back a user if it’s valid + a JWT token). We’re using the blob data type because I think it’s cool, but feel free to use any accepted data type.

With a successful backend response, we will set isAuthenticated to true, render the email on the screen, and store the JWT token. For Twitter, the backend will listen to both http://localhost:4000/api/v1/auth/twitter and http://localhost:4000/api/v1/auth/twitter/reverse. Based on the final response, we will also be able to render the email and store the backend-granted JWT token in the frontend.

Backend

Go to the root of the app and create a backend folder:

mkdir backend && cd backend

Let’s get an express app running. Run the following to obtain the global express generator:

npm i express-generator -g

Now, verify you’re inside the /backend folder and run the following:

express && npm i

Let’s clean up the server a bit. Run the following:

npm i --save express-server utils

and then modify bin/www to the following

#!/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);

OK, get your server up and running:

npm start

Hopefully you’re not getting any errors. If you navigate to http://localhost:4000 you should see the message ‘Welcome to express.’

Brace yourself

We have to do a lot of tedious configuration now.

  1. We have to setup Passport and connect it to our authentication routes to validate our tokens from the front end.
  2. We will also need to setup Mongo to handle storing our users.

I can hear you thinking: ‘Not another Mongo example!’ Well, to use a relational database properly you’d probably have to at least setup a migration and ORM system, which would make this tutorial unnecessarily longer. After this tutorial, I’d recommend checking out this tutorial: https://medium.com/@alexanderleon/creating-a-scalable-api-using-node-graphql-mysql-and-knex-710a1a475ff4

From there you can probably figure out how to replace what we do in Mongo with MySQL.

To get the configuration done as quickly as possible, let’s rapid fire our file structure skeleton. From the root of the backend app, run the following:

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

With that script, we setup our skeleton, installed the token validation libraries, installed the JWT token creator tools, installed CORS to enable communicating between the frontend app and backend app, and installed request which we’ll use to send an extra HTTP request to Twitter.

Replace /app.js with the following:

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;

Replace /utils/token.utils.js with the following:

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));
}
};

Replace /mongoose.js with the following:

'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;
};

Replace /config.js with the following (go ahead and add your keys and secrets):

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'
}
};

Replace /passport.js with the following:

'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);
});
}));
};

Replace /routes/index.js with the following:

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;

With that said and done, let’s get the server working again. Get the Mongo daemon by running the following command in a separate window:

mongod

Now, from the root of the backend app, run:

npm start

If all that worked correctly, return to your frontend app and give those buttons a click. They should all work now. If you’re like me, you will inevitably have the same email for more than one social platform. Your server will return a 500 error and will complain with something like the following:

MongoError: E11000 duplicate key error collection:

For testing purposes, just delete the content in your database before trying out the next social button. How you handle this in production settings is up to you. Options that come to mind are:

  1. Checking if that email is already registered and if it is, returning a friendly error like ‘Looks like you already registered with Facebook.’
  2. Checking if that email is already registered and if it is, linking the two social accounts together under the same user.

Anyways, that’s all folks!

Check out the source code for this tutorial here: https://github.com/alien35/social-auth-example

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade