Create a ReactJS client with Auth0 for a Serverless application (using IAM)
In our last post we learned how to use Auth0 in a React application to authenticate users and allow them access to our Serverless application. That approach used JWT tokens produced by Auth0 and AWS custom authorize functions to validate them.
For today’s post we will use a different approach. We will leverage Auth0’s integration with AWS to get AWS Access Key ID and AWS Secret Access Key attributes through a delegation endpoint to access API Gateway endpoints secured using IAM.
This approach allows us to use the AWS SDK on our web app to not just access API Gateway endpoints, but also other AWS services. On the downside, it couples our web app with AWS and it requires a lot of extra configuration.
We will start where we left on our previous post. You can find the code in my Github repositories for the web app and the Serverless app.
To recapitulate, we used the React+Webpack web application generated by Auth0 to create the base of our web client. We created a Client in Auth0, and that client is used to consume our Serverless application endpoints. We created two Serverless endpoints for a hello function, which only returns the context data sent in the request; one public helloPublic and one protected helloAuth0. In this post we will create a third endpoint, helloIAM, to protect with IAM.
Let’s start bu updating our serverless.yml file in the serverless app:
functions:
authorizerFunc:
handler: handler.auth
helloPublic:
handler: handler.hello
events:
- http:
path: helloPublic
method: get
cors: true
helloIAM:
handler: handler.hello
events:
- http:
path: helloIAM
method: get
cors: true
helloAuth0:
handler: handler.hello
events:
- http:
path: helloAuth0
method: get
cors: true
authorizer: authorizerFunc
As you can see, the endpoint for helloIAM and helloPublic have the same configuration. This is because, the Serverless framework still hasn’t provided a way to enable IAM authorization in API Gateway endpoint. This also means that serverless-offline has no way to enable this kind of authorization locally.
In order to enable IAM authorization, you will need to deploy your serverless app to AWS:
serverless deploy
Now, open AWS web console and navigate to the API Gateway service were you can find your Serverless-generated endpoints. In “Authorization Settings” change the “Authorization” field to AWS_IAM:
Try to access the helloIAM endpoint:
Now the endpoint is protected.
In order to allow our web app to consume the API endpoint, we have to enable Auth0 delegation through Addons. I used this Auth0 tutorial on creating a Serverless application and integrating it with AWS and Setup AWS for Delegated Authentication with APIs as the base for this post. Try not ignore the parts about configuration on the web app, as it uses an older version of the Auth0, which is not the same version given by the generated React code. The following steps will show us how to bypass that limitation.
For this example, use the following policy for your auth0-api-role role:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"execute-api:*"
],
"Resource": [
"*"
]
}
]
}
Create a rule in Auth0 as mentioned in the section Setup AWS for Delegated Authentication with APIs of the Auth0 tutorial.
Use this image for better context:
The setup described in those tutorials is long and full of details. I don’t want to copy it in detail here, so try to make sense of it while applying it. If you have questions on specific details on how I got it to work, just leave a comment or Tweet me. I’ll update this post if more information is required.
Once we configured both Auth0 and AWS, we need to update AuthService.js in the web app generated by Auth0 to retrieve the AWS keys provided by Auth0.
Unfortunately, Auth0.js v8 SDK does not support token delegation. V7 used to provide a convenient method to get this AWS token, but it hasn’t been implemented in v8. The functionality is still provided, though, through a /delegation endpoint. We can still retrieve the information by doing an AJAX request to it with Axios.
export default class AuthService extends EventEmitter {
constructor(clientId, domain) {
super()
this.delegate = {
client_id:clientId,
grant_type:'urn:ietf:params:oauth:grant-type:jwt-bearer',
scope:'open_id',
api_type:'aws'
}
this.auth0Endpoint = domain;
// Configure Auth0
this.lock = new Auth0Lock(clientId, domain, {
auth: {
redirectUrl: `${window.location.origin}/login`,
responseType: 'token'
}
})
// Add callback for lock `authenticated` event
this.lock.on('authenticated', this._doAuthentication.bind(this)) //...
}//...._doAuthentication(authResult){
var ctrl = this;
// Saves the user token
this.setToken(authResult.idToken)
// navigate to the home route
browserHistory.replace('/home')
// Async loads the user profile data
this.lock.getProfile(authResult.idToken, (error, profile) => {
if (error) {
console.log('Error loading the Profile', error)
} else {
this.setProfile(profile)
}
})
let serialize = function(obj) {
var str = [];
for(var p in obj)
if (obj.hasOwnProperty(p)) {
str.push(encodeURIComponent(p) + "=" + encodeURIComponent(obj[p]));
}
return str.join("&");
}
axios({
method: 'get',
url: "https://"+this.auth0Endpoint+"/delegation?"+serialize(Object.assign({id_token:authResult.idToken},this.delegate))
})
.then((res) => {
localStorage.setItem('awstoken',JSON.stringify(res.data.Credentials))
});
}
The highlights are:
- Configure the delegate and auth0Endpoint attributes exactly as you see in the previous code.
- Use axios to send a request to your client’s Auth0 delegation endpoint.
- Retrieve the Credentials property from the response and put in local storage.
With awstoken, now we can build a client to consume the API endpoint. It contains a Consumer Key ID and a Secret Key that the AWS SKD can use to consume any service in AWS, as long as the role and policy we created before allow it.
AWS offers a really cool feature. They will generate a SDK for you to consume API Gateway resources. You can download it, setup you Access Key ID and Secret Key and use the SDK pre-configured for your specific endpoint.
You can get it in the “Stages” section, in the “SDK Generation” tab for the corresponding stage (dev in our case). We can choose JavaScript as the platform we want to generate the SDK for.
The downside to this is that the generated JavaScript code is not structured for a Webpack build. It relies on you importing the right JS files in the HTML document. I modified the common libraries for you to be able to import them in your Webpack project, you can find the code here. I hope to move them in a custom NPM package in the near future to allow you to just import them in your project.
There is a file in the SDK which is not generic, though. And that’s the content of apigClient.js. That code is generated specifically for your API Gateways. I renamed it to TacoApiClient.js and modified it to work with Webpack and ES5. You can use my file as a reference to know what changes are required.
With that code, you can import the client into your React application.
In views/Main/Home/Home.js, inside the web app generated by Auth0, we will add the following function:
import TacoApiClient from 'utils/TacoApiClient'//...testIAM(){
var awstoken = JSON.parse(localStorage.getItem('awstoken')); let client = new TacoApiClient({
accessKey: awstoken.AccessKeyId,
secretKey: awstoken.SecretAccessKey,
sessionToken: awstoken.SessionToken,
region: 'us-east-2' // The region you are working out of.
})
var params = {
//This is where any header, path, or querystring request params go. The key is the parameter named as defined in the API
};
var body = {
//This is where you define the body of the request
};
var additionalParams = {
//If there are any unmodeled query parameters or headers that need to be sent with the request you can add them here
headers: {
},
queryParams: {
}
}; client.helloGet(params, body, additionalParams)
.then(result => {
console.log(result)
//This is where you would put a success callback
this.setState({
hello:result.data.message
})
}).catch( result => {
console.error(result)
//This is where you would put an error callback
});
}
For the last step, let’s add a button to our Home component to call the testIAM function:
render(){
const { profile, hello } = this.state
return (
<div className={styles.root}>
<h2>Home</h2>
<p>Welcome {profile.name}!</p>
<Button onClick={this.testIAM.bind(this)}>Test IAM</Button>
<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>
)
}
Load your web app:
Click on “Test IAM” and you should see a successful request:
Authenticating API Gateway endpoints with IAM is a good approach if you wan to tie yourself to AWS. The delegation endpoint allows us to configure AWS SDK in our web app to handle more services other than the API Gateway.
The approach is cumbersome, though. I wouldn’t advise it unless you really need to get AWS Client ID Key and Secret Key values. Once Auth0 provides a better way to access delegation points, this approach will get more manageable.
In next posts we will learn how to use scopes to provide more granular access control.