AWS Partner Network (APN) Blog

How to Integrate REST APIs with Single-Page Apps and Secure Them Using Auth0 – Part 2

Editor’s note: This is the second of a popular two-part series by Aravind Kodandaramaiah. Read Part 1 >>

By Aravind Kodandaramaiah, Solutions Architect at AWS who works closely with Global Systems Integrators (GSIs)

AWS SecurityIn my previous post on securing REST APIs with Auth0, you learned the basics of token-based authentication using JWT tokens, configured the Auth0 portal by registering a client, a resource (API) server and created a Auth0 rule to add scopes to the JWT token.

In this post, you will create 2 RESTful APIs using Amazon API Gateway and secure them using a custom authorizer configured with API Gateway and integrate these APIs with a SPA. Please note that AWS service charges may apply while using AWS services to walk through this post.

The two REST APIs you will create are the Movies and the Devices APIs. Access to the Movies API is only allowed to users authenticated using Amazon credentials, while access to the Devices API is only allowed to users authenticated using Google credentials. You will use a CloudFormation template to automate the creation of REST APIs, Lambda functions and S3 buckets required to run this solution. I will walk you through the AWS resources created by the CloudFormation template and also explain how JWT Verification can be implemented in a custom authorizer lambda function.

Solution Implementation and Deployment

  1. Download the solution source code from our GitHub repository.
  1. Clone the library into a local folder:

git clone https://github.com/awslabs/apn-blog.git

The contents of the apn-blog/APIAuthorization folder should look like this:

Image_1_Auth0_2

  1. Log in to your AWS account and create an Amazon S3 bucket. This bucket will be used to store the packaged zip file for the Custom Authorizer Lambda function. For instructions, see the Amazon S3 documentation.
  1. Navigate to the ‘CustomAuthLambda’ folder from step 2 and edit the ‘js’ file. Modify the values assigned to the following variables/parameters (search for //TODO in the source file):
    • signingSecret – Replace the value assigned to a value configured in Auth0 API settings portal.
    • audience – Replace the audience value to the ‘Identifier’ attribute value configured in Auth0 API settings portal.
  1. Zip the contents of the ‘CustomAuthLambda’ folder and upload it to the S3 bucket you created in step 3.
  1. Download this CloudFormation template and run it in your AWS account in the desired region. For detailed steps about how to create a CloudFormation stack based on a template, see this walkthrough.
  1. You must provide four parameters:
    • CustomAuthLambdaS3Bucket – Set this to the name of the S3 bucket you created in step 3.
    • CustomAuthLambdaS3KeyName – Set this to the name of the AWS Lambda packaged zip file you created in step 5.
    • StageName – Specify the name of the stage you want the REST API to be deployed in the API Gateway (e.g., beta, prod).
    • WebsiteS3BucketName – Specify the name of the S3 bucket that will host the SPA as a static website. This bucket will be created by CloudFormation. (See the rules for naming S3 buckets in the Amazon S3 documentation.)

The following schematic shows the association of REST APIs to the Lambda functions after the cloudformation stack is created.

Image_2_Auth0_2

Each REST APIs GET method is backed by a Lambda function. A distinct custom authorizer backed by a shared Lambda function is created for each of the GET method. If need be, different lambda functions can be built and associated with each custom authorizer.

You will find a list of all AWS resources created by this CloudFormation template at our GitHub location.

  1. Copy the value of the output key ‘S3BucketSecureURL’ from the Outputs tab of the AWS CloudFormation stack, and update this URL in the Auth0 portal under client settings for the Allowed Callback URLs
  1. In order to integrate the SPA with these REST APIs, you’ll need to make some modifications to the html file in the SPA source code (it’s in the folder ‘SinglePageApp’ from step 2). Open index.html in a text editor or IDE and make the following changes.
      • Replace the values of query parameter values sent to Auth0’s Authorize API:
        • Set the Authorize API URL domain name to the domain name of your client registered in the Auth0 portal under client settings.
        • Set the audience value to the ‘Identifier’ attribute value configured under API settings in the Auth0 portal.
        • Set client_id to the client ID of your application registered in the Auth0 portal.
        • Set the redirect_uri value to the CloudFormation stack’s output value for the key named ‘S3BucketSecureURL’.

You will make these changes in the following code fragment:

//TODO: Replace the URL and values for Query Parameters - audience, client_id, redirect_uri  
href="https://auth0jwtdemo.auth0.com/authorize?audience=https://StreamingResourceServer&nonce=" + nonce + 
"&response_type=id_token%20token&client_id=<enterid>&redirect_uri=http://auth0.myspa.s3-website-eu-west-
1.amazonaws.com/";
      • Replace the values of parameter values being sent to UserProfile API:
        • Set the domain to the domain name of your client registered in the Auth0 portal under client settings.
        • Set the clientID to the ‘client ID’ of your client registered in the Auth0 portal under client settings.
        • Set the callbackURL value to the CloudFormation stack’s output value for the key named ‘S3BucketSecureURL’

You will make these changes in the following code fragment:

!-- //TODO: ClientId, Callback URL, DomainName -->  
       var auth0 = new Auth0({
           domain:       'auth0jwtdemo.auth0.com',
           clientID:     '<enterid>',
           callbackURL:  'http://auth0.myspa.s3-website-eu-west-1.amazonaws.com',  
           responseType: 'token'
          });
      • Finally, replace the REST API endpoint URLs depending on the Stage endpoint URLs in API Gateway:
$('#btn-device').click(function(){
 //TODO: Replace this URL
 invokeAPI('https://kytza0mdgb.execute-api.us-west-2.amazonaws.com/beta/device', document.getElementById('deviceInfo')); 
});

$('#btn-movie').click(function(){
 //TODO: Replace this URL
 invokeAPI('https://ntq7hay36e.execute-api.us-west-2.amazonaws.com/beta/movie', document.getElementById('movieInfo'));
});
  1. Save the html file.
  2. Copy the contents of the ‘SinglePageApp’ folder (from step 2) to the S3 bucket created by CloudFormation, represented by the cloudformation template parameter WebsiteS3BucketName.
  3. Copy the S3BucketSecureURL output value from the CloudFormation stack and browse to this URL. You will be presented with a login link.
  4. Choose the login link. Auth0 will redirect you to a custom login screen that lists the two identity providers, Amazon and Google.
  5. Log in using Amazon credentials and choose Invoke Movie API. This should display the message “Movie Info retrieved from Movie API.” Choose Invoke Device API to display an “UnAuthorized” message.
  6. Log in using Google credentials and choose Invoke Device API. This should display the message “Device Info retrieved from Device API.” Choose Invoke Movie API to display an “UnAuthorized” message.

JWT Verification Using Custom Authorizer Lambda Function

Custom authorizers can be configured by using the API Gateway console or APIs. For this post, you configured the Auth0CustomAuthorizer Lambda function to be the custom authorizer for both the Movies and the Devices API. API Gateway provides flexibility to configure multiple custom authorizers for each API and each method in the API can use a different authorizer. To configure a custom authorizer, you must specify a unique name and select a Lambda function to act as the authorizer. You also need to indicate which field of the incoming request contains the JWT token. API Gateway will pass the value of the field to your Lambda authorizer. This field can be selected using the method.request.header.Authorization mapping expression.

I’ll walk you through the implementation of a custom authorizer Lambda function named Auth0CustomAuthorizer. The Lambda function needs to performs 3 tasks:

      1. Token extraction – Extract the JSON Web Token (JWT) access_token from the Authorization HTTP request header (using the event parameter of the lambda handler function).
      2. JWT Verification – Verify if the access_token is valid (it hasn’t been modified in transit, hasn’t expired, etc).
      3. Building IAM Policy – If the JWT access_token is found to be valid, the function then evaluates if the token contains the required scope based on the resource being accessed. Based on this evaluation, the Lambda function generates an AWS Identity and Access Management (IAM) Allow or Deny policy and returns the policy back to the API Gateway.

As per the API configuration settings in Auth0, the JWT tokens generated by Auth0 will be signed by using the HS256 algorithm with a secret key. The Secret key should be securely stored needs to be known only to the Custom Authorizer and Auth0. The custom authorizer can then verify (see ‘jwt.verify’ function call in the code below) that the signature obtained from its own hashing operation matches the signature on the JWT itself (i.e. it matches the JWT signature created by the authentication server) and that the JWT token hasn’t expired. If the signatures match, it means the JWT is valid. If the signatures don’t match, it means that the received JWT is invalid, which may be an indicator of a potential attack on the REST API. The Lambda function then generates a IAM policy which either allows or denies access to the REST API depending on the validity of the JWT token.

// The access token presented by the client application.
  var access_token = event.authorizationToken;  
  //TODO: Replace Client Secret (Refer to Auth0's settings)
  var signingSecret = 'ConfigureYourSigningSecret';
  
//TODO: Replace the audience parameter value
  jwt.verify(access_token, signingSecret, { audience: 'https://StreamingResourceServer', algorithms: '["HS256"]'}, 
function(err, decoded) {

    if (err) {
      console.log('JWT Verification Error');
      context.succeed(generate_policy(apiOptions, '', 'Deny', event.methodArn));      
    } 
    else {
      if ((decoded.scope == "read:Devices" && apiOptions.resource_path.trim() == 'device') ||
          (decoded.scope == "read:Movies" && apiOptions.resource_path.trim() == 'movie')) {
            context.succeed(generate_policy(apiOptions, decoded.sub, 'Allow', event.methodArn));
            console.log("Allow IAM Policy Generated");
      }
      else {  
        context.succeed(generate_policy(apiOptions, decoded.sub, 'Deny', event.methodArn));
        console.log("Deny IAM Policy Generated");
      }
    }
  });
};

Use Postman to Test APIs

The Postman Chrome extension is a convenient tool to test an API in API Gateway. See the Amazon API Gateway documentation for details on using the Postman Chrome App to test API calls. Copy the API stage URL from the API Gateway console and add the Authorization HTTP header to the GET request. When an invalid JWT token is passed, the API Gateway  returns a 403/Forbidden response due to the custom authorizer.

Image_3_Auth0_2

When a valid JWT token is passed, the API Gateway returns a 200/OK response.

Image_4_Auth0_2

In this two-part post, I’ve demonstrated how you can use an Amazon API Gateway custom authorizer to secure REST APIs by integrating with APN Technology Partner and identity platform provider, Auth0. I first configured a client, registered a resource server (API), created rules within the Auth0 portal and then created REST APIs by using API Gateway, created a custom authorizer using AWS Lambda, registered the custom authorizer with API Gateway, and implemented JWT verification.

Next Steps

There are many ways you can customize or extend this solution; for example, you can:

      1. Accelerate delivery of this SPA via Amazon CloudFront.
      2. Set up your own custom DNS domain name. Use services like AWS Route 53 and AWS Certificate Manager to set up your own Amazon CloudFront custom domain name and SSL certificate.
      3. Handle JWT token expiration and generate refresh tokens.
      4. Improve error handling in API Gateway and AWS Lambda – Refer to this blog for more details.
      5. Improve error handling in SPA.

The code for this post is available on GitHub at https://github.com/awslabs/apn-blog/tree/master/APIAuthorization.

If you have comments about this post, please submit them in the comments section. If you have questions about this solution or its implementation, please start a new thread on the API Gateway forum.