Social Login in React Native + Expo.io and AWS API Gateway + Lambda Authentication

Part 3 of Implementing an Authentication Flow in React Native

Introduction

This article is part 3 in a series on adding authentication to a react-native with redux application built in Expo.io that communicates to a server created using AWS API Gateway and Lambda functions. That’s a mouthful, but I was trying to be specific. However, I’m realizing more and more that the concepts presented here are not limited to just those technologies.

This series is also relevant to anybody that wants to implement token based authentication and authorization. Its relevant to those that want to become more familiar with API Gateway, Lambda functions, or AWS in general. Its also relevant to anyone that would like to implement social logins in there application. While I’m using the above group of technologies, the import thing to take away is the process, not the stack.

In this post I’ll finally get to actually adding social login on the client and performing authorization & authentication functionality on the serverless backend. The code for this application is located at the step2_add_authorizer_functionality of this Github repo.

Design

I’ll start with the UI from my previous post. The main additions to the UI for the application are pretty simple; add buttons that the user can click to begin the social login process, and add another button to enable the user to sign out (my sign out button is in a popup menu). Additionally, I added a react-native image component to display the user’s profile picture when they are logged in.

Since Expo.io provides APIs for Facebook and Google login, I will implement both of those services. Expo also does a great job of walking through the process of signing an application up to use both services. Following a services application signup process is a requirement to enable social login. You basically need to tell each service your application’s name in order to receive an application ID which identifies your application to the service, and an application secret that really identifies you the the service. The secret should never exist on the client since you don’t really have control of the client machine; there is nothing stopping an enterprising malicious user from determining your secret and using it willy-nilly all over the internet. Keep the secret on the server, which is something you can control.

UI with Social Login Buttoins. Note the Profile Picture Indicating this User is Signed In.

In AWS, I will create an additional Lambda function to be the authorizer and then set up API Gateway to use that authorizer to authenticate and authorize access to the APIs created in the previous article.

Client Social Login Implementation

I created components to render two buttons; one for each of the social login services to be implemented. The code for the Facebook button is shown below:

import { View, StyleSheet, Text, TouchableHighlight } from ‘react-native’;
import Icon from ‘react-native-vector-icons/FontAwesome’;
import { globalStyles } from ‘../globals/styles’;
const iconName = ‘facebook’;
export default FaceBookSignInButton = (props) => {
let renderable = (
<TouchableHighlight
underlayColor=’#99d9f4'
onPress={props.onPress}>
<View style={styles.buttonContainer}>
<Icon name={iconName} color=”white” size={25} />
<Text style={styles.btnText}>Sign In with Facebook</Text>
</View>
</TouchableHighlight>
)
if (props.type === ‘small’) {
renderable = (
<TouchableHighlight
underlayColor=’#99d9f4'
onPress={props.onPress}>
<View style={styles.buttonContainerSmall}>
<Icon name={iconName} color=”white” size={25} />
</View>
</TouchableHighlight>
)
}
return renderable
}
const backgroundColor = “#3b5998”;
const styles = StyleSheet.create(Object.assign({}, globalStyles, {
buttonContainer: {
flex: 1,
flexDirection: ‘row’,
alignItems: ‘center’,
backgroundColor,
paddingVertical: 7,
paddingHorizontal: 7,
borderRadius: 5,
justifyContent: ‘center’
},
buttonContainerSmall: {
justifyContent: ‘center’,
alignItems: ‘center’,
backgroundColor,
paddingVertical: 7,
paddingHorizontal: 7,
borderRadius: 5,
width: 48,
height: 48,
},
btnText: {
fontSize: 18,
color: ‘#FAFAFA’,
marginLeft: 10,
marginTop: 2,
}
}));

I decide to include the ability to render either a small or large button. The size of the button is dictated by a prop set by the user. Also, the button receives the function that will dispatch the action as a prop. This function is called when the button is pressed

The buttons are invoked in the parent component using the following code:

<FaceBookSignInButton
type=’small’
onPress={this.signInSocial.bind(this, ‘facebook’)} />

The function signInSocial contains a switch statement that dispatches the proper sign in type based on which social login method is selected; Google or Facebook.

signInSocial(social) {
switch (social) {
case ‘facebook’:this.props.dispatch(actions.authenticationActions.signInFacebook());
break;
case ‘google’:
this.props.dispatch(actions.authenticationActions.signInGoogle());
break;
}
}

Since both of Expo’s social sign in functions are asynchronous, I used the same promise pattern that I introduced in previous posts. This pattern created by combining redux-promise-middleware and redux-saga results in events being dispatched at specific points in the promise’s life cycle. The action that begins the process is shown below.

signInFacebook: function () {
return {
type: ‘SIGN_IN_FACEBOOK’,
payload: authenticationUtils.signInFacebook()
}
}

The signInFacebook function wraps the asynchronous Expo call Expo.Facebook.logInWithReadPermissionsAsync inside of a promise:

export const signInFacebook = () => {
return new Promise(function (resolve, reject) {
let accessToken = ‘’;
  Expo.Facebook.logInWithReadPermissionsAsync(appSecrets.facebook.clientID, {
permissions: [‘public_profile’, ‘email’, ‘user_birthday’],
})
.then((response) => {
switch (response.type) {
case ‘success’:
// token is a string giving the access token to use
// with Facebook HTTP API requests.
return response.token;
case ‘cancel’:
reject({
type: ‘error’,
msg: ‘login canceled’
})
break;
default:
reject({
type: ‘error’,
msg: ‘login failed’
})
}
})
.then((token) => {
accessToken = token;
return fetch(`https://graph.facebook.com/me? fields=id,name,email,birthday&access_token=${token}`);
})
.then((response) => {
return response.json();
})
.then((facebookJSONResponse) => {
console.log({ facebookJSONResponse });
if (facebookJSONResponse.hasOwnProperty(‘error’)) {
reject({
type: ‘error’,
});
}
resolve({
type: ‘success’,
credentials: Object.assign({}, facebookJSONResponse, { accessToken })
});
})
.catch(function (error) {
reject({
type: ‘error’,
msg: ‘Facebook login failed’
})
});
});
}

In the event of a successful login, I store the information that is returned inside the redux store. Unfortunately, each service returns different data, or sometimes the same data with different names, or even different data with the same names. When it comes down to it, you can’t expect normalized data from each service, so read the docs, or inspect the responses to find out what is what, and store what you need. Creating a normalized object that contains the required data with standardized names irregardless of the service used is the next step. The reducer that does this for SIGN_IN_FACEBOOK_FULFILLED events is shown below:

case ‘SIGN_IN_FACEBOOK_FULFILLED’:
if (action.payload.type !== ‘error’)
return Object.assign({}, authentication, {
signedIn: true,
type: ‘facebook’,
credentials: action.payload.credentials,
userInfo: {
id: action.payload.credentials.id,
name: action.payload.credentials.name,
email: action.payload.credentials.email,
accessToken: action.payload.credentials.accessToken,
birthday: action.payload.credentials.birthday
}
});
else
return authentication;

This code first looks at the type of payload that was sent. As seen in signInFacebook function, every response resolves to the SIGN_IN_FACEBOOK_FULFILLED reducer. However, successful logins are the only ones that resolve with a type of success , resulting in saving the credentials held in the response being saved to the redux store. Any response that does not have a type of success just returns an unaltered store.

I originally stored excess user specific information due to the mistaken notion that I might need it to create a robust authentication process. Fortunately, the services take care of that problem, and the only thing required to be sent to the server for authentication is the accessToken.

Server(less) Implementation

The API Gateway created in the previous post has to go through a couple of changes in order to enable custom authorizer usage. Additionally, an additional Lambda function must be created to act as the actual authorizer. AWS does of good job of documenting this process here. It goes over how to create allow and deny access policies which enable and allow access to the POST, GET, PUT, and DELETE Lambdas. The Lambda used in Amazon’s example does not incorporate the access tokens obtained by the client and passed to the user; however, that is exactly what we need to be able to do in order to authenticate the user. We are required to pass the required authentication data in a header.

The only data that the authorizer receives from the client is the designated “Identity token source” passed through the API Gateway from the client. The Identity token source is set during the custom authorizer setup in API Gateway dialog box.

I set up my authorizer to recognize that the Auth header contains the authentication information passed from the client. Since I need send more than one piece of information to my authorizer I made the client application build a “||” delimited string that contains the name of the social login service used, the access token, the service id for the user, and the email address. The email address isn’t required for authentication, but can be used for additional checks if desired. This authorizer splits the string on the delimiter and places the elements in an array so that I can access each element individually.

exports.handler = (event, context, callback) => {
// console.log(util.inspect(event, { showHidden: true, depth: null }));
// use split to parse out the pieces of data in the authentication header
let authorizationInfo = event.authorizationToken.split(“||”);
let auth = {
service: authorizationInfo[0],
accessToken: authorizationInfo[1],
id: authorizationInfo[2],
email: authorizationInfo[3]
}
//console.log(util.inspect(auth, { showHidden: true, depth: null }));
const token = auth.accessToken;

switch (auth.service) {
case ‘google’:
verifyGoogleToken(auth, callback, event);
break;
case ‘facebook’:
verifyFacebookToken(auth, callback, event);
break;
default:
callback(“Error: Invalid authentication service”);
break;
}
};

The authorizer then executes a verification function based on the requirments of social service that the user chose. The verification function used for Facebook tokens is shown below.

let verifyFacebookToken = (auth, callback, event) => {
return fetch(`https://graph.facebook.com/v2.9/debug_token? input_token=${auth.accessToken}&access_token=${facebookAppID}|${facebookClientSecret}`, {
method: ‘GET’
})
.then((response) => {
return response.json();
})
.then((json) => {
if (!json.data.is_valid) {
callback(null, generatePolicy(‘user’, ‘Deny’, event.methodArn));
console.log(‘invalid Facebook token’);
return ({
type: ‘error’,
msg: ‘invalid Facebook token’
})
}
else {
console.log(‘token valid’);
callback(null, generatePolicy(‘user’, ‘Allow’, event.methodArn));
return ({
type: ‘success’,
msg: ‘valid token’
})
}
})
}

Each service has a http endpoint that accepts an access token and respond with information on whether the access token is valid. Facebook requires a call to the Graph API “debug” endpoint documented here. Google documents how to authenticate their access token here.

One difference between the two services is that Google will tell you whether a token is valid by just passing the token, Facebook requires information identifying the application requesting the information. My server application uses the same application ID and client secret that my client uses. Also, Facebook includes an isValid key in the response which directly indicates token validity, while Google documents a different technique for checking.

Successful and unsuccessful token validations result in a call to generatePolicy with allow and deny effects respectively. generatePolicy creates the appropriate policy that either allows or denies access to the POST, PUT, GET, and DELETE Lambdas created in the previous post.

let generatePolicy = function (principalId, effect, resource) {
let authResponse = {};
authResponse.principalId = principalId;
if (effect && resource) {
let policyDocument = {};
policyDocument.Version = ‘2012–10–17’; // default version
policyDocument.Statement = [];
let statementOne = {};
statementOne.Action = ‘execute-api:Invoke’; // default action
statementOne.Effect = effect;
statementOne.Resource = resource;
policyDocument.Statement[0] = statementOne;
authResponse.policyDocument = policyDocument;
}
return authResponse;
}

Implement Changes to API Requests Functions

Now an authorizer stands between the Lambdas and our client. In order to get past that authorizer the required authentication information must be passed inside the header that the authorizer will receive. Fortunately, this can be done by making a small alteration to each of the HTTP request methods introduced in the previous post. You can see that an Auth header was added to the POST example shown below. This header contains the information that the authorizer will receive and use for authentication.

export const doPost = (service, userInfo) => {
return fetch(appSecrets.aws.apiURL, {
method: ‘POST’,
headers: {
‘Auth’: createAuthorizationString(service, userInfo)
},
body: JSON.stringify({
‘bodyParam1’: ‘this is the first param’,
‘bodyParam2’: ‘this is the second param’
})
})
.then((response) => {
if (response.status !== 200) {
return handleErrors(response.status);
}
else {
return response.text();
}
})
.then((response) => {
return handleResponse(response);
})
.catch(function (err) {
console.log(“Error: “, err);
})
}

The createAuthorizationString function builds the “||” delimited string mentioned previously.

const createAuthorizationString = (service, userInfo) => {
// either make sure this string is on one line or strip out characters that are
// not allowed in http headers by some APIs
let authString = `${service}||${userInfo.accessToken}||${userInfo.id}||${userInfo.email}`;
return authString;
}

As said before, each service requires different information for authentication. This means that the authorizer needs to know which service is being used each time it is called. Additionally, each service requires the access token since that is the actually item being checked. I also include the user’s service ID and email in case those two pieces of information are needed for any future authentication services I decide to include.

Finishing Touches

There are two final things to do; give the user the ability to sign out, and display some feedback to let the user know they are signed in.

Signing out is fairly simple. All we have to do is remove any credential information saved in the redux-store. I do this by dispatching an action when the user signs out that results in a reducer setting the credentials in the store to an empty object.

Additionally, I display the user’s profile picture as an indicator that they are logged in. Google sends a URL to the user’s profile picture along with the access token as part of the response to the initial sign in call. However, Facebook requires an additional call to a separate endpoint to get the picture. Obviously, that call requires you to have a token first. The sequence of logging in and then getting a picture is perfect for redux-saga. The following saga is run after a successful Facebook login:

function* getFacebookPictureAfterLogin(action) {
if (action.payload.type === 'success') {
console.log({ action })
yield put({
type: "GET_FACEBOOK_PROFILE_PICTURE",
payload: authenticationUtils.getFacebookProfilePicture( action.payload.credentials.accessToken,
action.payload.credentials.id)
});
}
}
export function* sagaGetFacebookPictureAfterLogin() {
yield takeEvery(['SIGN_IN_FACEBOOK_FULFILLED'], getFacebookPictureAfterLogin);
}

This redux-saga dispatches the GET_FACEBOOK_PROFILE_PIC TURE action that calls the following getFacebookProfilePicture function:

export const getFacebookProfilePicture = (accessToken, facebookUserID) => {
return new Promise(function (resolve, reject) {
fetch(`https://graph.facebook.com/${facebookUserID}/picture? redirect=false&type=large&access_token=${accessToken}`)
.then((response) => {
return response.json();
})
.then((json) => {
if (json.hasOwnProperty(‘error’)) {
reject({
type: ‘error’,
});
}
resolve({
type: ‘success’,
pictureData: json
});
})
.catch(function (error) {
console.log(‘Request failed’, error);
reject({
type: ‘error’,
msg: ‘failed to get Facebook picture’
});
});
});
}

Once you have the picture URL from either service you can display it where ever you want. I choose to display it in the header. I then use conditional rendering in react to only display the picture when the user is signed in.

Conclusion

Hopefully, this post helped to clarify how to combine react-native, Expo, social logins, and AWS to build upon the functionality created in previous posts. Next, I will go into caching the authentication data to local storage so that the user does not have to log in every time they open the application.

Reginald Johnson has maintained his passion for coding throughout his 20+ year career as an Officer in the United States Navy. He enjoys applying his training and experience in programming, Systems Engineering, and Operational Planning towards programming. Follow him Twitter @reginald3.