SSO with Firebase, React, Auth0 and Circle.so

Maté Gvo
7 min readJan 17, 2021

--

In this walk-through tutorial, I will explain how we implemented SSO (Single Sign On) using React, Firebase and a third party community management platform.

A bit of background… Our project has started as a React app backed with Firebase. As we have grown we have decided to use a third party community management platform, that would also become essential to our user experience. Therefore we needed a simple way of signing users into both our React application and the community platform. Firebase does not provide SSO functionality so we needed to combine it with another solution, we needed an oAuth-SSO server. At that point we already had a few thousand users who signed in to to our React app using Firebase, we had to make sure that they will be able to sign into the app without interruptions as well as a few other requirements:

  • we wanted to have control over who is signing into our app and the community platform,
  • have ease of adding sign-in methods and extra security layers such as multi-factor-authentication,
  • have password-less sign-in option for kids, who are generally not good at remembering and keeping passwords
  • existing users be able to sign in to their accounts without interruptions

We decided to go with auth0 as it’s inexpensive or even free to use until 7,000 monthly active users (users who signed in given month), and a couple other options: Lock for Web, iOS & Android, Up to 2 social identity providers

Nonetheless I see auth0 as a temporary solution. oAuth that auth0 based it’s business on is a standard that’s easy enough to implement when you want to have complete control over users authentication. At early stage using auth0 will help us save time and money though, as it provides social connections such as Google or Facebook auth, phone verification, password reset etc.

The implementation flow

Have a look at the overview of the implementation flow. You will find code samples in the following part.

Step I. Creating log-in button

Let’s start by creating an Auth0 log in button. It’s purpose is to redirect the user to auth0 log-in page. Auth0 provides a library that can do this and handle a session, but it doesn’t make sense to use it in our case. Creating the button is as simple as creating an anchor or clickable element

<PrimaryButton onClick={redirectToAuth0}>
Log in
</PrimaryButton>

The auth0 package is also capable of taking care of sessions, converting the authorization code into access token, and signing the user in after successful login. But we don’t want to use this functionality. Our React app is already using session management and we don’t want to be rewriting it now. Therefore we will create our own onClick handler for the log-in button:

redirectToAuth0 = () => {
try {
const params = qs.stringify({
response_type: 'code',
// auth0 application client id
client_id: process.env.REACT_APP_AUTH0_CLIENT_ID,
// this is redirect uri to which user is redirected after successful login
// it must match "valid redirect URIs" in auth0 backend
redirect_uri: process.env.REACT_APP_AUTH0_REDIRECT_URI,
scope: process.env.REACT_APP_AUTH0_SCOPE,
});

// finally we redirect the user to the log-in page
// https://dev-00xx0-x0.us.auth0.com/...
window.location.replace(
`${process.env.REACT_APP_AUTH0_DOMAIN}/authorize/?${params}`,
);
} catch (err) {
console.error('Error redirecting to auth0 log-in page',err)
}
}

Now after clicking log-in user should see a log in page like this:

You can customise and even entirely redesign this page in your auth0 dashboard

Step II. Capturing an authorization code passing to Firebase functions

Upon successful sign-in, auth0 will redirect us back to the application, to the redirect_uri we provided earlier, amending the uri with ?code= param.

http://example.com/?code=1234567890

We need to grab this code and send it to our backend for verification. To do it correctly, we will format it as authorization_token This way when our backend won’t get messy receiving authorisation tokens from different apps

const authorizationToken = {
grant_type: 'authorization_code',
client_id: process.env.REACT_APP_AUTH0_CLIENT_ID,
redirect_uri: process.env.REACT_APP_AUTH0_REDIRECT_URI,
code,
};

const { data: firebaseToken } = yield auth0TokenExchange(
authorizationToken,
);

And in our firebase functions we register a new endpoint that will handle the exchange of auth0 authorisation codes tokens

export const auth0TokenExchange = functions.https.onRequest((req, res) =>
cors(req, res, () => auth0Helper.exchangeToken(req, res)),
);

Step III. Send authorization code to backend and handle different business cases

export const exchangeToken = async (req, res) => {
try {
// we will capture redirect_uri as it will tell us which app the user is trying to log into
// req.body.data and req.body handle two cases of the request format, coming from
// our react app or a third party app
const { redirect_uri, client_secret, ...rest } = req.body.data || req.body;

The sign-in methods we want to use for our Firebase app and the community login are different. For business-logic related reasons we will use Google and Facebook login with our Firebase app and passwordless email link for the community. To handle these two cases we need two different auth0 apps respectively:

We will need to switch between their secret

const clientSecret =
redirect_uri === studentsCommunityCallbackUrl
? auth0.firebaseApp.client_secret
: auth0.communityApp.client_secret;
const credentials = {
redirect_uri,
client_secret: client_secret || clientSecret,
...rest,
};

And handle cases separately:

switch (redirect_uri) {
case appCommunityCallbackUrl:
return res.send(await exchangeTokenWithFirebase(credentials));
case communityAppCommunityCallbackUrl:
return res.send(await exchangeTokenWithCommunityApp(credentials));
default:
throw new Error(
'Invalid redirect_uri. Please contact administrator.',
);
}

Notice the two methods here: exchangeTokenWithFirebase and exchangeTokenWithCommunityApp We will go through what they do in next step.

Finally, if anything goes wrong, we will return an error in oAuth compatible manner

} catch (err) {
return res.send({
error: 'ERROR',
error_description: err.message,
});
}

Step IV. Validate authorization code and get auth0 user

Now our backend needs to validate authorization code and use it to get user object from which we will get email address. We will be using email as unique identifier that connects users database on auth0 side and our firebase authorization module, to finally retrieve user details.

First step is to get auth token using auth code

const tokenRequestEndpoint = `${auth0Domain}/oauth/token?`;const getAuth0AccessToken = async (credentials): Promise<any> =>
axios
.post(tokenRequestEndpoint, qs.stringify(credentials))
.then(({ data }) => data.access_token);

Next we get user using the access token from the previous step:

return axios
.get(profileRequestEndpoint, {
headers: {
Authorization: `Bearer ${accessToken}`,
},
})

And since we are using email as unique identifier, we need to ensure it is returned in the response:

const getAuth0UserProfile = () =>
axios
.get(profileRequestEndpoint, {
headers: {
Authorization: `Bearer ${accessToken}`,
},
})
.then(({ data }) => {
if (!data.email) {
const err = new Error('Email is required');
return Promise.reject(err);
}

return data;
});

Now we finally have our user’s email address.

Step V. Getting user and responding with (the right) token

Finally we can send back the access token used to sign user into the app. There’s two cases here. For firebase we will use createCustomAuth function and for 3rd party apps, we will respond with JWT token.

Let’s get our user from database first

const user = await admin
.firestore()
.collection('users')
.where('email', '==', auth0Profile.email)
.get()
.then(({ docs }) => docs.map((doc) => ({ ...doc.data(), uid: doc.id })))
.then(([user]) => user);

A) Create JWT Token for third party apps

const createJWTToken = async (uid) => {
const jwtToken = await jwt.sign(
{ uid, exp: Math.floor(Date.now() / 1000) + 60 * 60 },
privateKey,
{
algorithm: 'RS256',
},
);

return {
access_token: jwtToken,
token_type: 'bearer',
expires_in: 3600,
scope: auth0Scope,
};
};

The above response is a standard way that any oAuth application should be able to handle.

B) Create Firebase Custom token https://firebase.google.com/docs/auth/admin/create-custom-tokens

const userId = 'some-uid';
const additionalClaims = {
premiumAccount: true,
};

admin
.auth()
.createCustomToken(userId, additionalClaims)
.then((customToken) => {
// Send token back to client
})
.catch((error) => {
console.log('Error creating custom token:', error);
});

This custom token can be now used to sign user into our React app

firebase.auth().signInWithCustomToken(<token>)

Next steps

We have successfully authenticated our user to both React and a third party oAuth application. There’s a couple more things to consider now.

If user doesn’t exist in our firebase database yet, we need to create an account:

const createAuthUser = async (user) => {
const userRecord = await admin.auth().createUser({
displayName: `${user.given_name || ''} ${user.family_name || ''}`,
email: user.email,
photoURL: user.picture,
emailVerified: user.email_verified,
disabled: false,
});

await admin
.firestore()
.doc(`users/${userRecord.uid}`)
.set({
firstName: user.given_name || '',
lastName: user.family_name || '',
email: userRecord.email
});

return userRecord;
};

Finally we can focus on business logic.

Example — Let’s allow only company email address to sign in. We can do so by throwing an error after fetching user from auth0

if (!user.email.indexOf('example.com')) {
throw new Error(
'Cannot log in. This is authorised personel only area.',
);
}

Thanks and I hope this has helped you connect different applications using auth0, oAuth, Firebase and React! If so, please leave a comment or “clap”.

Implementing SSO certainly isn’t easy or straightforward to do, but using SSO gives you extra control over third party applications, allowing to add business logic on top of apps that we so often need to extend the functionality of, making it well worth the time.

--

--