AWS Compute Blog

SAML for Your Serverless JavaScript Application: Part I

Contributors: Richard Threlkeld, Gene Ting, Stefano Buliani

The full code for this blog, including SAM templates—can be found at the samljs-serverless-sample GitHub repository. We highly recommend you use the SAM templates in the GitHub repository to create the resources, opitonally you can manually create them.


Want to enable SAML federated authentication? You can use the AWS platform to exchange SAML assertions for short-term, temporary AWS credentials.

When you build enterprise web applications, it is imperative to ensure that authentication and authorization (AuthN and AuthZ) is done consistently and follows industry best practices. At AWS, we have built a service called Amazon Cognito that allows you to create unique Identities for users and grants them short-term credentials for interacting with AWS services. These credentials are tied to roles based on IAM policies so that you can grant or deny access to different resources.

In this post, we walk you through different strategies for federating SAML providers with Amazon Cognito. Additionally, you can federate with different types of identity providers (IdP). These IdPs could be third-party social media services like Facebook, Twitter, and others. You can also federate with the User Pools service of Amazon Cognito and create your own managed user directory (including registration, sign-In, MFA, and other workflows).

For example, you may want to build a JavaScript application that allows a user to authenticate against Active Directory Federation Services (ADFS). The user can be granted scoped AWS credentials to invoke an API to display information in the application or write to an Amazon DynamoDB table. In the Announcing SAML Support for Amazon Cognito AWS Mobile blog post, we introduced the new SAML functionality with some sample code in Java as well as Android and iOS snippets. This post goes deeper into customizing the ADFS flow and JavaScript samples.

Scenarios

In this post, we cover the scenario of “client-side” flow, where SAML assertions are passed through Amazon API Gateway and the browser code retrieves credentials directly from Amazon Cognito Identity. In the next, related post, we’ll cover “backend request logic”, where the SAML assertions and credentials selection take place in AWS Lambda functions allowing for customized business logic and audit tracking.

The full code for this scenario, including SAM templates—can be found at the samljs-serverless-sample GitHub repository. We highly recommend you use the SAM templates in the github repository to create the resources, opitonally you can manually create them.

Although this is a Serverless system, you can substitute certain components for traditional compute types, like EC2 instances, to integrate with ADFS. For terms and definitions used in this post, see Claims-based identity term definitions.

Prerequisites

For this blog post, you need ADFS running in your environment. Use the following references:

  1. ADFS federated with the AWS console. For a walkthrough with an AWS CloudFormation template, see Enabling Federation to AWS Using Windows Active Directory, ADFS, and SAML 2.0.
  2. Verify that you can authenticate with user example\bob for both the ADFS-Dev and ADFS-Production groups via the sign-in page (https://localhost/adfs/IdpInitiatedSignOn.aspx).
  3. Create an Amazon Cognito identity pool.

ADFS configuration overview

Before you start the tutorials, review a few AWS and SAML details, starting with the IAM roles that were created in the prerequisites. You may create a new IAM role and AD group for your application. For this post, we use the ADFS-Dev and ADFS-Production from the previous Enabling Federation to AWS Using Windows Active Directory, ADFS, and SAML 2.0 post as a learning exercise.

  1. In the IAM console, choose Roles, select ADFS-Dev, and choose Trust Relationships tab to see the following code:
    {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Effect": "Allow",
          "Principal": {
            "Federated": "arn:aws:iam::YOURACCOUNTNUMBER:saml-provider/ADFS"
          },
          "Action": "sts:AssumeRoleWithSAML",
          "Condition": {
            "StringEquals": {
              "SAML:aud": "https://signin.thinkwithwp.com/saml"
            }
          }
        }
      ]
    }

This policy allows a user authenticated with the SAML IdP called “ADFS” to assume the role. It also includes a condition where the audience restriction (contained in the SAML assertion) has an AudienceRestriction of https://signin.thinkwithwp.com/saml. The SAML assertion is sent to AWS with a header called SAMLResponse from ADFS via an HTTP POST. If you have followed the federation post listed in the Prerequisites section, this would have been configured in the ADFS console when you specified the AWS metadata URL as the relaying party. For more information about this process, see Configuring SAML Assertions for the Authentication Response.

lambdasamlone_1.png

lambdasamlone_2.png

After authentication, ADFS automatically redirects to the Relaying Party Application realm.

lambdasamlone_3.png

In the above screenshot, we used Chrome to view the SAMLResponse value after authenticating. You can do this in other browsers, as documented in How to View a SAML Response in Your Browser for Troubleshooting. There are “SAML Decoders” available on the Internet that allow you to view values such as Audience, Roles, and Destination if you paste in SAMLResponse. In Part II of this blog series we will walk through how this can be done programmatically.

Redirect from ADFS to a Serverless website followed by Amazon Cognito federation

The scenario in this first blog is less complex; for many organizational requirements, it might be the best route. Users authenticate against ADFS and receive AWS credentials from Amazon Cognito so that they can perform actions in your app. This sample application:

  1. Exposes a login mechanism to authenticate against ADFS and captures the SAMLResponse header. This is automatically kicked off when the user visits the website hosted on S3.
  2. Changes the ADFS-Dev role trust policy to allow users that are in the AWS-gDev Active Directory group to receive temporary AWS credentials from Amazon Cognito.
  3. Adds a mechanism in the code to select the application role for the user. In this post, the chosen role is ADFS-Dev.

Note that #3 is happening in the federated AWS console example when the user clicks the radio button after the redirect from the IdpInitiatedSignOn.aspx webpage that ADFS provides. For more information, see the Enabling Federation to AWS Using Windows Active Directory, ADFS, and SAML 2.0 post on the AWS Security blog. If users are only in a single Active Directory group, then #3 can be omitted, as the SAMLResponse will never contain multiple roles in the ADFS claims. Amazon Cognito can automatically assume the single role.

The architecture you build is outlined in the following diagram.

lambdasamlone_4.png

Tutorial: Redirect from ADFS to a Serverless website followed by Amazon Cognito federation

First, set up a Serverless website and initiate a login workflow to get credentials. Use S3 for hosting the single page web app.

S3 bucket creation

  1. In the S3 console, choose Create bucket and enter a unique bucket name. The following example bucket is called “serverlessweb” but yours can be something different.
  2. After the bucket is created, on the details page, choose Properties, Edit bucket policy.lambdasamlone_5.png
  3. Add the following policy, replacing YOURBUCKETNAMEGOESHERE. This policy allows anyone to issue GET requests on any of the objects that it contains, such as HTML or JavaScript files. Web browsers issue GET requests when they try to access websites and this policy allows users to load the resources.
    {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Sid": "PublicReadForGetBucketObjects",
                "Effect": "Allow",
                "Principal": "*",
                "Action": "s3:GetObject",
                "Resource": "arn:aws:s3:::YOURBUCKETNAMEGOESHERE/*"
            }
        ]
    }
  4. Choose Static Website Hosting, Enable website hosting. Type in ‘index.html’ and ‘error.html’ in the appropriate forms.lambdasamlone_6.png
  5. At this point, add a simple HTML file to your bucket and browse to https://YOURBUCKETNAME.s3-website-REGION.amazonaws.com and see the page (replace YOURBUCKETNAME and REGION as appropriate). As a starting point, you can use the following template. First, download the AWS JavaScript SDK and also place it in the bucket.
    <!DOCTYPE html>
      <html>
       <head>
        <title>
         Amazon Cognito SAML Example
        </title>
        <script src="aws-sdk.min.js">
        </script>
       </head>
       <body>
        <h1>
         Testing SAMLResponse
        </h1>
       </body>
      </html>
  6. After downloading the minified version of the JavaScript SDK (aws-sdk.min.js) and placing it in the bucket along with the above HTML file, verify that your page loads without any errors.

(Optional) CloudFront distribution set up

Before going further, you should set up one more thing: a CloudFront distribution. The S3 static web hosting is HTTP, and CloudFront or another CDN provider of your choice provides SSL.

While this isn’t mandatory to get these examples functional (you can do redirects from API Gateway to HTTP sites), you should have end-to-end HTTPS for your site and an AuthN/AuthZ system. Setting this up is minimal work and should be done.

  1. In the CloudFront console, create a new distribution of type Web.
  2. For Viewer Protocol Policy, choose HTTPS Only.
  3. For Origin Domain Name, select your S3 bucket.
  4. As a best practice, choose Restrict Bucket Access so that the bucket is protected from direct browsing. Then you can access the site via the listed Domain Name for the CloudFront distribution.

Login mechanism

Next, build a login mechanism. Your website can either prompt the user to log in with a button or automatically check to see if credentials are valid and redirect them with a login workflow.

This example takes the second approach, using JavaScript to check the status immediately when the user visits the page and redirect them if it’s an initial visit to get their credentials. Because users also get redirected to the page during the login process from API Gateway, the workflow needs to redirect users to the ADFS login as well as capture incoming SAMLResponse data based on some capture progress state. An example flow might look like the following:

function loginWorkflow(){
    var activelogin = sessionStorage.getItem('activelogin');
    if (activelogin=='inProgress'){                                   //ADFS login redirect from API Gateway
        var samlResponse = getParameterByName('SAMLResponse');
        sessionStorage.removeItem(‘activelogin’);
        if (samlResponse != null){
            getSamlCredentials(samlResponse.replace(/\s/g, ''));
        }
    } 
    else if (activelogin === null) {                                 //First page visit. Redirect to ADFS login.
        var RPID = encodeURIComponent('urn:amazon:webservices');
        var result = 'https://localhost/adfs/ls/IdpInitiatedSignOn.aspx' + "?loginToRp=" + RPID;
        sessionStorage.setItem('activelogin', 'inProgress');
        window.location = result;
    }   
    else {//Credentials exist, page refresh, etc.
        console.log('activelogin already exists in session and the value is ' + activelogin);
    }
}

The flow above sets a session variable to initiate a call to the ADFS IdP and return to the site to call a getSamlCredentials() routine after with SAMLResponse (more on this and getParameterByName() in a moment).

The SAMLResponse value from ADFS that AWS requires is only supported in the POST binding. However, S3 static websites can only receive GET requests. For this reason, use API Gateway to capture the SAMLResponse from ADFS for the JavaScript application. Use Lambda as a “proxy” and redirect the user back to the static website along with SAMLResponse, which is encapsulated in a query string.

Lambda function configuration

  1. In the Lambda console, create a function named samlRedirect using /Scenario1/lambda/redirect/index.js from the GitHub repository with Node.js 4.3 as the runtime. The execution role only needs permission to right to CloudWatch Logs for logging.
  2. If you don’t have a basic execution role handy, you can ask Lambda to create a new one when you create the function.
  3. Create environment variables named LOG_LEVEL and REDIRECT_URL. Set LOG_LEVEL to info and REDIRECT_URL to your S3 static website URL (either the endpoint URL displayed on the properties of the bucket when you enabled static hosting or—if you completed the optional step earlier of setting up a CloudFront distribution— the domain name listing).

API Gateway configuration

  1. In the API Gateway console, create an API called SAMLAuth or something similar.
  2. Choose Resources and create a child resource called SAML.lambdasamlone_7.png
  3. Create a POST method. For Integration Type, choose Lambda. Select the samlRedirect function. For Method Request Header, enter SAMLResponse.lambdasamlone_8.png

When you created the samlRedirect function earlier, that function set the location property to contain your static website URL along with SAMLResponse appended as a query string. To finish the redirect, configure API Gateway to respond with a 302 status code.

  1. Delete the Method Response code for 200 and add a 302. Create a Response Header called Location and use an Empty Response Model of Content-Type application/json.lambdasamlone_9.png
  2. Delete the Integration Response for 200 and add one for 302 that has a Method response status of 302. Edit the Response header for Location with a Mapping value of integration.response.body.location.lambdasamlone_10.png
  3. To have the Lambda function capture SAMLResponse and RelayState, edit the Integration Request and add a Body Mapping Template of Content-Type application/x-www-form-urlencoded with the following template:
    {
    "SAMLResponse" :"$input.params('SAMLResponse')",
    "formparams" : $input.json('$')
    }
  4. Save the changes.
  5. On your /saml resource root, choose Actions, Enable CORS, Enable CORS and replace existing CORS headers.
  6. Choose Actions, Deploy API. Use a stage of Prod or something similar. In Stage Editor, choose SDK Generation. For Platform, choose JavaScript and then choose Generate SDK. Save the folder someplace close. Take note of the Invoke URL at the top as you need this for ADFS next.

ADFS redirect configuration

  1. In the ADFS console, edit the properties under Relaying Party Trusts for Amazon Web Services. Disable (clear) the Automatically update relaying party field.lambdasamlone_11.png
  2. Choose Endpoints. For Binding, choose POST; For URL, enter the InvokeURL from API Gateway when you deployed the API. Make sure to add /saml onto the end of the InvokeURL value.lambdasamlone_12.png

    To recap what you’ve built:

    • A webpage that redirects users to the ADFS login page if they don’t have credentials.
    • Upon successful authentication, ADFS sends the user back to the webpage (via API Gateway) along with the SAMLResponse.
    • The SAMLResponse is passed back to the original website as part of the query string.

    To get credentials from Amazon Cognito, grab this query string in your JavaScript code and send it as part of a logins map value for the getCredentialsForIdentity call.

  3. In the Amazon Cognito Federated Identities console, locate the identity pool that you set up in the prerequisites. Find the identity pool ID and put it in the top of the following JavaScript code as well as the appropriate AWS region.
    const identityPool = 'YOURCOGNITOPOOLID';
    AWS.config.region = 'COGNITOREGION'; // Region
    
    AWS.config.credentials = new AWS.CognitoIdentityCredentials({
        IdentityPoolId: identityPool
    });
    
    var identityId = localStorage.getItem('cognitoid');
    if (identityId != null){
        console.log('Identity ID: ' + identityId);
        loginWorkflow();
    } else {
        console.log('Calling GetID');
        AWS.config.credentials.get(function(){
            console.log('Identity ID: ' + AWS.config.credentials.identityId);
            localStorage.setItem('cognitoid', AWS.config.credentials.identityId);
            identityId = localStorage.getItem('cognitoid');
            loginWorkflow();
        });
    }

This code is the entry point for your application by first ensuring that a unique user ID is generated with Amazon Cognito. It then initiates the loginWorkflow() code from earlier to return the SAMLResponse value to the browser in a query string.

The following is a sample utility function from a Stack Overflow query, which allows you to grab this value from the query string. Put this code after the above sample.

function getParameterByName(name, url) {
    if (!url) {
      url = window.location.href;
    }
    name = name.replace(/[\[\]]/g, "\\$&amp;");
    var regex = new RegExp("[?&amp;]" + name + "(=([^&amp;#]*)|&amp;|#|$)"),
        results = regex.exec(url);
    if (!results) return null;
    if (!results[2]) return '';
    return decodeURIComponent(results[2].replace(/\+/g, " "));
}

Now that your webpage has the SAMLResponse base64-encoded value from ADFS, this can be passed to Amazon Cognito in order for the client to get AWS credentials. The getSamlCredentials() routine called by loginWorkflow() looks something like the following:

function getSamlCredentials(samlResponse) {
    var role = 'arn:aws:iam::ACCOUNTNUMBER:role/ADFS-Dev';
    var params = {
        IdentityId: identityId,
        CustomRoleArn: role,
        Logins: {
            'arn:aws:iam:: ACCOUNTNUMBER:saml-provider/ADFS' : samlResponse,
        }
    };

    cognitoidentity.getCredentialsForIdentity(params, function(err, data) {
        if (err) console.log(err, err.stack);
        else {
            console.log('Credentials from Cognito: ');
            console.log(data.Credentials);
        }
    });
}

Notice that ACCOUNTNUMBER for the ARN must be updated in both role variables, which in this case are hardcoded to show the selection process. If you’re not returning multiple roles in your SAML claim, then this parameter isn’t required. If you do, then you might want some logic for selecting the role for the client to assume. Part II of this blog series will cover how you can do this on the backend rather than the client.

Also, take note that the key in the logins map matching up to the samlResponse value is the ARN of the IdP that you created in the IAM console with your ADFS federation metadata document. Update this as well with your account number. This information is what allows Amazon Cognito to do validation for the SAMLResponse value against the ADFS trust.

If you try this code now, it won’t work. First, enable the SAML IdP as an authentication provider in your Amazon Cognito identity pool.

  1. In the Amazon Cognito Federated Identities console, select the pool and scroll to Authentication Providers. Choose SAML. Select the checkbox next to your IdP for ADFS
  2. Choose Save.lambdasamlone_13.png

Next, modify the trust policy to allow Amazon Cognito to assume the role on behalf of the user when the service receives a valid SAMLResponse assertion. Amazon Cognito does this by calling the STS AssumeRoleWithWebIdentity API action.

In the IAM console, edit the trust policy for the ADFS-Dev role with the following policy (insert the Amazon Cognito pool ID where appropriate, as you did in the JavaScript code earlier):

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "",
      "Effect": "Allow",
      "Principal": {
        "Federated": "cognito-identity.amazonaws.com"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "cognito-identity.amazonaws.com:aud": "YOURCOGNITOPOOLID"
        },
        "ForAnyValue:StringLike": {
          "cognito-identity.amazonaws.com:amr": "authenticated"
        }
      }
    }
  ]
}

Now that your Amazon Cognito trusts have been set up, you should be able to test your webpage. The credentials returned to console logging in the “data” object takes the form of a triple (AccessKey, SecretKey, Token), which can be used to sign requests to AWS services. A common scenario for applications is to do this via API Gateway by setting AWS_IAM authorization on a method. If you do this, then you can pass this credentials object through to a SDK request. If the role that you specified above has invoke rights to that API, then they’ll successfully call the method.

For example, if you create an API with API Gateway, you can enable AWS_IAM on those methods. For the role specified above (passed into the logins map), ensure that it has rights to invoke the method. For more details, see Control Access to API Gateway with IAM Permissions. Then, when you deploy the API, you can use the Generate SDK tab on the stage to generate and download a JavaScript SDK of your methods. You then include this in your webpage to call these methods. For more information, see Use a JavaScript SDK Generated by API Gateway. At the bottom of that topic, it shows how you can pass in ACCESSKEY and SECRETKEY to the apigClientFactory.newClient constructor.

Because you are using Amazon Cognito Identity with short-lived credentials (expiring in an hour), you can pass the temporary accessKey, secretKey and sessionToken into the apigClientFactory.newClient constructor:

        var apigwClient = apigClientFactory.newClient({
        accessKey: data.Credentials.AccessKeyId,
        secretKey: data.Credentials.SecretKey,
        sessionToken: data.Credentials.SessionToken
    });  

A working version of the webpage code demonstrating this functionality is in the GitHub repo under /scenario1/website/index.html. Update the configs.js file in the same directory with your appropriate region, Cognito Identity Pool, SAML IdP ARN, and the ADFS-Dev Role ARN. The SAM template also creates a MOCK endpoint in API Gateway with AWS_IAM Authorization, along with a button on the webpage to “ping” as a test to see if the federated credentials are working.

lambdasamlone_14.png

Final Thoughts

Hopefully this walk through tutorial has helped you understand how SAML authentication and AWS authorization with Amazon Cognito Identity works at a deeper level. If you haven’t done so already please do take a few moments to test the full working code located at the samljs-serverless-sample GitHub repository to see this in action and let us know your thoughts.

This scenario may be enough to get you started however some organizations might be looking to do a bit more. For instance you might want to move the role selection away from the client and to a backend process or implement a custom IAM scoping scheme based on business logic. In Part II of this series we’ll walk through how you can accomplish this.