fbpx

Use Amazon Cognito to add claims to an identity token for fine-grained authorization

With Amazon Cognito, you can quickly add user sign-up, sign-in, and access control to your web and mobile applications. After a user signs in successfully, Cognito generates an identity token for user authorization. The service provides a pre token generation trigger, which you can use to customize identity token claims before token generation. In this blog post, we’ll demonstrate how to perform fine-grained authorization, which provides additional details about an authenticated user by using claims that are added to the identity token. The solution uses a pre token generation trigger to add these claims to the identity token.

   <h2>Scenario</h2> 
   <p>Imagine a web application that is used by a construction company, where engineers log in to review information related to multiple projects. We’ll look at two different ways of designing the architecture for this scenario: a standard design and a more optimized design.</p> 
   <h3>Standard architecture</h3> 
   <p>A sample standard architecture for such an application is shown in Figure 1, with labels for the various workflow steps:</p> 
   <ol> 
    <li>The user interface is implemented by using ReactJS (a JavaScript library for building user interfaces).</li> 
    <li>The user pool is configured in <a href="https://aws.amazon.com/cognito/" target="_blank" rel="noopener noreferrer">Amazon Cognito</a>.</li> 
    <li>The back end is implemented by using <a href="https://aws.amazon.com/api-gateway/" target="_blank" rel="noopener noreferrer">Amazon API Gateway</a>.</li> 
    <li><a href="https://aws.amazon.com/lambda/" target="_blank" rel="noopener noreferrer">AWS Lambda</a> functions exist to implement business logic.</li> 
    <li>The AWS Lambda CheckUserAccess function (5) checks whether the user has authorization to call the AWS Lambda functions (4).</li> 
    <li>The project information is stored in an <a href="https://aws.amazon.com/dynamodb/" target="_blank" rel="noopener noreferrer">Amazon DynamoDB</a> database.</li> 
   </ol> 
   <div id="attachment_25474" class="wp-caption aligncenter"> 
    <img aria-describedby="caption-attachment-25474" src="https://infracom.com.sg/wp-content/uploads/2022/06/Figure1.png" alt="Figure 1: Lambda functions that need the user’s projectID call the GetProjectID Lambda function" width="800" class="size-full wp-image-25474"> 
    <p id="caption-attachment-25474" class="wp-caption-text">Figure 1: Lambda functions that need the user’s projectID call the GetProjectID Lambda function</p> 
   </div> 
   <p>In this scenario, because the user has access to information from several projects, several backend functions use calls to the <span>CheckUserAccess</span> Lambda function (step 5 in Figure 1) in order to serve the information that was requested. This will result in multiple calls to the function for the same user, which introduces latency into the system.</p> 
   <h3>Optimized architecture</h3> 
   <p>This blog post introduces a new optimized design, shown in Figure 2, which substantially reduces calls to the <span>CheckUserAccess</span> API endpoint:</p> 
   <ol> 
    <li>The user logs in.</li> 
    <li>Amazon Cognito makes a single call to the <span>PretokenGenerationLambdaFunction-pretokenCognito</span> function.</li> 
    <li>The <span>PretokenGenerationLambdaFunction-pretokenCognito</span> function queries the Project ID from the DynamoDB table and adds that information to the Identity token.</li> 
    <li>DynamoDB delivers the query result to the <span>PretokenGenerationLambdaFunction-pretokenCognito</span> function.</li> 
    <li>This Identity token is passed in the authorization header for making calls to the Amazon API Gateway endpoint.</li> 
    <li>Information in the identity token claims is used by the Lambda functions that contain business logic, for additional fine-grained authorization. Therefore, the <span>CheckUserAccess</span> function (7) need not be called. </li> 
   </ol> 
   <p>The improved architecture is shown in Figure 2.</p> 
   <div id="attachment_25475" class="wp-caption aligncenter"> 
    <img aria-describedby="caption-attachment-25475" src="https://infracom.com.sg/wp-content/uploads/2022/06/Figure2.png" alt="Figure 2. Get the projectID and inset it in a custom claim in the Identity token" width="800" class="size-full wp-image-25475"> 
    <p id="caption-attachment-25475" class="wp-caption-text">Figure 2. Get the projectID and inset it in a custom claim in the Identity token</p> 
   </div> 
   <p>The benefits of this approach are:</p> 
   <ol> 
    <li>The number of calls to get the Project ID from the DynamoDB table are reduced, which in turn reduces overall latency.</li> 
    <li>The dependency on the <span>CheckUserAccess</span> Lambda function is removed from the business logic. This reduces coupling in the architecture, as depicted in the diagram.</li> 
   </ol> 
   <p>In the code sample provided in this post, the user interface is run locally from the user’s computer, for simplicity.</p> 
   <h2>Code sample</h2> 
   <p>You can <a href="https://awsiammedia.s3.amazonaws.com/public/sample/257-cognito-pre-token-generation-trigger/addclaimstoidtoken.zip" target="_blank" rel="noopener noreferrer">download a zip file</a> that contains the code and the <a href="https://aws.amazon.com/cloudformation/" target="_blank" rel="noopener noreferrer">AWS CloudFormation</a> template to implement this solution. The code that we provide to illustrate this solution is described in the following sections.</p> 
   <h2>Prerequisites</h2> 
   <p>Before you deploy this solution, you must first do the following:</p> 
   <ol> 
    <li>Download and install <a href="https://www.python.org/downloads/" target="_blank" rel="noopener noreferrer">Python 3.7</a> or later.</li> 
    <li>Download the <a href="https://aws.amazon.com/sdk-for-python/" target="_blank" rel="noopener noreferrer">AWS SDK for Python (Boto3) library</a> by using the following pip command.<br><span>pip install boto3</span></li> 
    <li>Install the <a href="https://docs.python.org/3/library/argparse.html" target="_blank" rel="noopener noreferrer">argparse</a> package by using the following pip command.<br><span>pip install argparse</span></li> 
    <li>Install the <a href="https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-install.html" target="_blank" rel="noopener noreferrer">AWS Command Line Interface (AWS CLI)</a>.</li> 
    <li>Configure the <a href="https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-configure.html" target="_blank" rel="noopener noreferrer">AWS CLI</a>.</li> 
    <li>Download a code editor for Python. We used <a href="https://code.visualstudio.com/" target="_blank" rel="noopener noreferrer">Visual Studio Code</a> for this post.</li> 
    <li>Install <a href="https://nodejs.org/en/download/" target="_blank" rel="noopener noreferrer">Node.js</a>.</li> 
   </ol> 
   <h2>Description of infrastructure</h2> 
   <p>The code provided with this post installs the following infrastructure in your AWS account.</p> 
   <table width="100%"> 
    <tbody> 
     <tr> 
      <td width="45%"><strong>Resource</strong></td> 
      <td width="55%"><strong>Description</strong></td> 
     </tr> 
     <tr> 
      <td width="45%">Amazon Cognito user pool</td> 
      <td width="55%">The users, added by the addUserInfo.py script, are added to this pool. The client ID is used to identify the web client that will connect to the user pool. The user pool domain is used by the web client to request authentication of the user.</td> 
     </tr> 
     <tr> 
      <td width="45%">Required <a href="http://aws.amazon.com/iam" rel="noopener noreferrer" target="_blank">AWS Identity and Access Management (IAM)</a> roles and policies</td> 
      <td width="55%">Policies used for running the Lambda function and connecting to the DynamoDB database. </td> 
     </tr> 
     <tr> 
      <td width="45%">Lambda function for the pre token generation trigger</td> 
      <td width="55%">A Lambda function to add custom claims to the Identity token.</td> 
     </tr> 
     <tr> 
      <td width="45%">DynamoDB table with user information</td> 
      <td width="55%">A sample database to store user information that is specific to the application.</td> 
     </tr> 
    </tbody> 
   </table> 
   <h2>Deploy the solution</h2> 
   <p>In this section, we describe how to deploy the infrastructure, save the trigger configuration, add users to the Cognito user pool, and run the web application.</p> 
   <p><strong>To deploy the solution infrastructure</strong></p> 
   <ol> 
    <li><a href="https://awsiammedia.s3.amazonaws.com/public/sample/257-cognito-pre-token-generation-trigger/addclaimstoidtoken.zip" target="_blank" rel="noopener noreferrer">Download the zip file</a> to your machine. The <span>readme.md</span> file in the <span>addclaimstoidtoken</span> folder includes a table that describes the key files in the code.</li> 
    <li>Change the directory to <span>addclaimstoidtoken</span>.<br><span>cd addclaimstoidtoken</span></li> 
    <li>Review <span>stackInputs.json</span>. Change the value of the <span>userPoolDomainName</span> parameter to a random unique value of your choice. This example uses <span>pretokendomainname</span> as the Amazon Cognito domain name; you should change it to a unique domain name of your choice.</li> 
    <li>Deploy the infrastructure by running the following Python script.<br><span>python3 setup_pretoken.py</span> <p>After the CloudFormation stack creation is complete, you should see the details of the infrastructure created as depicted in Figure 3.</p> 
     <div id="attachment_25453" class="wp-caption aligncenter"> 
      <img aria-describedby="caption-attachment-25453" id="_To_add_users" src="https://infracom.com.sg/wp-content/uploads/2022/06/image3-1024x356-1.png" alt="Figure 3: Details of infrastructure" width="700" class="size-large wp-image-25453"> 
      <p id="caption-attachment-25453" class="wp-caption-text">Figure 3: Details of infrastructure</p> 
     </div> </li> 
   </ol> 
   <p>Now you’re ready to add users to your Amazon Cognito user pool.</p> 
   <p><strong>To add users to your Cognito user pool</strong></p> 
   <ol> 
    <li>To add users to the Cognito user pool and configure the DynamoDB store, run the Python script from the <span>addclaimstoidtoken</span> directory.<br><span>python3 add_user_info.py</span></li> 
    <li>This script adds one user. It will prompt you to provide a username, email, and password for the user.<br><blockquote> 
      <p><strong>Note</strong>: Because this is sample code, advanced features of Cognito, like multi-factor authentication, are not enabled. We recommend enabling these features for a production application.</p> 
     </blockquote> <p>The <span>addUserInfo.py</span> script performs two actions:</p> 
     <ul> 
      <li>Adds the user to the Cognito user pool. 
       <div id="attachment_25457" class="wp-caption aligncenter"> 
        <img aria-describedby="caption-attachment-25457" src="https://infracom.com.sg/wp-content/uploads/2022/06/image4.png" alt="Figure 4: User added to the Cognito user pool" width="700" class="size-full wp-image-25457"> 
        <p id="caption-attachment-25457" class="wp-caption-text">Figure 4: User added to the Cognito user pool</p> 
       </div> </li> 
      <li>Adds sample data to the DynamoDB table. 
       <div id="attachment_25458" class="wp-caption aligncenter"> 
        <img aria-describedby="caption-attachment-25458" src="https://infracom.com.sg/wp-content/uploads/2022/06/image5-1024x405-1.png" alt="Figure 5: Sample data added to the DynamoDB table named UserInfoTable" width="700" class="size-large wp-image-25458"> 
        <p id="caption-attachment-25458" class="wp-caption-text">Figure 5: Sample data added to the DynamoDB table named UserInfoTable</p> 
       </div> </li> 
     </ul> </li> 
   </ol> 
   <p>Now you’re ready to run the application to verify the custom claim addition.</p> 
   <p><strong>To run the web application</strong></p> 
   <ol> 
    <li>Change the directory to the <span>pre-token-web-app</span> directory and run the following command.<br><span>cd pre-token-web-app</span></li> 
    <li>This directory contains a ReactJS web application that displays details of the identity token. On the terminal, run the following commands to run the ReactJS application.<br><span>npm install</span><br><span>npm start</span> <p>This should open <span>http://localhost:8081</span> in your default browser window that shows the <strong>Login </strong>button.</p> 
     <div id="attachment_25459" class="wp-caption aligncenter"> 
      <img aria-describedby="caption-attachment-25459" src="https://infracom.com.sg/wp-content/uploads/2022/06/image6.png" alt="Figure 6: Browser opens to URL http://localhost:8081" width="495" height="253" class="size-full wp-image-25459"> 
      <p id="caption-attachment-25459" class="wp-caption-text">Figure 6: Browser opens to URL http://localhost:8081</p> 
     </div> </li> 
    <li>Choose the <strong>Login</strong> button. After you do so, the Cognito-hosted login screen is displayed. Log in to the website with the user identity you created by using the <span>addUserInfo.py</span> script in step 1 of the <a href="https://aws.amazon.com/blogs/security/use-amazon-cognito-to-add-claims-to-an-identity-token-for-fine-grained-authorization/#_To_add_users" rel="noopener noreferrer"><strong>To add users to your Cognito user pool</strong></a> procedure. 
     <div id="attachment_25460" class="wp-caption aligncenter"> 
      <img aria-describedby="caption-attachment-25460" src="https://infracom.com.sg/wp-content/uploads/2022/06/image7.png" alt="Figure 7: Input credentials in the Cognito-hosted login screen" width="456" height="516" class="size-full wp-image-25460"> 
      <p id="caption-attachment-25460" class="wp-caption-text">Figure 7: Input credentials in the Cognito-hosted login screen</p> 
     </div> </li> 
    <li>When the login is successful, the next screen displays the identity and access tokens in the URL. You can reveal the token details to verify that the custom claim has been added to the token by choosing the <strong>Show Token Detail</strong> button. 
     <div id="attachment_25461" class="wp-caption aligncenter"> 
      <img aria-describedby="caption-attachment-25461" src="https://infracom.com.sg/wp-content/uploads/2022/06/image8.png" alt="Figure 8: Token details displayed in the browser" width="600" class="size-full wp-image-25461"> 
      <p id="caption-attachment-25461" class="wp-caption-text">Figure 8: Token details displayed in the browser</p> 
     </div> </li> 
   </ol> 
   <h2>What happened behind the scenes?</h2> 
   <p>In this web application, the following steps happened behind the scenes:</p> 
   <ol> 
    <li>When you ran the npm start command on the terminal command line, that ran the <span>react-scripts</span> start command from <span>package.json</span>. The port number (8081) was configured in the <span>pre-token-web-app/.env</span> file. This opened the web application that was defined in <span>app.js</span> in the default browser at the URL <span>http://localhost:8081</span>.</li> 
    <li>The <strong>Login</strong> button is configured to navigate to the URL that was defined in the <span>constants.js</span> file. The <span>constants.js </span>file was generated during the running of the <span>setup_pretoken.py</span> script. This URL points to the Cognito-hosted default login user interface.</li> 
    <li>When you provided the login information (username and password), Amazon Cognito authenticated the user. Before generating the set of tokens (identity token and access token), Cognito first called the pre-token-generation Lambda trigger. This Lambda function has the code to connect to the DynamoDB database. The Lambda function can then access the project information for the user that is stored in the userInfo table. The Lambda function read this project information and added it to the identity token that was delivered to the web application. <p><strong>Lambda function code</strong></p> 
     <div class="hide-language"> 
      <pre class="unlimited-height-code"><code class="lang-text"><span>const</span> <span>AWS</span> = <span>require</span>(<span>"aws-sdk"</span>);

// Create the DynamoDB service object
var ddb = new AWS.DynamoDB({ apiVersion: “2012-08-10” });

// PretokenGeneration Lambda
exports.handler = async function (event, context) {
var eventUserName = “”;
var projects = “”;

<span>if</span> (!event.userName) {
    <span>return</span> <span>event</span>;
}

<span>var</span> <span>params</span> = {
    ExpressionAttributeValues: {
        <span>":v1"</span>: {
            <span>S: event.userName</span>
        }
    },
    KeyConditionExpression: <span>"userName = :v1"</span>,
    ProjectionExpression: <span>"projects"</span>,
    <span>TableName</span>: "UserInfoTable"
};

event.response = {
    "claimsOverrideDetails": {
        "claimsToAddOrOverride": {
            <span>"userName"</span>: <span>event.userName</span>,
            <span>"projects"</span>: <span>null</span>
        },
    }
};

<span>try</span> {
    <span>let</span> <span>result</span> = <span>await</span> <span>ddb</span>.<span>query</span>(<span>params</span>).<span>promise</span>();
    <span>if</span> (<span>result.Items.length</span> &gt; <span>0</span>) {
        <span>const</span> <span>projects</span> = <span>result.Items</span>[<span>0</span>][<span>"projects"</span>][<span>"S"</span>];
        <span>console</span>.<span>log</span>(<span>"projects = "</span> + <span>projects</span>);
        event.response.claimsOverrideDetails.claimsToAddOrOverride.projects = <span>projects</span>;
    }
}
<span>catch</span> (<span>error</span>) {
    <span>console</span>.<span>log</span>(<span>error</span>);
}

<span>return</span> <span>event</span>;

};
The code for the Lambda function is as follows.

 

  • After a successful login, Amazon Cognito redirected to the URL that was specified in the App Client Settings section, and added the token to the URL.
  • The webpage detected the token in the URL and displayed the Show Token Detail button. When you selected the button, the webpage read the token in the URL, decoded the token, and displayed the information in the relevant text boxes.
  • Notice that the Decoded ID Token box shows the custom claim named projects that displays the projectID that was added by the PretokenGenerationLambdaFunction-pretokenCognito trigger.
   <h2>How to use the sample code in your application</h2> 
   <p>We recommend that you use this sample code with the following modifications:</p> 
   <ol> 
    <li>The code provided does not implement the API Gateway and Lambda functions that consume the custom claim information. You should implement the necessary Lambda functions and read the custom claim for the event object. This event object is a JSON-formatted object that contains authorization data.</li> 
    <li>The ReactJS-based user interface should be hosted on an <a href="https://aws.amazon.com/s3/" target="_blank" rel="noopener noreferrer">Amazon Simple Storage Service (Amazon S3)</a> bucket. </li> 
    <li>The <span>projectId</span> of the user is available in the token. Therefore, when the token is passed by the <span>Authorization</span> trigger to the back end, this custom claim information can be used to perform actions specific to the project for that user. For example, getting all of that user’s work items that are related to the project.</li> 
    <li>Because the token is valid for one hour, the information in the custom claim information is available to the user interface during that time.</li> 
    <li>You can use the <a href="https://docs.amplify.aws/lib/q/platform/js/" target="_blank" rel="noopener noreferrer">AWS Amplify library</a> to simplify the communication between your web application and Amazon Cognito. <a href="https://aws.amazon.com/amplify/" target="_blank" rel="noopener noreferrer">AWS Amplify</a> can handle the token retention and refresh token mechanism for the web application. This also removes the need for the token to be displayed in the URL.</li> 
    <li>If you’re using Amazon Cognito to manage your users and authenticate them, using the <a href="https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-identity-pools.html" target="_blank" rel="noopener noreferrer">Amazon Cognito user pool</a> to control access to your API is easier, because you don’t have to write the authentication code in your authorizer.</li> 
    <li>If you decide to use <a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-use-lambda-authorizer.html" target="_blank" rel="noopener noreferrer">Lambda authorizers</a>, note the following important information from the topic <a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-use-lambda-authorizer.html#api-gateway-lambda-authorizer-create" target="_blank" rel="noopener noreferrer">Steps to create an API Gateway Lambda authorizer</a>: “In production code, you may need to authenticate the user before granting authorization. If so, you can add authentication logic in the Lambda function as well by calling an authentication provider as directed in the documentation for that provider.”</li> 
    <li>Lambda authorizer is recommended if the final authorization (not just token validity) decision is made based on custom claims.</li> 
   </ol> 
   <h2>Conclusion</h2> 
   <p>In this blog post, we demonstrated how to implement fine-grained authorization based on data stored in the back end, by using claims stored in an identity token that is generated by the Amazon Cognito pre token generation trigger. This solution can help you achieve a reduction in latency and improvement in performance. </p> 
   <p>For more information on the pre token generation Lambda trigger, refer to the <a href="https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-lambda-pre-token-generation.html" target="_blank" rel="noopener noreferrer">Amazon Cognito Developer Guide</a>.</p> 
   <p>&nbsp;<br />If you have feedback about this post, submit comments in the<strong> Comments</strong> section below. If you have questions about this post, <a href="https://console.aws.amazon.com/support/home" target="_blank" rel="noopener noreferrer">contact AWS Support</a>.</p> 
   <p><strong>Want more AWS Security news? Follow us on <a title="Twitter" href="https://twitter.com/AWSsecurityinfo" target="_blank" rel="noopener noreferrer">Twitter</a>.</strong>
   <!-- '"` -->