Tuesday, November 27, 2018

Simple federated sign-on with Amazon Cognito Part 1 - The "basics"

I recently found myself needing to climb the learning curve for adding federated user sign-in to a webpage with Amazon Cognito. What follows is a quick summary of how you can do the same. For those who want to jump ahead, they can just take a look at the matching Bitbucket Snippet.




First things first, the assumption here is that the webpage is using plain, client side JavaScript. That makes our application a "user-agent-based application" which is a "public client" in OAuth parlance, as per RFC 6749 where OAuth2 authorization is specified. Public clients are assumed to be incapable of keeping their secrets, well, secret. That makes the use of the 'state' parameter and PKCE security largely useless from a security standpoint, however, we're going to use them because they are absolutely critical to properly implementing a "confidential client". Since this is intended primarily to be a learning exercise, it's worth the extra effort to see how they can be used.

Second things second, this is using the federated identities functionality baked into Cognito. This allows determining the identity of users based on an external authentication source. I am specifically interested in the public providers such as Login with Amazon, Facebook, and Google. This guide will focus on Login with Amazon.

The authentication flow we're trying to accomplish is as follows:
  1. User clicks "Login" link on our page
  2. User is directed to an Amazon Cognito hosted authentication page where they select the federated identity provider they wish to use
  3. User is directed to a login page for the federated identity provider they selected
  4. User authenticates with the federated identity provider
  5. User is returned to our page with an authentication code, which can in turn be exchanged for an access code which can be used to the get user information
First, setup a user pool. Go to the Cognito service in the AWS Management Console, and select "Manage User Pools". From the "Your User Pools" page, select "Create a user pool". The defaults are fine for our toy example. Now, select "Identity providers" under "Federation" from the right hand side. From here, we configure the providers we want users to be able to pick from when they login to our page.

For Login With Amazon, it will be necessary to configure Login With Amazon through Amazon Developer Services. Keep in mind that this is not Amazon Web Services, and is managed through a separate interface! Head over to Amazon Developer Services, go to the "Developer Console", click "Login with Amazon" at the top of the page, and click "Create a New Security Profile". You will be required to provide a URL of a "consent privacy policy", it may be available over HTTP or HTTPS and must be available over the internet. I served the example from the snippet using S3. After creating your security profile, take note of the "Client ID" and "Client Secret" shown when you click "Show Client ID and Client Secret" under "OAuth 2 Credentials".

Going back to the "Identity providers" in the interface for our Cognito user pool, you can now select "Login with Amazon". The requested "Amazon app ID" is the "Client ID" from the security profile in the last step, and the "App secret" is the "Client Secret". For "Authorize scope" you can simply enter "profile" for this example, or take a look at the allowed scopes in the documentation.

Now we need an app client so our page can interact with the Cognito service. Click on "App clients" under "General settings" on the left side of the Cognito user pool interface. Click "Add an app client", give our new app client a name, and uncheck "Generate client secret" since our page is a public client and can't keep secrets. Finally, click "Create app client". Take note of the resulting "App client id".

Next, configure the app client to work with our page. Go to "App client settings" under "App integration". Check "LoginWithAmazon" under "Enabled Identity Providers" to enable users to log in with Amazon. We also need to register the callback URLs. If your page is being served to the internet, the callback URLs must be available via HTTPS. If you are serving the page locally, you can use localhost. We're going to assume the page is being served locally for test purposes. Enter 'http://localhost:8000/?action=login' for the "Callback URL(s)" and 'http://localhost:8000/?action=logout' for the "Sign out URL(s)". Check "Authorization code grant" under "Allowed OAuth Flows" and check "openid" and "profile" under "Allowed OAuth Scopes". Finally, click "Save changes".

A bit of explanation about Allowed OAuth Flows and Allowed OAuth Scopes is required. By selecting the authorization code grant flow type, we're telling Cognito that, after the user successfully authenticates, we want an authorization code returned to us. Our client (cognito.js from the snippet) will exchange that authorization code for an access token, which will in turn be used to access the authenticated user's "profile". We could have used the "implicit grant" flow, which returns an access token directly, but that is only recommended for user-agent based applications, and exposes the access token directly in the URL bar. As such the authorization code grant flow is preferable, and more generalizable to confidential clients which are capable of keeping secrets. Note that since we are working with a public client we did not generate the client secret when we set up our app client, if we did, it would be used when making our requests. As for OAuth scopes, the openid scope is required for access to the profile scope, as per the Cognito documetation.

Next up, we need to set up a domain for our Cognito user pool. Click "Choose domain name" at the bottom of the "App client settings" page. Type in whatever domain you want, click "Check availability", when you find one that comes up as available click "Save changes".

We now have everything we need to set up the demo given in the snippet. At the top of cognito.js, replace '<APP_BASE_URL>' with the URL that index.html is being served at. For example, if you're serving using 'python -m SimpleHTTPServer', you would enter 'http://localhost:8000'. '<COGNITO_POOL_CLIENT_ID>' gets replaced with the app client id that was generated when we created our app client. Replace '<COGNITO_POOL_URI>' with the Amazon Cognito domain that was created when you chose the domain name.

That should be it. When you load our demo page at the base url (localhost:8000 if you've been following along) you should see a "Login" link. Clicking that will take you to the Amazon sign in page (since we only have the Login with Amazon identity provider enabled). Signing in should redirect you to the base url with the authorization code in the 'code' query parameter and 'login' as the 'action' query parameter, after that is parsed, you should be redirected to the base url with no query parameters, and the "Logout" link should be shown. Additionally, the Cognito user profile should be displayed in the web console. Clicking "Logout" should redirect you to the Cognito pool logout URL, which will in turn redirect you to the base url with 'logout' as the 'action' query parameter, which will clean up the session and finally redirect you to the base url with no query parameters.

Tune in next time when we walk through the cognito.js source in detail!

No comments:

Post a Comment