Source code:
Backend: https://github.com/alex-paterson/react-native-oauth-example-backend
Frontend: https://github.com/alex-paterson/react-native-oauth-example-frontend
If you find this tricky: take my course on full-stack development with React Native.
Introduction
I get asked about social authentication a lot, so here I'm going to include some code snippets for getting it set up in an application running Express/MongoDB on the backend, and React Native on the frontend.
I'll be using Facebook as an OAuth2.0 provider. Facebook, like Twitter and Google, have their own annoying idiosyncracies that you need to be aware of when using them as an authentication provider. For future reference, the readme for this package called react-native-oauth (which we won't be using) sums up the unique things you need to do to get Twitter/Facebook/Google Oauth2.0 working in your app. Other OAuth2.0 providers such as Dropbox require less setup on their end.
We won't be using Facebook's SDK because we don't need it and it hides how OAuth2.0 really works.
For your reference, I'll be using react-native-cli v1.0.0, and react-native v0.36.
Get started by creating a new project with react-native init OauthExample
.
How It'll Work
- The user hits "Facebook Login".
- Our app opens up a webview at the Facebook oauth URL (we include query parameters telling it where to redirect to afterwards).
- The user signs in.
- Facebook generates an authentication token, and redirects the user back to our app (with query parameter containing the authentication token).
- Our app sends this authentication token to our backend.
- Our backend queries the Facebook graph API with this token, getting the user's personal information.
- Our backend uses this information to either find the user in the database, or create a new entry for them.
- Our backend generates a new authentication token by hashing the user's unique identifier with a secret key.
- This token is sent back to the client to authenticate any future API calls to the backend.
Application Links
Extra application setup is required to use the react-native Linking library. Confusingly, we need to link the Linking library. The instructions for linking libraries with native code are here; you need to open up the xcode project file, and under Build Phases > Link Binary With Libraries, confirm the inclusion of RCTLinking.a. Then, under Build Settings, search for Header Search Paths and add a new one: $(SRCROOT)/../node_modules/react-native/Libraries
(and make it recursive).
Next, we need to add some code to our AppDelegate file, located here:
The new method we'll add will handle incoming links:
// AppDelegate.m
// Put this import up the top
#import "RCTLinkingManager.h"
//...
- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url
sourceApplication:(NSString *)sourceApplication annotation:(id)annotation
{
return [RCTLinkingManager application:application openURL:url
sourceApplication:sourceApplication annotation:annotation];
}
//...
Great. Now we'll register a custom URL scheme; after this, we'll be able to open our app by navigating to oauthexample://...
in safari. Add a new URL type under your Xcode project's Info > URL Types tab.
For the Identifier, use a unique reverse-domain style identifier (I used com.alexanderpaterson.oauthexample), and in the URL Schemes field, use oauthexample.
Now that's done. If you run the simulator and navigate to oauthexample://test
in safari, it'll ask to open up your app. Now, onto the actual authentication.
We need to start by registering a new app with Facebook.
The Facebook App
Navigate to developers.facebook.com, hit My Apps in the top right, and then select + Add A New App.
Use whatever you like for the Display Name, Contact Email, and Category.
After creating your app, select Settings > Basic and + Add Platform. Select iOS, and add a Bundle ID that is fb{YOUR_APP_ID}. Like this:
Also enable Single Sign On.
Now here's something specific to Facebook: this BundleID has to be the URL Scheme property of our custom URL Type, like this (I changed the identifier as well, to keep it unique if I add other URL Types):
Supposedly the Facebook URL Scheme has to be the first on the list for it to work, so watch out for that.
Now to write our app.
The React Native Application
Our app will have a button, that when pressed, takes a user to a Facebook URL where they can log in. We do this with the Linking library, and we also specify some options in the query parameters. You'll need your AppID here, too. Here's what your index.ios.js should look like:
// index.ios.js
/**
* Sample React Native App
* https://github.com/facebook/react-native
* @flow
*/
import React, { Component } from 'react';
import {
AppRegistry,
StyleSheet,
Text,
View,
TouchableOpacity,
Linking
} from 'react-native';
export default class OauthExample extends Component {
_handleURL(event) {
console.log(event.url);
// Bit of a hack to get the token from this URL...
// implement yours in a safer way
console.log(event.url.split('#')[1].split('=')[1].split('&')[0]);
}
_facebookLogin() {
Linking.openURL([
'https://graph.facebook.com/oauth/authorize',
'?response_type=token',
'&client_id='+'1247676148624015',
'&redirect_uri=fb1247676148624015://authorize',
'$scope=email' // Specify permissions
].join(''));
}
componentDidMount() {
Linking.addEventListener('url', this._handleURL);
}
render() {
return (
<View style={styles.container}>
<TouchableOpacity onPress={this._facebookLogin}>
<Text style={styles.welcome}>
Facebook Login!
</Text>
</TouchableOpacity>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#F5FCFF',
},
welcome: {
fontSize: 20,
textAlign: 'center',
margin: 10,
}
});
AppRegistry.registerComponent('OauthExample', () => OauthExample);
If you run the simulator, hit the button, and sign in, you should see the following logged in Xcode:
fb1247676148624015://authorize/#access_token=EAARuwT2iKo8BADVRh1qR5NjBZABSK6eWFSrn9ZBo6sTsoWQc4ZA9Rs9bmdWJHIxHiPzBjeZACEBq6nyXEC5ZCeXnrq00lwKHruxQg4fPB7AsrLjb8OuE4eeNRaocuyc3aWgKDjEdJwhqgAMp2Yysi5xpuAbSZBeZAAZD&expires_in=5183432
If you look closely, you'll see we now have an access_token. Awesome. Now what we'll do is write a backend to consume this token, double-check it against the Facebook servers, create or find a user, and send back a custom authentication token.
The Backend
Create a completely new directory called OauthExampleBackend next to your OauthExample react native project, cd
into it, and run npm init
, giving it a lowercase name. Install the packages we'll need by running npm install --save mongoose express axios
.
Now, obviously I'm writing a whole backend here, so there's going to be a bit of code. I'll just throw it all at you. If you can't understand it, take my course.
Starting with the model:
// models/user.js
var mongoose = require('mongoose');
var userSchema = new mongoose.Schema({
email: {
type: String,
unique: true,
lowercase: true,
required: true
},
facebook_id: {
type: String,
unique: true,
required: true
},
name: {
type: String,
required: true
},
});
module.exports = mongoose.model('user', userSchema);
And our only controller:
// controllers/auth_controller.js
var jwt = require('jwt-simple'),
User = require('../model/user'),
axios = require('axios'),
SECRET = require('../config').SECRET;
function tokenForUser (user) {
var obj = {
sub: user._id,
iat: new Date().getTime()
};
return jwt.encode(obj, SECRET);
}
exports.requireAuth = function(req, res, next) {
var authHeader = req.get('Authorization');
var jwtToken = jwt.decode(authHeader, SECRET);
var user_id = jwtToken.sub;
User.findById(user_id, function(err, user) {
if (err) { return next(err); }
if (!user) { return next(new Error("User not found.")); }
req.user = user;
next();
});
}
exports.facebookAuth = function(req, res, next) {
var token = req.body.token;
axios.get(`https://graph.facebook.com/v2.8/me?fields=id,name,email&access_token=${token}`).then(function (response) {
var facebook_id = response.data.id;
var name = response.data.name;
var email = response.data.email;
User.find({facebook_id: response.data.id}, function(err, users) {
user = users[0];
if (err) { return next(err); }
if (!user) {
var user = new User({
facebook_id: facebook_id,
email: email,
name: name
});
user.save(function(err) {
if (err) { return next(err); }
res.json({token: tokenForUser(user)});
});
} else {
res.json({token: tokenForUser(user)});
}
});
}).catch(function(error) {
return next(error);
});
}
A config file:
// config.js
module.exports = {
SECRET: "This is a secret. Keep it out of version control."
}
And an index file:
// index.js
var express = require('express'),
morgan = require('morgan'),
mongoose = require('mongoose'),
bodyParser = require('body-parser'),
AuthController = require('./controllers/auth_controller');
var app = express();
var router = express.Router();
mongoose.connect('mongodb://localhost:testOauth/testOauth')
var protectedAction = function(req, res) {
res.send("Here's some protected information!");
}
router.route('/facebook_auth')
.post(AuthController.facebookAuth);
router.route('/protected')
.get([AuthController.requireAuth, protectedAction]);
app.use(morgan('combined'));
app.use(bodyParser.json({type:'*/*'}));
app.use('/v1', router);
app.listen(3000, function(err) {
if (err) { return console.log(err); }
console.log("Listening on port 3000.");
});
Run this simple server with node index.js
.
Updating The Frontend
Simply replace your index.ios.js file with the following:
// index.ios.js
/**
* Sample React Native App
* https://github.com/facebook/react-native
* @flow
*/
import React, { Component } from 'react';
import axios from 'axios';
import {
AppRegistry,
StyleSheet,
Text,
View,
TouchableOpacity,
Linking
} from 'react-native';
export default class OauthExample extends Component {
_facebookLogin() {
Linking.openURL([
'https://graph.facebook.com/oauth/authorize',
'?response_type=token',
'&scope=email',
'&client_id='+'1247676148624015',
'&redirect_uri=fb1247676148624015://authorize'
].join(''));
}
componentDidMount() {
Linking.addEventListener('url', (event) => {
// This is all the login logic that takes place after the user is redirected
// back to the app from the third party site.
var facebookToken = event.url.split('#')[1].split('=')[1].split('&')[0];
axios.post('http://localhost:3000/v1/facebook_auth', {token: facebookToken}).then((response) => {
var token = response.data.token;
// Save this token and use it for future protected API calls like this:
// axios.get('http://localhost:3000/v1/protected', {headers: {authorization: token}}).then((res) => {
// console.log("Successfully made authenticated API call!!");
// }).catch((err) => {
// console.log(err);
});
}).catch((err) => {
console.log(err);
});
});
}
render() {
return (
<View style={styles.container}>
<TouchableOpacity onPress={this._facebookLogin}>
<Text style={styles.welcome}>
Facebook Login!
</Text>
</TouchableOpacity>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#F5FCFF',
},
welcome: {
fontSize: 20,
textAlign: 'center',
margin: 10,
}
});
AppRegistry.registerComponent('OauthExample', () => OauthExample);
We now have a frontend and backend with a simple Facebook OAuth2.0 login flow.