OpenID Connect Authorization Code Flow with AWS Cognito

2011_0203 — Natural Stone_6 / Ben Hosking

Earlier this year, I was working on a project that was using AWS Cognito (as the identity stack) and the AWS API Gateway (as the front-door to all of the API calls). AWS Cognito is a relatively new player in the identity space. It doesn’t support the full OAuth2 or OpenID Connect specs, but, does support most of what I would generally consider the important parts. This post will be the first in a series about AWS services in the Identity and API Management spaces and how they can be used together in common patterns that we’ve explored with other technology stacks. In this post, we’ll do an AWS Cognito configuration overview for an OpenID Connect application.

Overview

AWS Cognito has two major components: Identity Pools and User Pools. Identity Pools are the original functionality deployed in 2014; this largely uses proprietary AWS interfaces and libraries to accomplish the task of authenticating users. Furthermore, Identity Pools have no concept of claims (standard or custom) that are stored in the system; it is entirely a federated identity construct. User Pools are the more recent addition to the Cognito feature set; User Pools are a multi-tenant LDAP-like user repository combined with an OAuth2 and OpenID Connect interface. User Pools also provide federation services using SAML, OpenID Connect, Facebook, Google+, and Amazon (at the time of this writing). User Pools don’t have as many federation options as Identity Pools, but the number of options are increasing.

AWS Cognito uses JSON Web Tokens (JWTs) for the OAuth2 Access Tokens, OIDC ID Tokens, and OIDC Refresh Tokens. The refresh token is actually an encrypted JWT — this is the first time I’ve actually seen JWE used with an Identity Provider (where it wasn’t an optional feature).

This example will use User Pools because of the support for standards-based identity functionality and we want to be able to store user claims and potentially update them later.

Assumptions

As covered in many other blog posts on the topic of OAuth2 and OIDC, these specs cover a wide variety of scenarios. In the interest of keeping this series of articles at a managable length, we have to look at a simple example.

Let’s make some assumptions about requirements regarding how this example will work.

  • A Public Client will be used for testing purposes. Later, we will use this to test Single Page Applications (SPAs) or mobile apps.
  • We will adhere to the OIDC spec as closely as possible for this example.
  • The Identity Provider will be AWS Cognito.
  • The user definitions stored in Cognito will have a set of standard attributes (claims) that all users must have including email, first name, and last name.
  • User self-registration will be used.
  • There will be no user device tracking.
  • Email addresses will be used as user names.
  • Email addresses will be validated via a One Time Password (OTP) that is sent to the provided email address. Registration cannot proceed without it.
  • Out-of-the-box Cognito user sign up, sign on, log off, password change, and other standard fields will be used in this example. Less work for us:).

Cognito Setup

This section describes the AWS Cognito User Pool configuration needed for this example.

  • Log into AWS Console.
  • Navigate to the AWS Cognito service.
  • Click on the Manage User Pools button (shown above). Or, if Cognito has already been configured to some extent, click the User Pools link at the top.
  • Click the “Create a user pool” button on the right hand side.
  • Enter a pool name. We’ll use BlogDemo for this example.
  • Click “Step Through Settings”.
  • Select “email address or phone number”.
  • Under that, select “Allow email addresses”.
  • Select the following standard attributes as required:
Email
Family name
Given name
  • Scroll to the bottom.
  • Click Next Step.
  • Accept the defaults on this screen.
  • Click the Next Step button.
  • Accept the defaults on this screen.
  • Click the Next Step button.
  • Accept the defaults on this screen.
  • Click the Next Step button.
  • Click the Next step link.
  • Click No.
  • Click Next Step.
  • We will create an application definition a bit later. Keeping things simple right now.
  • Click Next Step.
  • We don’t have any need for Triggers or customized Sign Up/Sign In behavior for this example.
  • Scroll down.
  • Click Save Changes.
  • Click Create pool.
  • Wait a little while.
  • If successful, you will see the following:
  • Make note of the Pool Id. You will need this when configuring the application below.

Create Application Definition

This section adds an OAuth2 application definition to the User Pool we just created.

  • Go to the App clients screen in the AWS Cognito management screen for the User Pool we just created.
  • Click “Add an app client”.
  • Enter an App client name. This demo is using “BlogDemo SPA” (or whatever name works for your use case).
  • Enter a Refresh token expiration (in days). We will use the default of 30 days. Use whatever value works for your situation.
  • Do not select “Generate client secret”. This example will use a public client (ie, no client secret).
  • Do not select any other check boxes.
  • Click the “Set attribute read and write permissions” button.
  • Let’s make this simple and only give the user read and write access to the required attributes. So, uncheck everything except the email, given name, and family name fields.
  • Click “Create app client”
  • Click “Show Details”.
  • Take note of the app client id. We will need that in the next section.
  • Go to the App integration->App client settings screen.
  • Click the “Cognito User Pool” check box under Enabled Identity Providers.
  • Add http://localhost:3000/callback to the Callback URLs field.
  • Click the “Authorization code grant” checkbox under Allowed OAuth Flows.
  • Click the checkboxes next to email, openid, aws.cognito.signin.user.admin, and profile.
  • Click the “Save changes” button.
  • Click on the domain name tab.
  • Add a sub-domain called blogdemo or a name that is available.
  • Click the Check availability button.
  • As long as it reports “This domain is available”, the name you have chosen will work.
  • Click the “Save changes” button.
  • Click on the App integration->Resource server tab.
  • Click the “Add a resource server” link.
  • Enter “blogdemo.rcbj.net” in the Name field.
  • Enter “https://blogdemo.rcbj.net” in the Identifier field.
  • Enter “Read” in the Scope Name field and Description field.
  • When a second Scope field appears, enter “Write” in the Name field and Description field.
  • Click the “Save config” button.
  • Go back to the App Integration->App client settings tab.
  • Click the checkboxes next to the two custom scopes we just created.
  • Click the “Save config” button.

At this point, you can use the application definition with the OAuth2 + OIDC Debugger to test the configuration.

Important URLs for User Pool

In order to test the system, there are several endpoints that we need to know about for this Cognito User Pool. For the example I put together, the endpoints are the following:

Authorization Endpoint: https://blogdemo.auth.us-west-2.amazoncognito.com/oauth2/authorize

Token Endpoint: https://blogdemo.auth.us-west-2.amazoncognito.com/oauth2/token

UserInfo Endpoint: https://blogdemo.auth.us-west-2.amazoncognito.com/oauth2/userInfo

Login Endpoint: https://blogdemo.auth.us-west-2.amazoncognito.com/login

Logout Endpoint: https://blogdemo.auth.us-west-2.amazoncognito.com/logout?logout_uri=https://blogdemo.rcbj.net

You will notice that the hostname follows a general pattern of:

https://USER-POOL-NAME.REGION.amazonaws.com

More information about User Pool endpoints are available here.

There are also OIDC Discovery Endpoints:

OIDC Discovery Endpoint: https://cognito-idp.us-west-2.amazonaws.com/us-west-2_WaPN6Bb7H/.well-known/openid-configuration

JWKS Endpoint: https://cognito-idp.us-west-2.amazonaws.com/us-west-2_WaPN6Bb7H/.well-known/jwks.json

These endpoints are available from https://cognito-idp.REGION.amazonaws.com/USER-POOL-ID.

More information about these endpoints is available here.

Test With The OIDC + OAuth2 Debugger

We can test the new application definition with the OAuth2 + OIDC Debugger. This will confirm that the configuration works.

  • Follow the setup instructions in the readme.
  • Open a browser.
  • Go to “http://localhost:3000”.
  • Choose OIDC Authorization Code Flow from the Authorization Grant drop down.
  • Enter “https://blogdemo.auth.us-west-2.awscognito.com/oauth2/authorize” as the Authorization Endpoint (or the correct endpoint for your configuration).
  • Enter “https://blogdemo.auth.us-west-2.awscognito.com/oauth2/token” as the Token Endpoint (or the correct endpoint for the your configuration).
  • Choose Yes for “Use Refresh Token”.
  • Use the defaults for other fields.
  • Put the client identifier you saved from earlier into the Client ID field in the “Request Authorization Code” section.
  • Put “https://localhost:3000/callback” as the Redirect URL.
  • Put “openid blogdemo.rcbj.net/read blogdemo.rcbj.net/write” in the Scope field (or use custom scopes that are relevant to your configuration).
  • Click the Authorize button.
  • If you have not created a user account yet in your User Pool, you will need to click the Sign Up link (we’ll assume that is the case).
  • Enter your email address in the Email field.
  • Enter your first name (or some name) in the Given name field.
  • Enter your last name (or some name) in the Family name field.
  • Enter a password that satisfies all of the password constraints defined for the user pool. The UI will tell you when this is the case.
  • Click Sign up.
  • Go to your email client and retrieve the verification code that Cognito emailed to you.
  • Click Confirm Account.
  • After several HTTP redirects, you will be redirected to the debugger redirect URI.
  • From the Configuration box, click the Yes radio button next to “Use refresh token”.
  • Enter the client identifier in the Client ID field.
  • Enter http://localhost:3000/callback as the Redirect URL field.
  • Enter “openid blogdemo.rcbj.net/read blogdemo.rcbj.net/write” in the Scope field (or whatever value is valid in your configuration).
  • Click the Get Token button.
  • Three tokens are returned as depicted above: access_token, refresh_token, and id_token.
  • Scroll down to the “Obtain New Access Token Using Refresh Token” section.
  • Enter your client identifier in the Client ID field.
  • Enter “openid blogdemo.rcbj.net/read blogdemo.rcbj.net/write” in the Scope field (or whatever value is valid for your configuration.
  • Click the Get Token button.
  • You will see two tokens returned: access_token and id_token. The same refresh token can be used for as long as it is valid (30 days by default with Cognito).
  • You could continue to obtain new tokens for as long the refresh token is valid.
  • So, we now know that Cognito is configured the way we need it to be to work with the test applications we will be using later.

See The User In Cognito

  • Go to the Users and Groups screen in the Cognito Admin section of the AWS Console.
  • You can search for the user you just created by choosing email from the drop down, enter the user’s email address, and press enter.
  • Click on the username link of the user that is listed.

User Authentication

The response from the token endpoint following a successful end user authentication looks like the following:

{
"id_token": "eyJraWQiOiJUcTd6K0tidsfasdfsfdaUTZnVGZPWFwveEpUOFJDaXZOT1B6cjRCUT0iLCJhbGciOiJSUzI1NiJ9.eyJhdFfsadfsdfsdfsdfocmpSaDhRSE9BRmlHdmNadyIsInN1YiI6IjlkYTA4YjljLTBjNTctNDUyNi1hODFhLTE0MTA3MDllOTMzMiIsImF1ZCI6IjVvNjNkZWtua2VocjZxMTVqbGVpbjd2NHVnIiwidG9rZW5fdXNlIjoiaWQiLCJhdXRoX3RpbWUiOjE1MzQwNjEwNjAsImesedfsdfdsfvXC9jb2duaXRvLWlkcC51cy13ZXN0LTIuYW1hem9uYXdzLmNvbVwvdXMtd2VzdC0yX1dhUE42QmI3SCIsImNvZ25pdG86dXNlcm5hbWUiOiI5ZGEwOGI5Yy0wYzU3LTQ1MjYtYTgxYS0xNDEwNzA5ZTkzMzIiLCJleHAiOjE1MzQwNjQ2NjAsImdpdmVuX25hbWUiOiJSb2JlcnQiLCJpYXQiOjE1MzQwNjEwNjAsImZhbWlseV9uYW1lIjoiQnJvZWNrZWxtYW5uIiwiZW1haWwiOiJicm9lY2tlbEByY2JqLm5ldCJ9.r3091WOO9weaFdmpFB7M_u_NBvK8sa8KbxG37PsfYl8ierlFrYMJYtXC01IOJhPfXD5YnQd6oE8IuZkU2XmLuhgYzPfsafsdfsdfouoT1NBh-EVsE7DZAT6jQugapkDcTD07ApsqpJED79omkM2WX0pn34xrza3lQydNCcLKvwMSJG9ENLsArqBedJ3tglgBMQE_r-rFlUdmg",
"access_token": "eyJraWQiOiJvsagfasfasdfasdGhRVmt3cnhsSmdmZG11VUU1K203RHh2czRZPSIsImFsZyI6IlJTMjU2In0.eyJzdWIiOiI5zsdfasdfasdfadsfdsafasdfasdfasdfasdfasdfasdfadfadsfasdfasdfasdfasdfMiLCJzY29wZSI6Im9wZW5pZCBkMW54Zjh5emQ2YzVicy5jbG91ZGZyb250Lm5ldFwvc2NvcGUxIiwiYXV0aF90aW1lIjoxNTM0MDYxMDYwLCJpc3MiOiJodHRwczpcL1wvY29nbml0by1pZHAudXMtd2VzdC0yLmFtYXpvbmF3cy5jb21cL3VzLXdlc3QtMl9XYVBONkJiN0giLCJleHAiOjE1MzQwNjQ2NjAsImlhdCI6MTUzNDA2MTA2MCwidmVyc2lvbiI6MiwianRpIjoiYTMxMTkxZDktOTNhNy00YmFhLWJmOWUtYTYwZmY5ODI5ZjUzIiwiY2xpZW50X2lkIjoiNW82M2Rla25rZWhyNnExNWpsZWluN3Y0dWciLCJ1c2VybmFtZSI6IjlkYTA4YjljLTBjNTctNDUyNi1hODFhLTE0MTA3MDllOTMzMiJ9.BDk_zid-F8XHhZhVOdmYHUCGS0XymjjCkzz4CyqgNTBGCQO6kjjWu9t5_8xllpTsjCwhqrK-5Ba8RNUdZNf8vsbas7IzRRNqIMS4CSYxp22LeYXr7r_inpTm2nWMM9SynTO6GGVg1sk3wvb505ylXTL2hF0IH0TazuiqQkb0w4BJyZIyycGCie8D28DUBgEZRYVV9w4pc-QZ6P1iFRziHcqr9icK5DIoEMe7DYou-vC0re-RW-xCz9YCuLhD7gTWADqS1_qXia7npjhtcCjgvglEPmc1r-p-RrC8hD2ATHZSP9V4paVwuhjddlkkp3RSV9JplN8jSxCbvLDJHkrdsA",
"refresh_token": "eyJjdHkiOiJKV1QiLdsfsdfsdMjU2R0NNIiwiYWxnIjoiUlNBLU9BRVAifQ.CoZZ0filEm6jYCm64Uc8MfXSGMzE7cngoFcMjr2dY9EsdfasdfadfsfadsfasdfagadczvcxzbbvbfdzvcxzvzcxczvzxvcE194dcCFD-KzSSagbTrtjKWMuhu2OVAALQes1q4jUT3Rcg4KoTETJ2RLlIJnvf4Pp_Jh34bScEWvF4sYug2PAw44sgz5TQ3Lp5EuBV_TQPru25lTzrnH7Y7vtY1-mWjY2wlSSOoobnVnxi8IUrKRNxi_OMJneTDqOOw.p-aRDfsA_y9_VgXQ.gTo7W6KfItDR6Bk7LhkP0SFMbmJz2FT5P4Cj8vMgASvSZkRIWuJfDrhsM7AwotIk8NOMc6t0daUTY8wRlRlaS3AZ8Cm2hSgPLWs1zqWOoHBGFbGBvNY9bqQZud1jM5XcLibUb1_Jqsu7LBUbBWu461d4ypwlbFSgIN53TCg6WBrfWncc7ZB4Eit9__Cyg0HmYz-jmpbzGiav76hA1Ji4oSw8aYYbrzleyh1nwAHyw4aOF6bupBgV88sgAcnH4WSocToqfB64cxrR8DSA8fvvgj2qdySTv1e6oKTWXKruzeoYJ1sF2z2a7RIHGsa3FK6XyGsHiWkcnrtV7Opl39e5iGXHJ5BRDwrA926n_kALEV4NcG4yLabiqc6SzO1YQbqkYFkJixeL4oMRSCRXyWyNoYccicFuh112Tdk6Z-3JXbV0xZoGNcWmVrIydzT-hLyaCY00P4IkeluL7sPJEsWTvB3As1F9Vx7s4bWgm4iQUfOea6M0iJu8gtd6CUtOL6SecXStayO7oGCct-9e8D9t2cwmTlp2dDikDDd86mdxlN75anoCPYi99kEslSLoj2h3isOIPhn0nnh-ZAG-ln85gbLlv07YqxlO7rlKX4N9K3Hv8Fhpl9pMLRpXgVVbYmNjYXK8Y1NLFviw0FRd10AsNshZ2Sb0rhMQ77zlfW56FoaqJva78gWEDY8ZH6mj75o5up_wJdOMhrz72TOoNvZjSwERuRUGC37aVSl4DYM5VgGR0MdQaslWGlhaQobd9lSb_5uV7ibHfaRBNVy3fDtdRXetd18OTz37yaPHDfkSXmrTCGXvPDV4pG6hg3mZpB7RyI94A5ZkCI5BAgoOfdbmce7JyymLVvOqS9o1zDJ6r2zabfFT1H00ZpNRfLDV1Q4a3n1nHLUNSzbTph3HaJ4fn7I72iKDqn-0UeObP2640qGeI5K2VsNO0AtVzvgI_t7tqNdgEfoiYtseliIJ_rYEpwzaKDt33-zh57zuwEmE86F6P_j6KN0uajcFZkVGtdtDX7dRNSJbMhjaVsWz-72Fx4QUJ0Nu0rxHcC74A8QbialN-mCwMbiZehbP6xLm65ntFAMyDUDl2AP02VrOl_P7290v9ryAPnqt1hFh0CTWnbAlTgnRReIwv5yk64YgLvsFV68ENzwwg45Z - Z7bBGADk0fTpTypWMmbGf3WThjXdjC97Iz0g8MHhsMqynSddHVHCsgI7zVZ7rxw8YhC38wM8rb9zA42E7YzRLxrmpUuCfxAtWhU-97BevU6uDBCgp2FHo8YQOmleJNwzYFh7w.3xlqUobOFP_V1a-H7-tGnQ",
"expires_in": 3600,
"token_type": "Bearer"
}

The ID Token JWT Payload looks like:

{
"at_hash": "RT7jMhrjRh8QHOAFiGvcZw",
"sub": "9da08b9c-0c57-4526-a81a-1410709e9332",
"aud": "5o63deknkehr6q15jlein7v4ug",
"token_use": "id",
"auth_time": 1534061060,
"iss": "https://cognito-idp.us-west-2.amazonaws.com/us-west-2_WaPN6Bb7H",
"cognito:username": "9da08b9c-0c57-4526-a81a-1410709e9332",
"exp": 1534064660,
"given_name": "Robert",
"iat": 1534061060,
"family_name": "Broeckelmann",
"email": "rcbj@rcbj.net"
}

The Access Token JWT Payload looks like:

{
"sub": "9da08b9c-0c57-4526-a81a-1410709e9332",
"token_use": "access",
"scope": "openid blogdemo.rcbj.net/read blogdemo.rcbj.net/write",
"auth_time": 1534061060,
"iss": "https://cognito-idp.us-west-2.amazonaws.com/us-west-2_WaPN6Bb7H",
"exp": 1534064660,
"iat": 1534061060,
"version": 2,
"jti": "a31191d9-93a7-4baa-bf9e-a60ff9829f53",
"client_id": "5o63deknkehr6q15jlein7v4ug",
"username": "9da08b9c-0c57-4526-a81a-1410709e9332"
}
  • Go to the Users and Groups screen of the BlogDemo User Pool.
  • Click on the username of the user that you just created.
  • You can see the user attributes here.

Summary

In this post, we looked at the AWS Cognito setup for an OpenID Connect client application and demonstrated it working with OAuth2 + OIDC Debugger.

In subsequent posts, we’ll look at:

  • AWS Cognito out-of-the-box native user registration and login.
  • Demonstrate federated user registration and login with social login providers (Facebook, Google+), SAML2, and OpenID Connect.
  • How to use access tokens issued from AWS Cognito with the AWS API Gateway.

Image: 2011_0203 — Natural Stone_6 / Ben Hosking