Create a ReactJS client with Auth0 for a Serverless application (using Custom Authorizers)
We know how to deploy the application we created with the Serverless Framework in AWS. A CloudFormation script is created for us, which in turn will create our API Gateway, Lambda and DynamoDB instances.
Now it’s time to see some UI action.
I choose React and Webpack just because I started to learn them a few months back and this is an excellent opportunity to improve that knowledge. If you’re more into other frameworks like Angular, feel free to use them. The main concepts we will use here are not React-exclusive
Also, like we described in our previous post, the Serverless endpoints we have deployed are not yet protected. That’s something we want to fix as soon as possible.
Auth0 is a SaaS solution to handle user management and authentication. Not long ago they announced their new pricing plans, including a free plan for developers. They integrate with OAuth and AWS seamlessly, making it an excellent candidate for our sample application.
Why use a service like Auth0? Can’t we use something like AWS Cognito?You can. You can even integrate Auth0 with AWS Cognito if you wish.
Also, Auth0’s value is all the extra security infrastructure provided out of the box. Given that we want to forget about infrastructure (that’s why we are going serverless, after all) as much as possible; we can use Auth0 and basically forget about the security details that are behind it.
With Auth0 we don’t have to couple our web app to AWS as it works with multiple service providers. But aren’t we already coupling ourselves with AWS from the moment we choose AWS Lambda for our application? you might be asking yourself, dear developer.
Our back-end is unavoidably tied to AWS (for now), but our web application doesn’t have to. Since our client will be consuming REST endpoints, all the real implementation details are encapsulated.
With Auth0, a user can log in with social network credentials, like Facebook, and it returns JWT tokens containing data about the logged in user. For this first post we will use this token along a custom Authorizer function to validate if a valid user is making a request to the REST endpoint.
In future posts we will show how to use Auth0’s integration with AWS to use IAM authorization instead.
So, let’s get to it.
The first thing we want to do is create a new account with Auth0, if you don’t have one yet.
Create a new Client inside Auth0’s web console:
On the next screen, select “Single Page Web Applications”, and click “Create”:
Once created, you can go to the “Quick Start” tab in the Client page. Select “React” as the type of client application you want to use and you will something like the following:
Click in “Download” and a ZIP file will be downloaded into your computer. This file contains a pre-configured React application with Webpack, including environment data for your client. We will use this project as the base for our new web app.
Let’s make one single change to the application, before we run it. The project uses Webpack’s Dev Server for development, running by default on port 3000. Let’s change the port to 3001 to allow us to run the application at the same time as the serverless-offline plugin we have in our Serverless app.
In webpack.config.js, add a port attribute to the dev server, and change the publicPath attribute:
var config = getConfig({
isDev: isDev,
in: join(src, 'app.js'),
out: dest,
port: 3001,
html: function (context) {
return {
'index.html': context.defaultTemplate({
title: 'auth0 React Sample',
publicPath: isDev ? 'http://localhost:3001/' : '',
Run the project with the following command:
npm run start
Open your browser in http://localhost:3001 and you will see the new application running:
Before we log in, let’s add the URL “http://localhost:3001/login” to Auth0’s Client’s “Settings” tab, in the “Allowed Callback URLs” field to authorize our local app to log in using Auth0.
Now, log in using any social network and you will see a screen like the following:
Now, let’s modify our Serverless application, TacoGallery, to expose a new endpoint protected by a custom authorizer function.
We will use the same Lambda function the Serverless framework generated when we created our project, hello.
module.exports.hello = (event, context, callback) => {
var tacoGallery = new TacoGallery();
const response = {
statusCode: 200,
headers: {
"Access-Control-Allow-Origin" : "*", // Required for CORS support to work
"Access-Control-Allow-Credentials" : true // Required for cookies, authorization headers with HTTPS
},
body: JSON.stringify(tacoGallery.hello(event)),
};
callback(null, response);
};
The hello function definition is the following:
hello(event){
return {
message: 'Go Serverless v1.0! Your function executed successfully!',
input: event,
}
}
For test purposes, we want to generate two endpoints for this function: helloPublic which will be unprotected, and helloAuth0 which will be secured by a custom authorizer function.
We also need to create a new Lambda to be our authorizer function.
The function will receive the JWT token generated by Auth0 and decode it. If the token is valid the function will allow the request and attach a AWS policy to it, which will tell AWS which resources the user has access to.
I used this project as reference to create my own authorizer function: https://github.com/jghaines/lambda-auth0-authorizer?files=1. I used the script lib.js to decode and validate the request. I had to modify it to work with serverless-webpack, and removed some things I don’t want to support like saving the user information in DynamoDB. You can find my modified version here.
We put the custom authorizer function in our main handler.js file:
import lib from './lib'//...// Lambda function index.handler - thin wrapper around lib.authenticate
module.exports.auth = function( event, context ) {
try{
lib.authenticate( event )
.then( context.succeed )
.catch( err => {
if ( ! err ) context.fail( "Unhandled error case" );
// if ( err.message ) context.fail( err.message );
console.log(err);
context.fail( err );
});
}
catch(err){
console.log(err);
context.fail( err );
}
};
In our serverless.yml file, let’s change our function’s definition:
functions:
authorizerFunc:
handler: handler.auth
helloPublic:
handler: handler.hello
events:
- http:
path: helloPublic
method: get
cors: true
helloAuth0:
handler: handler.hello
events:
- http:
path: helloAuth0
method: get
cors: true
authorizer: authorizerFunc
Also note that we enabled CORS to allow our web application, which will be hosted in a different domain, to call the API Gateway endpoints.
More details in his configuration can be found here.
The token validation function depends on having OAuth0’s configuration to be able to decode the token, so let’s add environment variables in the serverless application. Add the following attributes to serverless.yml too:
provider:
name: aws
runtime: nodejs6.10
region: us-east-2
stage: dev
environment:
AUTH0_DOMAIN: <YOUR_CLIENT_DOMAIN>
AUTH0_CLIENTID: <YOU_CLIENT_ID>
You can get both YOUR_CLIENT_DOMAIN and YOU_CLIENT_ID from Auth0 Client configuration.
I created a policyDocument.json file inside a config folder, which will be the policy attached to the user’s request if the token is valid:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "Stmt1459758003000",
"Effect": "Allow",
"Action": [
"execute-api:Invoke"
],
"Resource": [
"arn:aws:execute-api:*"
]
}
]
}
The “arn” attribute in “Resource” allows access to all the endpoints in AWS. In a production environment, you want to restrict this to only grant access to you application’s endpoints. More info on ARNs can be found here.
As a last step, since the token validator uses Auth0’s NPM library, make sure to install it as a dependency in your Serverless app.
npm install --save auth0@2.5.0
Make sure not to install it using “ — save-dev” or serverless-webpack won’t include it in the build.
Also, the token parser function uses babel-preset-stage-2 internally, so let’s install that too:
npm install --save-dev babel-preset-stage-2
And add it to .babelrc:
{ "presets": ["es2015","stage-2"] }
Your serverless app is ready to be deployed. Let’s run it locally:
sls offline start -r us-east-1 --noTimeout
You should see in the console something like this:
> sls offline start -r us-east-1 --noTimeoutDynamodb Local Started, Visit: http://localhost:8000/shell
Serverless: DynamoDB - created table TacoGallery
Serverless: Starting Offline: dev/us-east-1.Serverless: Routes for authorizerFunc:Serverless: Routes for helloPublic:
Serverless: GET /helloPublicServerless: Routes for helloAuth0:
Serverless: GET /helloAuth0
Serverless: Configuring Authorization: helloAuth0 authorizerFunc
Try to access the endpoints:
Now, let’s modify our web app to consume these endpoints.
We will use axios to do AJAX requests to the API endpoint. In the web app project, install Axios if you don’t have it yet:
npm install --save axios
Inside views/main/Home/Home.js, add a variable to setup the main endpoint in the Home component:
constructor(props, context) {
super(props, context)
//... this.serviceEndpoint = 'http://localhost:3000';
}
And the following functions:
testPublic(){
var config = {};
axios.get(
this.serviceEndpoint+'/helloPublic',
config
).then( ( response ) => {
this.setState({
hello:response.data.message
})
} )
.catch((err) => {console.error(err)})
}
testAuth0(){
var token = localStorage.getItem('id_token');
var config = {
headers: {'Authorization': "Bearer " + token}
};
axios.get(
this.serviceEndpoint+'/helloAuth0',
config
).then( ( response ) => {
this.setState({
hello:response.data.message
})
} )
.catch((err) => {console.error(err)})
}
The testAuth0 function will do an AJAX request to the endpoint, including the Authorization header. The value of that header comes from the attribute id_token, which the project generated by Auth0 is putting in the browser’s local storage during login.
To finishe, update the Home render method:
render(){
const { profile, hello } = this.state
return (
<div className={styles.root}>
<h2>Home</h2>
<p>Welcome {profile.name}!</p>
<Button onClick={this.testAuth0.bind(this)}>Test Auth0</Button>
<Button onClick={this.testPublic.bind(this)}>Test Public</Button>
<Button onClick={this.logout.bind(this)}>Logout</Button>
<pre>{hello}</pre>
</div>
)
}
The web app should now show the following once you log in:
Click on “Test Auth0” and you should get the endpoint’s response successfully:
You can now deploy your serverless application to AWS, change the endpoint variable in the Home component, and you will have a Serverless application with secured endpoints which only allow requests from authenticated users.
This approach has it’s advantages and disadvantages: The authorize function assumes the JWT token is valid (including if the token is still valid in time) and most of the time this is a correct assumption. This makes validation quick and lightweight. However, for more delicate operations you might want to do extra validation to confirm the token wasn’t hand-crafted by a malicious user. An approach on how to do this is shown here. It doesn’t use Auth0, but the logic behind it is the same: once you receive the token, do an extra request against Auth0’s authorization server to confirm the token validity. This approach brings extra overhead, though, as it makes an extra request every time the function is executed. That’s the trade-off for extra security.
Custom authorize functions is just a way to authorize API Gateway execution. In the next post, we will go with an alternative approach, using AWS IAM to secure our endpoints.
Also, in future posts we will go through how to set scopes to grant different levels of access to users.