home / 2017.03.30 16:25 /aws /api gateway /cognito /sts /federated identities

Integrating Cognito federated identities and a custom authentication service with secured services exposed through the API Gateway

This document explores how we can use federated Cognito identities authenticated through our own custom service to access secured APIs exposed through API Gateway. The scenario we are considering is creating temporary users that we can identify through Cognito, then obtain some credentials for those temporary users to access the a secure service we have exposed through API Gateway. Going through our custom authorization service means we can use fields like family name and date of birth to identify users, and relying on Cognito and API Gateway to handle the authentication flow will make the whole solution more secure that if we were to roll out our custom implementation.

We need to go thorough the following steps to implement this:

Setting up federated identities in Amazon Cognito

Go to the Amazon Cognito console and select manage federated identities. You will need to create a new user pool for this (I named it tempusers). Give a name to your new pool and don’t check the enable access to unauthenticated identities. Under authentication providers click on the custom tab and set a name for your developer provider. Save your changes, create the pool and Cognito is ready for business.

The federated pool will also create two roles, one for authenticated and another for nonauthenticated users. You will need to go to the IAM console and give the authenticated user role permissions to run API Gateway if we are to use credentials for this role to access secured services exposed through API Gateway.

Creating a custom web authentication service

I created a very simple Java Lambda to work as an authentication service:

public class AuthenticationLambda implements RequestHandler<Request, Response> {

    private static final String POOL_ID = "federated pool id";
    public static final long TOKEN_DURATION = 60 * 15l;
    private final AmazonCognitoIdentity identityClient;
    public static final String DEVELOPER_PROVIDER = "provider name used when creating the federated pool";

    public AuthenticationLambda() {
        identityClient = AmazonCognitoIdentityAsyncClientBuilder.defaultClient();
    }
}
public class AuthenticationLambda implements RequestHandler<Request, Response> {

    // constants and constructor

    public Response handleRequest(Request request, Context context) {
        String username = authenticateUser(request.getFamilyName(), request.getDateOfBirth());
        return getToken(username);
    }
}
public class AuthenticationLambda implements RequestHandler<Request, Response> {

    // constants and constructor

    // handler

    private String authenticateUser(String familyName, String dateOfBirth) {
        // this method will look in some database and check if the values correspond to a user,
        // the return that user's id/username
        if ("Popescu".equals(familyName) && "19800101".equals(dateOfBirth)) {
            return "PopescuD";
        } else {
            return null;
        }
    }
}
public class AuthenticationLambda implements RequestHandler<Request, Response> {

    // constants and constructor

    // handler

    // authenticate user

    private Response getToken(String username) {
        if (username != null) {
            GetOpenIdTokenForDeveloperIdentityRequest request =
                    new GetOpenIdTokenForDeveloperIdentityRequest();
            request.setIdentityPoolId(POOL_ID);

            HashMap<String,String> logins = new HashMap<>();
            logins.put(DEVELOPER_PROVIDER, username);
            request.setLogins(logins);
            request.setTokenDuration(TOKEN_DURATION);
            GetOpenIdTokenForDeveloperIdentityResult response = identityClient.getOpenIdTokenForDeveloperIdentity(request);

            Response lambdaResponse = new Response();
            lambdaResponse.setIdentityId(response.getIdentityId());
            lambdaResponse.setToken(response.getToken());
            return lambdaResponse;
        } else {
            return new Response();
        }
    }
}

This is our whole authorization service. The Request and Response classes are simple Java beans. We still need to package the Lambda and upload it to Amazon, and we also have to expose this service as an API through Amazon API Gateway. This API does not require authentication.

Create an API secured through IAM credentials

We also need an API that is secured through AWS_IAM. We can use another Lambda that returns some data (a JSON) and expose it through Amazon API Gateway. Once that is done, we need to go to our method configuration in API Gateway, click the method request section and under authorization settings select AWS_IAM for the authorization field. We can now deploy the API to staging (both the secured API and the authorization service need to be deployed).

Logging in with our custom authentication service

I created a JavaScript file to handle the whole authentication service. This file can be easily adapted in a UI that takes user credentials, then accesses the secured service for the required data to build the application UI.

We start out with the following required libraries and constants:

var https = require('https');
var collecter = require('./collecter.js').jsonCollecter;

var ROLE_ARN = "federated pool authenticated role";
var REGION = 'region'
var SERVICE = 'execute-api'

Next step is to call our authentication API and obtain the token:

function loginToCustomAuthService(familyName, dateOfBirth, callback) {
    var options = {
        method: 'POST',
        host: 'api staging url',
        path: '/cognitotest/auth'
    }
    var body = {
        familyName: familyName,
        dateOfBirth: dateOfBirth
    }
    var request = https.request(options, collecter(callback));
    request.write(JSON.stringify(body));
    request.end();
}

var familyName = "Popescu";
var dateOfBirth = "19800101";
loginToCustomAuthService(familyName, dateOfBirth, console.log);

Running this program should print the token response to the console:

{ identityId: '****',
  token: '****' }

Collecter library

var bl = require('bl');

exports.collecter = function(callback) {
    return function(result) {
        function onceCollected(err, data) {
            var string = data.toString();
            callback(string);
        }
        result.setEncoding('utf8');
        result.pipe(bl(onceCollected));
        result.on('error', console.error);
    }
}

exports.jsonCollecter = function(callback) {
    return function(result) {
        function onceCollected(err, data) {
            var string = data.toString();
            callback(JSON.parse(string));
        }
        result.setEncoding('utf8');
        result.pipe(bl(onceCollected));
        result.on('error', console.error);
    }
}

Obtaining credentials from Amazon STS

Now that we have the token, we can use it to obtain credentials from the Security Token Service. For this, we first need to add some new dependencies, the AWS SDK for JavaScript from which we instantiate the STS object:

var AwsSdk = require('aws-sdk')
var STS = new AwsSdk.STS();

Then we can use the STS object to securely obtain credentials from Amazon:

function obtainCredentials(roleArn, token, callback) {
    STS.assumeRoleWithWebIdentity({
        RoleArn: roleArn,
        RoleSessionName: 'someRoleSessionName',
        WebIdentityToken: token
    }, function(error, response) {
        callback(response.Credentials);
    })
}

var familyName = "Popescu";
var dateOfBirth = "19800101";

loginToCustomAuthService(familyName, dateOfBirth, function(tokenResponse) {
    obtainCredentials(ROLE_ARN, tokenResponse.token, console.log);
});

Running the program now should print something like the following output on the console:

{ AccessKeyId: '****',
  SecretAccessKey: '****',
  SessionToken: '****',
  Expiration: 2017-03-30T14:14:12.000Z }

Accessing a secured API with a signed request

We now have the access key id, the secret access key and the session token, all necessary to sign a request made to a secured API. We need to include the library that will do the signing, the implementation of which is described in a different document:

var signature = require('./signature.js');

And the final implementation of our client is:

function loadDataFromSecuredService(accessKeyId, secretKey, sessionToken, callback) {
    var options = {
            method: 'GET',
            host: 'staging api url',
            path: '/cognitotest/secured_api_path'
    };
    // build the signature, body is empty
    signature.signRequest(options, '', accessKeyId, secretKey, REGION, SERVICE, sessionToken);
    https.request(options, collecter(callback)).end();
}

var familyName = "Popescu";
var dateOfBirth = "19800101";

loginToCustomAuthService(familyName, dateOfBirth, function(tokenResponse) {
    obtainCredentials(ROLE_ARN, tokenResponse.token, function(credentials) {
        loadDataFromSecuredService(credentials.AccessKeyId, credentials.SecretAccessKey, credentials.SessionToken, console.log);
    });
});

Executing the program now should print the data provided by your secure service, in my example this data is a JSON object:

{ fields:
   [ { name: 'family name', value: '' },
     { name: 'first name', value: '' },
     { name: 'address', value: '' },
     { name: 'postcode', value: '' } ] }

This is the while process of using a custom authorizer service that creates identities in a federated Cognito pool, then using those identities to obtain credentials from Amazon STS and finally using those credentials to access secured APIs exposed through Amazon API Gateway.

References