AWS Startups Blog
Dynamic Websites Using the AWS SDK for JavaScript in the Browser, Part 2: Auth0 and Token-based…
By Matt Yanchyshyn, Solutions Architect, AWS
Introduction
The previous post introduced the AWS SDK for JavaScript in the Browser and showed you how to set it up with Login with Amazon for web identity federation. We used this technique to gain read and write access to an Amazon DynamoDB table without including AWS credentials in our JavaScript code. We then added NVD3 to visualize the query results. Finally we demonstrated how to lock down your data by adding fine-grained access control to your Amazon DynamoDB tables with IAM policies that reference unique federated user IDs.
In today’s post we’re going to take it up a notch and learn how to build more complex sites with persistent sessions and many more authentication options. We still won’t use any back-end Amazon Elastic Cloud Computer (EC2) instances—it’s all client-side HTML and JavaScript, hosted on Amazon Simple Storage Services (S3) and powered by direct calls to Amazon DynamoDB. Instead we will leverage a technique called “token-based authentication” where signed JSON web tokens (JWT) are stored in HTML5 web storage (localStorage) and then sent with every request.
We’ll also set up SAML identify federation using Auth0, an AWS technology partner. Auth0 lets you use multiple identity providers for authentication, including your own custom data store, without adding a crazy amount of code to your application. Taken together, token-based authentication and flexible identity federation with Auth0 will allow you to build complex, dynamic websites with frictionless login and persistent sessions that potentially cost just a few pennies per month to host on Amazon S3!
Much of this post builds on the work that AWS technology partner Auth0 has done in the area of token-based authentication and identity federation for accessing AWS APIs. Auth0 published a series of blog posts on their website that explain how token-based authentication works, and how it compares with traditional cookie-based authentication. There’s also a good post on their blog about using these JSON tokens along with AWS IAM to access AWS APIs. They’ve put this all together in a demo that shows how to build a back end–less file sync tool using Amazon S3.
In this post, we’ll be walking through very similar code that builds on the Amazon DynamoDB example that we built in the first part of this series.
Identity Federation with Auth0
Many modern web and mobile applications have turned to third parties for authentication and identity management to reduce sign-in friction and offload the burden of maintaining their own identity stores. End users have established trust with companies such as Amazon, Google, Twitter, Microsoft and others. They want to use these companies’ systems to login to their applications for security reasons, or simply because they can’t be bothered to remember another password. In addition, many corporate users don’t want to have to manage multiple logins for their ever-increasing ecosystem of applications at work; they want to use a single set of corporate credentials for everything.
As we saw in the first part of this post, implementing third-party authentication with a single service like Login with Amazon is easy to implement using the AWS SDKs. But once you start adding additional authentication options like Google, Twitter, Facebook, corporate SAML providers, or custom identity stores held in your own databases, it gets a lot more complicated. Your application code will quickly get huge and unmanageable as you add the authentication code for each service.
Auth0 is an AWS technology partner that offers SaaS for user management and identity brokering that helps solve this problem. With their service, you can manage multiple authentication mechanisms by providing a common platform to tie them all together. When you use the Auth0 SDK, just a few lines of code can give your users as many authentication mechanisms as you want. Auth0 handles multiple providers on their back end, which you configure for each service that you wish to use. Auth0 also centralizes the metrics for all of the identity providers so you don’t have to go looking in multiple places to know who is using your application.
Importantly, Auth0 can act as a SAML provider for AWS IAM so you can simplify identity federation using multiple providers. It also allows you to give your users secure access to AWS APIs using STS.
Auth0 Setup
To get started, open an account on https://auth0.com or via the AWS Marketplace. You can use the free account for this example.
Once your Auth0 account is created, follow the instructions to create a new application in the Apps/APIs section. Copy the Client Id and domain under your Auth0 application settings:
Next, add a few Connections—services that your users will be able to use to authenticate to your application. In this example we’re going to add Amazon, Google, Facebook, Twitter, and GitHub. You should now see something like this on your Auth0 Dashboard:
Auth0 gives you the ability to specify exactly how much information end users must share with your application from their preferred identity provider. For example, with Login with Amazon you can ask users if it’s okay to pass their postal code to your application. Other providers, such as Facebook and Google, have a very large number of specific attributes and permissions that you can configure in the Auth0 interface. (It’s important to strike a balance: try not to ask so much that you might alienate potential users who don’t want to share personal information, but at the same time consider your business needs and request permissions accordingly.)
In addition to the social providers, you can add enterprise providers such as AD/LDAP, ADFS, PingFederate or any provider that supports SAMLp. You can also connect Auth0 to external databases, which is useful if you have an identify store tucked away on an old RDBMS or something that you’re already using for another application.
Finally, under Apps / APIs, click the Add-ons icon beside the application that you created and set Amazon Web Services (AWS) API to ON. This will allow Auth0 to interact with AWS STS so we can call AWS APIs via the JavaScript SDK using temporary security credentials.
AWS Setup
Auth0 has developed a quick start guide for many common technologies such as Node.js, Angular.js, Java, PHP, ASP.NET, and many others. For this example we’ll be using jQuery + AWS API.
First, set up Auth0 as a SAML identity provider in AWS IAM, as explained in the Auth0 documentation. (Make sure that you are logged into Auth0 when you open this link in your browser because some the instructions pull data from your Auth0 account.) In the second part of the instructions, “Creating Roles,” we create an AWS IAM role that grants access to AWS resources for users who authenticate via your Auth0 domain. In other words, users who use Amazon, Google, Facebook or whatever other identity providers that you configured in Auth0 to access your website will be granted temporary AWS STS credentials giving them access to AWS resources in your account as specified in the IAM role policy document.
When you get to the “Creating Roles” section in the Auth0 documentation, add the following policy document instead of the one shown in the example. This IAM role grants access to the Amazon DynamoDB table that we created in the first part of this blog series. In addition, we modify the DynamoDB fine-grained permissions that we set up in the last post to use the unique SAML user ID instead of the Login with Amazon ID. This will lock down the DynamoDB tables in the same way as we did before, but this time the tables are linked to the user as authenticated by an identity provider via Auth0.
Be sure to replace YOUR-AWS-ACCOUNT-ID with your own AWS account ID and also change the DynamoDB table name if you did not use the one specified in the instructions from the last blog post:
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "dynamodb:Query", "dynamodb:UpdateItem", "dynamodb:Scan" ], "Resource": [ "arn:aws:dynamodb:us-east-1:YOUR-AWS-ACCOUNT-ID:table/browser-metrics-per-user" ], "Condition": { "ForAllValues:StringEquals": { "dynamodb:LeadingKeys": ["${saml:sub}"], "dynamodb:Attributes": [ "userId", "browser", "count" ] }, "StringEqualsIfExists": { "dynamodb:Select": "SPECIFIC_ATTRIBUTES" } } }, { "Effect": "Allow", "Action": ["dynamodb:Scan"], "Resource": [ "arn:aws:dynamodb:us-east-1:368101875365:table/browser-metrics-per-user" ], "Condition": { "ForAllValues:StringEquals": { "dynamodb:Attributes": ["browser"] }, "StringEqualsIfExists": { "dynamodb:Select": "SPECIFIC_ATTRIBUTES" } } } ] }
As instructed in the Auth0 documentation, note the role ARN of the IAM role that you created and also the provider ARN of the SAML identity provider that you created for Auth0 in the AWS Management Console.
Now that we’ve set up both AWS and Auth0 we can move on to the code.
Token-based Authentication
Tokens have several advantages over cookies when you’re working with Amazon S3-hosted, client-side applications written in only HTML and JavaScript:
First, CORS and cookies don’t mix. When your application is using multiple domains (for example, one that hosts a service to handle authentication and another to host your application code), you can’t access cookies in document.cookie or the HTTP headers using JavaScript since they honor same-origin policies. Tokens, on the other hand, are sent using standard HTTP headers so they’ll work across domains.
Second, when you use cookie-based authentication you have to store session information in an external data source, usually a database. With tokens you can store all needed information locally in the browser’s HTML5 Web Storage.
These days, tokens are supported in the vast majority of modern web browsers, including on mobile devices. Different browsers have different storage limits, anywhere from 2.5 MiB on the low end to an average of 5 MiB on most browsers. This may not seem like much, but compare it to the average cookie storage limit of 4 KB!
HTML/JavaScript
You can download the full source code for the examples below by viewing our sample login page and viewing the page source.
In your application code folder, create a new file called config.js and fill it in:
window.config = { role: 'AWS IAM role ARN that you copied from the IAM console', principal: 'Provider ARN of the SAML IdP that you created for Auth0', domain: 'your.auth0.com Domain', clientID: 'Auth0 application clientID', targetClientId: 'Auth0 application clientID', region: 'us-east-1' };
Next, create a new version of the index.html file that we built in the first part of this series. Note that we include the config.js file that we just created above, plus jQuery. This time we’re also adding some new libraries: Auth0 Widget SDK and store.js, a wrapper to make it easy to work with HTML5 Web Storage (localStorage).
<!DOCTYPE html> <html lang="en"> <head> <title>AWS Javascript Browser SDK: Auth0</title> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <script src="//code.jquery.com/jquery-1.11.0.min.js"></script> <script src="//cdn.auth0.com/w2/auth0-widget-5.0.min.js"></script> <script src="js/config.js"></script> <script src="js/store.min.js"></script> </head> <body> <a href="#" class="btn-login">Login</a> <script> // check for HTML5 Web Storage (localStorage) support using store.js library init() function init() { if (!store.enabled) { alert('Local storage is not supported by your browser. Please disable "Private Mode", or upgrade to a modern browser.') return } } // reset state store.clear(); </script> </body> </html>
The first few lines of code after the opening <body> tag use the store.js library to check whether the web browser viewing this page supports HTML5 Web Storage (localStorage). We also clear any existing localStorage, just in case.
The second part displays a simple login link. It’ll look like this in a browser:
Right now, clicking this link doesn’t do anything, so let’s add some code to trigger an Auth0 login interface. The first ready function configures the Auth0 login widget with the values you specified in config.js. The click function below listens for click events on the login link. After a successful login a JSON Web Token (JWT) is passed back to the browser and stored in localStorage. Once stored, we forward the user to the next page, page2.html.
Add the following lines of code above the closing </script> tag. (If you’re not sure where to add it, refer to the page source for our sample login page.
$(document).ready(function() { var widget = new Auth0Widget({ domain: window.config.domain, clientID: window.config.clientID, callbackURL: location.href, callbackOnLocationHash: true });
$('.btn-login').click(function(e) { e.preventDefault(); widget.signin({ popup: true }, null, function(err, profile, token) { if (err) { console.log(err); } else { store.set('userToken', token); store.set('profile', JSON.stringify(profile)); location.href = 'page2.html'; } }); }); });
Before this will work we need to go back to the Auth0 Dashboard and adjust the App Callbacks URLs section:
The screenshot above shows https://s3.amazonaws.com/startup-blog-examples/example-auth0/index.html. Yours should be the full URL of the index.html file that you upload to Amazon S3.
Next, create a new file called page2.html that will live in the same Amazon S3 bucket as index.html. As before, you can view the page source to see the source code for this sample page.
In addition to jQuery, we also include the AWS SDK, D3, and NVD3 for graphing our DynamoDB data, along with the simple browser identification script that we used last time. Note that in page2.html we’re using a slightly different Auth0 JavaScript library than the widget one that we used in index.html:
<!DOCTYPE html> <html lang="en"> <head> <title>AWS Javascript Browser SDK: Auth0</title> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <script src="//code.jquery.com/jquery-1.11.0.min.js"></script> <script src="//sdk.amazonaws.com/js/aws-sdk-2.0.8.min.js"></script> <script src="//d2cdc0w3k7b2of.cloudfront.net/identify_browser.js"></script> <script src="//d2cdc0w3k7b2of.cloudfront.net/d3.min.js" charset="utf-8"></script> <script src="//d2cdc0w3k7b2of.cloudfront.net/nv.d3.min.js"></script> <link href="//d2cdc0w3k7b2of.cloudfront.net/nv.d3.min.css" rel="stylesheet" type="text/css"> <script src="js/store.min.js"></script> <script src="//cdn.auth0.com/w2/auth0-3.1.min.js"></script> <script src="js/config.js"></script> </head> <body> Hello, world. </body> </html>
Upload index.html, page2.html and js/config.js to your Amazon S3 bucket. Remember to create an Amazon S3 bucket policy or set the ACL for the index.html and page2.html files to public-read so that everyone can access the website. You should also use HTTPS to access the pages or the login phase might not work.
When you load the Amazon S3–hosted index.html page in a browser and click the Login link you should see an Auth0 login popup like this:
Try logging in. After a successful authentication you should redirected to page2.html and see “Hello, world” written on the screen.
Getting Access to AWS via Auth0
Now that Auth0 authentication is working and we’ve learned how to write the JWT (token) into localStorage, we can use that token to assume an AWS IAM role and receive temporary AWS credentials from AWS STS. This works by taking the token stored in localStorage and passing it to the Auth0 authentication API, which then calls out to AWS IAM and identifies itself as a valid SAML provider for the account. AWS STS then returns a set of temporary credentials that can be used to access AWS resources, restricted by the IAM role that you associated with the IAM SAML identity provider for Auth0.
We’re going to add four initial functions to page2.html. Add the following code directly below the opening <body> tag. If you’re not sure where the add it, view the source of our page 2 sample.
The first function, get_aws_token, retrieves temporary credentials from AWS STS and stores them in a localStorage variable named aws_creds.
The second function, user, makes it easy to retrieve data returned by Auth0 following a successful authentication: AWS temporary credentials and profile information (such as name, email, profile photo, and other information) that the identity provider (such as Amazon, Google or Facebook) returns to the application. What’s in the profile object varies per service and also depends on how you set up each service’s authentication application.
The initialize_security_context function is arguably the most important since it makes sure that the user has been properly authenticated and, if they were, continues to execute code to populate the rest of the page. You should call all code that requires valid AWS credentials or identity provider profile information from within the initialize_security_context function block.
Finally, we have a simple logout function that clears localStorage and returns users to the login page after they click a Logout link.
<script type="text/javascript"> function get_aws_token(options, callback) { var auth0 = new Auth0({ domain: options.domain, clientID: options.clientID, callbackURL: 'dummy' }); auth0.getDelegationToken(options.targetClientId, user.get().token, { role: options.role, principal: options.principal }, callback); }
window.user = { get: function() { if (!store.get('profile')) return; return { profile: JSON.parse(store.get('profile')), token: store.get('userToken'), aws_creds: store.get('aws_creds') ? JSON.parse(store.get('aws_creds')) : undefined }; } };
function initialize_security_context(callback) { if (!user.get()) return location.href = 'index.html'; // if token is not expired, return if (user.get().aws_creds && new Date(user.get().aws_creds.Expiration) > new Date()) return callback();
get_aws_token(window.config, function(err, delegationResult) { if (err) return location.href = 'index.html'; store.set('aws_creds', JSON.stringify(delegationResult.Credentials)); callback(); }); }
initialize_security_context(function() { $('.btn-logout').on('click', function() { store.clear(); location.href = 'index.html'; }); alert("It's working!"); }); </script>
<div id="logout" style="clear:both"> <a href="#" class="btn-logout">Logout</a> </div>
</body> </html>
Upload the new page2.html file to Amazon S3 and get reauthenticated from the index.html page. If all is working correctly, you should see a popup alert that says “It’s working!”
Putting It All Together
It may not look like much so far, but at this stage we’ve already built a fully functional dynamic website that can maintain its state across a refresh or page navigations using localStorage and make secure calls to AWS APIs with temporary credentials granted through identity federation. All that’s left to do is to merge this code with the example that we built last time using DynamoDB.
We’ve already associated a new IAM role with the IAM SAML identity provider entry for Auth0, so the temporary AWS credentials stored in the aws_creds localStorage object should be valid to make calls to the DynamoDB table from the app we built last time. That means that all that’s left to do is to drop in the code we wrote in the first part of this blog post series into the initialize_security_context function block. To see an example, refer to the source of our page 2 sample.
To recap, we are detecting what kind of web browser you’re using, putting that information into a DynamoDB table using the AWS STS temporary credentials, doing a scan of the full DynamoDB table to read all entries, and then graphing the results using the NVD3 library.
That’s it! When you login at our sample page, or use your own code that you wrote using the instructions above, you should see a pie chart and list that update periodically. Try logging in with multiple browsers at the same time: your NVD3 pie chart should change dynamically as it reads new information from DynamoDB.
Conclusion
After we published the first blog post on this topic, someone on Twitter called it “0-tier architecture.” That’s maybe not 100% accurate, since we’re still relying on Amazon S3 for hosting the HTML and JavaScript files plus Amazon DynamoDB for database tasks, but it does capture the essence of what we’ve built: Auth0’s powerful but easy to use identity tools combined with a token-based approach allows us to build a dynamic website with persistent sessions where almost everything happens on the client side, removing the need for Amazon EC2–based application servers.
Designing applications this way is not only cost effective but also operationally efficient. In the true AWS spirit, you can reduce undifferentiated heavy lifting by leveraging Auth0 for identity management and authentication tasks, Amazon S3 for static file hosting and Amazon DynamoDB for NoSQL data storage. There are no more servers to manage so you can focus on what matters most: your code.