Authentication and Authorization to Amazon Cognito With Lambdas
Take a look at this tutorial on this difficult-to-find topic.
Join the DZone community and get the full member experience.
Join For FreeAuthentication
In our project, we were using Amazon Cognito for authentication, authorization and user management. It’s very easy to use, basically, you just need to create a user pool, identity pool, and users (everything you can “click” from AWS console).
I will not go into the details, you can read how to do this step by step from official AWS docs.
To authenticate from a web application you simply need to use this code:
var authenticationData = {
Username : 'username',
Password : 'password',
};
var authenticationDetails = new AmazonCognitoIdentity.AuthenticationDetails(authenticationData);
var poolData = { UserPoolId : 'us-east-1_TcoKGbf7n',
ClientId : '4pe2usejqcdmhi0a25jp4b5sh3'
};
var userPool = new AmazonCognitoIdentity.CognitoUserPool(poolData);
var userData = {
Username : 'username',
Pool : userPool
};
var cognitoUser = new AmazonCognitoIdentity.CognitoUser(userData);
cognitoUser.authenticateUser(authenticationDetails, {
onSuccess: function (result) {
var accessToken = result.getAccessToken().getJwtToken();
/* Use the idToken for Logins Map when Federating User Pools with identity pools or when passing through an Authorization Header to an API Gateway Authorizer*/
var idToken = result.idToken.jwtToken;
},
onFailure: function(err) {
alert(err);
},
});
As you can see besides providing a username and password, we also need to create a user pool object, which requires pool ID and client ID.
Everything is straightforward; however, in our case, we had to authenticate to different user pools. Our use case also provides us a pool name but we don't know, between the pool ID and client ID, which one needs to be provided together with username and password. But how do we get them? One of the options would be to provide a hardcoded list of both IDs, but in such case, we would need to do a redeployment of the UI. There is also no JavaScript API method to get the user pool by its name.
However, AWS has Java Cognito SDK which supports all kinds of operations on user pools and users. So why not to try move authentication to lambda?
We created a simple lambda which get 3 parameters (username, password, pool name). Let’s take a look at Cognito API SDK. It is coming from:
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-cognitoidp</artifactId>
<version>1.11.269</version>
</dependency>
The interface which we are interested in is called AWSCognitoIdentityProvider. To create default implementation, type:
AWSCognitoIdentityProvider cognito = AWSCognitoIdentityProviderClientBuilder.defaultClient();
To be able to invoke each API method you need to give your lambda the proper roles and permissions. Each API method is under a different action. You can define them in your SAM or configure it directly from AWS console.
PolicyDocument:
Statement:
-
Effect: "Allow"
Action: [
"cognito-idp:AdminInitiateAuth",
"cognito-idp:DescribeUserPool",
"cognito-idp:DescribeUserPoolClient",
"cognito-idp:ListUserPoolClients",
"cognito-idp:ListUserPools"
]
Now let’s look what this interface provides us. Unfortunately, we cannot get the user pool object by its unique name, only by its ID. But we can get all the user pools with their names so we can find ours and get all necessary data.
List<UserPoolDescriptionType> userPools =
cognito.listUserPools(new ListUserPoolsRequest().withMaxResults(20)).getUserPools();
UserPoolDescriptionType
has a name, which we compare with our name and ID. With the ID, we can browse for the user pool object which will contain everything which we need for authentication.
ListUserPoolClientsResult response =
cognito.listUserPoolClients(
new ListUserPoolClientsRequest()
.withUserPoolId(userPoolId)
.withMaxResults(1)
);
UserPoolClientType userPool =
cognito.describeUserPoolClient(
new DescribeUserPoolClientRequest()
.withUserPoolId(userPoolId)
.withClientId(
response.getUserPoolClients().get(0).getClientId()
)
).getUserPoolClient();
We can now authenticate the user. Since we are doing it on the server side, we can use a Non-SRP authentication flow and pass the username and password directly.
try {
Map<String, String> authParams = new HashMap<>(2);
authParams.put("USERNAME", loginRequest.getUsername());
authParams.put("PASSWORD", loginRequest.getPassword());
AdminInitiateAuthRequest authRequest =
new AdminInitiateAuthRequest()
.withClientId(userPool.getClientId())
.withUserPoolId(userPool.getUserPoolId())
.withAuthFlow(AuthFlowType.ADMIN_NO_SRP_AUTH)
.withAuthParameters(authParams);
AdminInitiateAuthResult result =
client.adminInitiateAuth(authRequest);
AuthenticationResultType auth = result.getAuthenticationResult();
} catch (final UserNotFoundException
| NotAuthorizedException exception) {
exception.printStackTrace();
}
AuthenticationResultType
contains accessToken
, idToken
and refreshToken
so everything what our web client app needs.
Authorization
We also need to change authorization. Our existing implementation of authorization is based on the Cognito default mechanism. Example configuration:
securityDefinitions:
CognitoUserPool:
type: "apiKey"
name: "Authorization"
in: "header"
x-amazon-apigateway-authtype: "cognito_user_pools"
x-amazon-apigateway-authorizer:
type: "cognito_user_pools"
providerARNs:
- !Ref UserPoolARN
Provider:
UserPoolARN:
Type: String
Default: 'arn:aws:cognito-idp:us-east-1:782624688943:userpool/us-east-xxx'
As you see here this security definition is connected to a concrete user pool which, in our case, will not work because of authenticating to multiple user pools. So here we need to write a lambda, but this time for authorization.
The authorization lambda is getting two parameters:
-
authorizationToken
which is our JWTaccessToken
which is passed in header from our UI client -
methodArn
it’s Amazon Resource Name of full method which is needed for returning auth policy
The first step is to verify the JWT against public keys which are separate for each user pool. They are here:
https://cognito-idp.us-east-1.amazonaws.com/%s/.well-known/jwks.json
Where "%s" is, the user pool ID should be, which you can take from processing accessToken
(part of issuer). I will not write here details on how to verify the suck key I used jose4j. For this, you can check examples (using the https jwks endpoint).
After verifying jwt we need a return policy which will tell AWS to allow or deny the request. This is a little bit tricky because the authorization policy needs to have concrete fields, but this is very well-described by Jack Kohn in AWS Labs.
In our case, principalId
is a JWT subject.
Now we only need to configure our lambda in SAM and we are done:
securityDefinitions:
AuthorizationLambda:
type: "apiKey"
name: "Authorization"
in: "header"
x-amazon-apigateway-authtype: "custom"
x-amazon-apigateway-authorizer:
authorizerCredentials:
Fn::GetAtt: [ RestApiAuthorizerRole, Arn ]
authorizerUri:
Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${AuthorizationFunction.Arn}/invocations
authorizerResultTtlInSeconds: 0
type: "token"
During the implementation of authentication and authorization via Lambdas, it wasn’t easy to find something about this topic. It’s because our use case was not typical and now when you write a Javascript client you will simply use the js cognito API to do this. I hope this short article/tutorial will be helpful.
Opinions expressed by DZone contributors are their own.
Trending
-
DevOps in Legacy Systems
-
Alpha Testing Tutorial: A Comprehensive Guide With Best Practices
-
SeaweedFS vs. JuiceFS Design and Features
-
How To Ensure Fast JOIN Queries for Self-Service Business Intelligence
Comments