Desktop and Application Streaming

Adding SAML authentication to an Amazon AppStream 2.0 SaaS Portal

The Amazon AppStream 2.0 team released a workshop aimed at helping independent software vendors (ISV) move their Windows applications to a software as a service (SaaS) model. Some ISVs have reported that their customers would like to use their own identity provider with the SaaS to provide easier access to a large user base. This blog provides instructions for providing setting up integration with a SAML identity provider (IdP) using the workshop SaaS portal as a starting point.

Overview

The modifications to your existing ISV SaaS portal provide a new authentication flow that identifies users by email addresses and directs them, if appropriate, to the IdP sign on page.  Once authenticated, using token exchange, a streaming URL is generated for the user and they are then redirected to their streaming session.

Workstation connects to Amazon CloudFront, and Amazon S3 for static web content. Workstation connects to Amazon Cognito and SAML provider tor authentication token exchange. Workstation connects to Amazon API Gateway to proxy connection to AWS Lambd, and finally Amazon AppStream 2.0.

In this walkthrough you will complete the following tasks:

  1. Create a new Amazon CloudFront distribution
  2. Adding SAML identity provider to Amazon Cognito
  3. Build an AWS Lambda backend for SAML forwarding
  4. Update the RESTful web service using Amazon API Gateway
  5. Update the existing SaaS portal static web content

Prerequisites

For this walkthrough, you should have the following prerequisites:

Step 1. Create a new Amazon CloudFront distribution

In this step, we create an Amazon CloudFront distribution to provide TLS encryption to the Amazon S3 content delivery. This distribution replaces the S3 hosted website for the SaaS portal.

  1. Open the Amazon S3 console.
  2. Choose Create Distribution.
  3. Choose Get Started under Web to create a web distribution
  4. For Origin Domain Name, choose the SaaS portal S3 bucket’s REST API endpoint from the drop-down menu.
  5. For Restrict Bucket Access, choose Yes.
  6. For Origin Access Identity, choose Create a New Identity.
  7. For Grant Read Permissions on Bucket, choose Yes, Update Bucket Policy.
  8. Under Default Cache Behavior Settings, change Viewer Protocol Policy to Redirect HTTP to HTTPS.
  9. Scroll to the end and choose Create Distribution.
  10. Make a note of the new distribution’s Domain Name.

Information about configuring a custom domain name can be found in the CloudFront developer guide.

Step 2. Add SAML authentication to your Amazon Cognito User Pool

First we need to add an Amazon Cognito domain to your existing user pool.  Use this as the assertion consumer service (ASC) when configuring the SAML IdP.

  1. Open the Amazon Cognito console.
  2. Choose Manage User Pools.
  3. Choose your existing User Pool.
  4. Make a note of the Pool Id.
  5. Next under App integration, select Domain name.
  6. Enter a unique domain prefix for your user pool, choose Check availability, once the address is verified as available, choose Save changes.
  7. Make a note of the complete domain name.

Next add Amazon Cognito as a service provider (SP) to your SAML Identity Provider (IdP), this blog uses the following steps for adding AWS SSO as the IdP, but the steps for adding a SP to your specific IdP may be different.

  1. Open the AWS SSO console.
  2. Choose Applications then choose Add a new application.
  3. Just below the dialog box for searching by name, choose Assertion Consumer Service
  4. Enter a Display name, and optionally a Description.
  5. Scroll down to the AWS SSO metadata section and copy down the AWS SSO SAML metadata file URL.
  6. Next scroll to the Application metadata section and choose If you don’t have a metadata file, you can manually type your metadata values.
  7. Enter your Amazon Cognito domain name with the path /saml2/idpresponse, i.e. https://<domain-prefix>.auth.<region>.amazoncognito.com/saml2/idpresponse in the Application ACS URL box.
  8. Enter you Amazon Cognito URN in the Application SAML Audience, i.e. urn:amazon:cognito:sp:<Cognito Pool Id>
  9. Choose Save changes
  10. On the next screen, choose the Attribute mappings tab
  11. Enter ${user:email} for Subject, with the format unspecified.
  12. Choose Add new attribute mapping and enter the following values.
  • http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress for User attribute in the application.
  • ${user:email} for Maps to this string value or user attribute in AWS SSO.
  • unspecified as the Format.
  1. Press Save changes.

Finally, return to the Amazon Cognito console to configure the SAML IdP for the user pool.

  1. Return to the Amazon Cognito console.
  2. Under Federation, choose Identity Providers, and then SAML.
  3. Enter the metadata URL, and a provider name.  For Identifiers enter the FQDN of the SAML users. i.e. the part of the user account after the @, but not including the @ symbol.
  4. Press Create provider.
  5. Next, under Federation, choose Attribute mapping, and then choose the SAML tab.
  6. Click Add SAML attribute, then enter SAML attribute http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress, and choose Email for User pool attribute, then choose Save changes
  7. Next choose App client settings under App integration and enable the newly created Identity Provider by ticking the box next to the name.
  8. Enter the Callback URL using the CloudFront URL with the path /signin.html, i.e. https://<cloudfront domain name>/signin.html.
  9. Choose Allowed OAuth Flow, Implicit grant, and Allowed OAuth Scopes email and openid.
  10. Press Save changes.

Step 3. Build an AWS Lambda backend for SAML forwarding

We will now build a Lambda function backend to determine if a user should be forwarded to a SAML IdP based on the user name entered on the sign-in page.  We will first need to create the IAM policy for the Lambda function.

  1. Open the IAM console.
  2. In the navigation pane, choose Policies.
  3. Choose Create policy.
  4. Choose the JSON tab.
  5. Copy and paste the following JSON policy into the policy document box.
  6. Replace <Region-Code> with the Region code that corresponds to the AWS Region where your Amazon Cognito User Pool exists. Replace <Account-Id> with your AWS Account ID. Finally, replace <Pool-Id> with the Pool ID noted earlier.
  7. When you finish, choose Review policy.
  8. Enter a Name.
  9. Choose Create policy.
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "cognito-idp:GetIdentityProviderByIdentifier",
                "cognito-idp:DescribeUserPoolClient"
            ],
            "Resource": [
                "arn:aws:cognito-idp:<Region-Code>:<ACCOUNT-NUMBER>:userpool/<Pool-Id>"
            ]
        },
        {
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
                ],
            "Resource": "*"
        }
    ]
}

Next create an IAM role for the Lambda function to assume, and associate it with the IAM policy just created.

  1. Open the IAM console.
  2. In the navigation pane, choose Roles.
  3. Choose Create Role.
  4. For Select type of trusted entity, keep AWS service selected.
  5. Choose Lambda, and then choose Next: Permissions.
  6. In the Filter policies search box, type name of the policy created in the previous step. When the policy appears in the list, choose the check box next to the policy name.
  7. Choose Next: Tags. Although you can specify a tag for the policy, a tag is not required.
  8. Choose Next: Review.
  9. Enter a Role name.
  10. Choose Create role.

Finally, create the Lambda function.

  1. Open the Lambda console.
  2. In the upper right corner of the Functions page, choose Create a function.
  3. On the Create function page, keep Author from scratch selected.
  4. Under Basic information, enter a name for the function, and choose Node.js 12.x
  5. Under Permissions, select the icon next to Choose or create an execution role. Then for Execution role, choose Use an existing role, and for Existing role, choose role created in the previous step.
  6. Choose Create function.
  7. In the Function code section, on the index.js tab, the placeholder code displays. Delete the placeholder code, and copy and paste the code below onto the tab.
  8. Replace the <CloudFront-Domain-Name> with your CloudFront domain name noted earlier.
  9. Choose Save in the upper right corner.
// Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

const AWS = require('aws-sdk');
const cognitoidentityserviceprovider = new AWS.CognitoIdentityServiceProvider;
const corsURL = '<CloudFront-Domain-Name>'; //This should be the domain of the website that originated the request, example: https://xyz123456.cloudfront.net

exports.handler = (event, context, callback)=> {
    var req = JSON.parse(event.body);
    console.log(req);
    var params = {
      IdpIdentifier: req.IdpIdentifier,
      UserPoolId: req.UserPoolId
    };
    cognitoidentityserviceprovider.getIdentityProviderByIdentifier(params, function(err, data) {
    if (err) errorResponse(context.awsRequestId, callback);
    else     passResponse(data, req.ClientId, context.awsRequestId, callback);
    });
};

function errorResponse(awsRequestId, callback) {
    callback(null, {
        statusCode: 500,
        body: JSON.stringify({
            Reference: awsRequestId,
        }),
        headers: {
            'Access-Control-Allow-Origin': corsURL,
        },
    });
}

function passResponse(data, ClientId, awsRequestId, callback) {
    var params = {
          ClientId: ClientId,
          UserPoolId: data.IdentityProvider.UserPoolId
        };
        cognitoidentityserviceprovider.describeUserPoolClient(params, function(err, dataresp) {
          if (err) errorResponse(awsRequestId, callback);
          else {
              if (dataresp.UserPoolClient.SupportedIdentityProviders.includes(data.IdentityProvider.ProviderName)) {
                callback(null, {
                    statusCode: 200,
                    body: JSON.stringify({
                        Reference: awsRequestId,
                    }),
                    headers: {
                        'Access-Control-Allow-Origin': corsURL,
                    },
                });
              } else {errorResponse(awsRequestId, callback)}
          }
        });
}

Step 4. Update the RESTful web service using Amazon API Gateway

In this step we will update the existing RESTful API to add an additional POST method to proxy requests to the Lambda function created in the previous step.

  1. Open the Amazon API Gateway console.
  2. Choose the existing API used for your SaaS portal.
  3. Choose Resources from the navigation, with the root path selected, choose Create Resource from the Actions menu.
  4. On the New Child Resource screen, enter the name saml as the Resource Name, and check the box for Enable API Gateway CORS, and click Create Resource.
  5. With your newly created resource (/saml) selected in the Resources pane, choose Actions, Create Method.
  6. Under OPTIONS, choose POST, and choose the check mark to the right of the list to save your changes.
  7. In the POST Setup pane, for Integration type, keep Lambda Function selected. Choose the Use Lambda Proxy integration check box. For Lambda Region, verify that the Region where you created your Lambda function is selected. Finally, for Lambda Function, type the name of the function that you created in the previous step.
  8. Choose Save.
  9. In the Add Permission to Lambda Function dialog box, choose OK to confirm your changes.
  10. Choose Actions, Deploy API, then choose the existing deployment stage from the drop down list and press Deploy.

Step 5. Update the existing AWS resources created during the SaaS workshop

To take advantage of the newly created CloudFront distribution and authentication method, we need to update the existing SaaS portal Lambda function. We will change the CORS domain variable and add username cleanup function.  Updating the CORS Access-Control-Allow-Origin is required since the site will now be reached via the CloudFront URL. Additionally, a function is needed to correct the format of the username value used in the API call Create Streaming URL.

  1. Open the Lambda console.
  2. Choose the existing function for creating streaming URLs for the SaaS portal.
  3. In the Function code section, on the index.js tab, update ‘Access-Control-Allow-Origin’ to equal the CloudFront URL. i.e. https://xyz123456.cloudfront.net
  4. Replace the text noted below in the code block.

Replace:
const username = event.requestContext.authorizer.claims['cognito:username'];
With:
var username
if (event.requestContext.authorizer.claims['cognito:username'].split('_')[1]) {
username = event.requestContext.authorizer.claims['cognito:username'].split('_')[1];
} else {username = event.requestContext.authorizer.claims['cognito:username']}

  1. Save the function and close the Lambda console.

Next update the static web content created for the SaaS portal. We will start with the file /assets/js/config.js, and append the newly created Cognito domain name.

  1. Open the Amazon S3 console.
  2. In the S3 buckets pane, in the search box, type the name of the Amazon S3 bucket that you created and where you copied the SaaS portal website files.
  3. When the bucket appears in the list, choose the bucket name.
  4. On the Overview tab, in the file list, navigate to assets/js/config.js.
  5. Choose the check box next to the config.js file, and choose Download.
  6. Navigate to the location where you downloaded this file on your local computer, and open the file.
  7. In the file, at the end of the invokeUrl line, but inside the curly brackets, add a comma, and a line return, then insert authUrl: ‘<Cognito Domain Name>’, replacing <Cognito Domain Name> with the Cognito Domain Name noted earlier, i.e. https://<unique-name>.auth.<region>.amazoncognito.com
  8. Save your changes and close the file.
  9. On the Amazon S3 console Overview tab, choose Upload.
  10. Drag your edited config.js file into the Upload window, and choose Upload.
  11. Wait for the upload to complete, and verify that the updated file appears in the list on the Overview tab.

Finally, we will need to update the file /assets/js/cognito-auth.js to add support for forwarding the user to the SAML provider and using the returned authorization token.

  1. Open the Amazon S3 console.
  2. In the S3 buckets pane, in the search box, type the name of the Amazon S3 bucket that you created and where you copied the SaaS portal website files.
  3. When the bucket appears in the list, choose the bucket name.
  4. On the Overview tab, in the file list, navigate to assets/js/cognito-auth.js.
  5. Choose the check box next to the config.js file, and choose Download.
  6. Navigate to the location where you downloaded this file on your local computer, and open the file.
  7. In the file, insert the code block below on a new line following the line $('#verifyForm').submit(handleVerify); within the function onDocReady.
$('#emailInputSignin').focusout(checkIDP);
if ($('#signinForm')) {
    var url = window.location.href
    try {
    var id_token = new URL(url).hash.split('&').filter(function(el) { if(el.match('id_token') !== null) return true; })[0].split('=')[1];
    $('#signinForm').html("");
    $( "header:first" ).html("Signing in");
    examplecorpURLGenCall(id_token);
    } catch {
        console.log("No auth token found")
    }
};
  1. Insert the following function to the end of the file, before the last line of the file, which reads: }(jQuery));
function checkIDP(){
        var idp = $('#emailInputSignin').val().split('@')[1]
        if (idp){
            var data = JSON.stringify({
                ClientId : _config.cognito.userPoolClientId,
                IdpIdentifier : idp,
                UserPoolId : _config.cognito.userPoolId
                })
            var request = new XMLHttpRequest()
            request.open('POST', _config.api.invokeUrl + '/saml', true)
            request.onload = function () {
                console.log(JSON.parse(this.status))
                var status = JSON.parse(this.status)
                if (status == "200"){
                    var authRedirect = _config.api.authUrl + '/oauth2/authorize?identity_provider=' + idp + '&redirect_uri=' + window.location.href + '&response_type=TOKEN&client_id=' + _config.cognito.userPoolClientId
                    window.location.href = authRedirect
                }
            }
            request.send(data)
            }
    }
  1. Save your changes and close the file.
  2. On the Amazon S3 console Overview tab, choose Upload.
  3. Drag your edited cognito-auth.js file into the Upload window, and choose Upload.
  4. Wait for the upload to complete, and verify that the updated file appears in the list on the Overview tab

Conclusion

You have successfully modified your existing SaaS portal to allow authentication from an SAML IdP. The SaaS portal will now inspect user names and determine if the domain name matches the identifier configured for the SAML IdP and redirect the user to sign in to the provider. Once authenticated to the SAML IdP, the user is directed back to the SaaS portal where the token is used as the authorizer to generate the streaming URL allowing the user to connect to the AppStream 2.0 session.

Clean up

To avoid incurring future charges, remove any unneeded resources that were created. Delete the CloudFront distribution, Lambda function, additional API Gateway resource, IAM Policy, and Role.