AWS Developer Tools Blog
Building a serverless developer authentication API in Java using AWS Lambda, Amazon DynamoDB, and Amazon Cognito – Part 1
Most of us are aware of the support for a developer authentication backend in Amazon Cognito and how one can use a custom backend service to authenticate and authorize users to access AWS resources using temporary credentials. In this blog, we will create a quick serverless backend authentication API written in Java and deployed on Lambda. You can mirror this workflow in your current backend authentication service, or you can use this service as it is.
The blog will cover the following topics in a four-part series.
- Part 1: How to get started with Java development on Lambda using the AWS Toolkit for Eclipse.
- Part 1: How to use Java Lambda functions for custom events.
- Part 2: How to create a simple authentication microservice that checks users against an Amazon DynamoDB table.
- Part 2: How to integrate with the Amazon Cognito Identity Broker to get an OpenID token.
- Part 3: How to locally test your Java Lambda functions through JUnit before uploading to Lambda.
- Part 4: How to hook up your Lambda function to Amazon API Gateway.
The Lambda workflow support in the latest version of the AWS Toolkit for Eclipse makes it really simple to create Java functions for Lambda. If you haven’t already downloaded Eclipse, you can get it here. We assume you have an AWS account with at least one IAM user with an Administrator role (that is, the user should belong to an IAM group with administrative permissions).
Important: We strongly recommend you do not use your root account credentials to create this microservice.
After you have downloaded Eclipse and set up your AWS account and IAM user, install the AWS Toolkit for Eclipse. When prompted, restart Eclipse.
We will now create an AWS Lambda project. In the Eclipse toolbar, click the yellow AWS icon, and choose New AWS Lambda Java Project.
On the wizard page, for Project name, type AuthenticateUser. For Package Name, type aws.java.lambda.demo (or any package name you want). For Class Name, type AuthenticateUser. For Input Type, choose Custom Object. If you would like to try other predefined events that Lambda supports in Java, such as an S3Event or DynamoDBEvent, see these samples in our documentation here. For Output Type, choose a custom object, which we will define in the code later. The output type should be a Java class, not a primitive type such an int or float.
Choose Finish.
In Package Explorer, you will now see a Readme file in the project structure. You can close the Readme file for now. The structure below shows the main class, AuthenticateUser, which is your Lambda handler class. It’s where you will be implementing the handleRequest function. Later on, we will implement the unit tests in JUnit by modifying the AuthenticateUserTest class to allow local testing of your Lambda function before uploading.
Make sure you have added the AWS SDK for Java Library in your build path for the project. Before we implement the handleRequest function, let’s create a Data class for the User object that will hold our user data stored in a DynamoDB table called User. You will also need to create a DynamoDB table called User with some test data in it. To create a DynamoDB table, follow the tutorial here. We will choose the username attribute as the hash key. We do not need to create any indexes for this table. Create a new User class in the package aws.java.lambda.demo and then copy and paste the following code:
Note: For this exercise, we will create all our resources in the us-east-1 region. This region, along with the ap-northeast-1 (Tokyo) and eu-west-1 (Ireland) regions, supports Amazon Cognito, AWS Lambda, and API Gateway.
package aws.dhruv.lambda.services;
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBAttribute;
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBHashKey;
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBTable;
@DynamoDBTable(tableName="User")
public class User {
private String userName;
private Integer userId;
private String passwordHash;
private String openIdToken;
@DynamoDBHashKey(attributeName="username")
public String getUserName() { return userName; }
public void setUserName(String userName) { this.userName = userName; }
@DynamoDBAttribute(attributeName="userid")
public Integer getUserId() { return userId; }
public void setUserId(Integer userId) { this.userId = userId; }
@DynamoDBAttribute(attributeName="passwordhash")
public String getPasswordHash() { return passwordHash; }
public void setPasswordHash(String passwordHash) { this.passwordHash = passwordHash; }
@DynamoDBAttribute(attributeName="openidtoken")
public String getOpenIdToken() { return openIdToken; }
public void setOpenIdToken(String openIdToken) { this.openIdToken = openIdToken; }
public User(String userName, Integer userId, String passwordHash, String openIdToken) {
this.userName = userName;
this.userId = userId;
this.passwordHash = passwordHash;
this.openIdToken = openIdToken;
}
public User(){ }
}
You will see we are leveraging annotations so we can use the advanced features provided by the DynamoDB Mapper. The AWS SDK for Java provides DynamoDBMapper, a high-level interface that automates the process of getting your objects into Amazon DynamoDB and back out again. For more information about annotating your Java classes for use in DynamoDB, see the developer guide here.
Our Java function will ingest a custom object from API Gateway and, after execution, return a custom response object. Our custom input is a JSON POST body that will be invoked through an API Gateway endpoint. A sample request will look like the following:
{ "userName": "Dhruv", "passwordHash": "8743b52063cd84097a65d1633f5c74f5" }
The data is passed in as a LinkedHashMap of key-value pairs to your handleRequest function. As you will see later, you will need to cast your input properly to extract the values of the POST body. Your custom response object looks like the following:
{ "userId": "123", "status": "true", "openIdToken": "eyJraWQiOiJ1cy1lYXN0LTExIiwidHlwIjoiSldTIiwiYWxnIjoiUl" }
We need to create an implementation of the Response class in our AuthenticateUser class as follows.
public static class AuthenticateUserResponse{
protected Integer userId;
protected String openIdToken;
protected String status;
public Integer getUserId() { return userId; }
public void setUserId(Integer userId) { this.userId = userId; }
public String getOpenIdToken() { return openIdToken; }
public void setOpenIdToken(String openIdToken) { this.openIdToken = openIdToken; }
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
}
Now that we have the structure in place to handle a custom event, in part 2 of this blog post, we will finish the implementation of the handleRequest function that will do user validation and interact with Amazon Cognito.