A primer on the Amazon  Cognito Node SDK
Tags: aws, node, tutorial
Published: 2020-09-06 21:25:38

Amazon Cognito is an authentication service provided by AWS. It is commonly used with AWS Amplify to provide authentication for applications, and Amazon provides plenty of documentation for that purpose. 


However, resources for using the Cognito SDK directly are more scarce. It's my hope that this article can save you the various Stack Overflow answers, AWS documentation articles, and pure trial and error it it took me to get a functional authentication system using this service.


Included at the end of the article is a Github repo for a bare-bones Express app demonstrating some of the Cognito methods. The line "for the purposes of this tutorial" indicates a step I took while creating the User Pool for that application.

# Setting up Cognito
1. Navigate to the Cognito service in the AWS console
2. Click "Manage user pools", then "Create a user pool"

	### Create your User Pool
3. Enter your pool name and click "step through settings"

	### Attributes
	![Image depicting the Attributes configuration page in Cognito](https://dev-to-uploads.s3.amazonaws.com/i/tq73kow2zuqp6r8jncdw.png)
4. Choose how you want your uses to be able to sign in 
    - For the purposes of this tutorial I'll be going with email only
5. Select any required attributes you wish each user to have

	### Policies
	![Image depicting the Policies configuration page in Cognito](https://dev-to-uploads.s3.amazonaws.com/i/omq929rfrrlairpefhej.png)
6. Choose your password requirements 
7. Choose if users can sign themselves up, or if admins need to register users

	### MFA and verifications
	![Image depicting the MFA and verifications configuration page in Cognito](https://dev-to-uploads.s3.amazonaws.com/i/pbnjdmgd75b98f1m7s59.png)
8. Choose if you want to enable Multi Factor Authentication 
	– MFA adds a level of complexity that's out of scope for this tutorial; may be the topic of a future article however.
9. Choose how you want users to be able to recover their accounts
	- For the purposes of this tutorial I'll be going with "email only"

	### Message Customization
	![Image depicting the Message configuration page in Cognito](https://dev-to-uploads.s3.amazonaws.com/i/rnpjuwfq8x4cp63u7i53.png)
10. Message Customization: here you can customize the email messages that get sent out to users when they sign up for your application
	- If you don't setup AWS SES and instead use Cognito to send emails, you're limited to 50 emails per day. This isn't good enough for production usage, but for personal/side projects it should fit your needs
11. Email verification – Code vs Link
	- Link based verification: user is emailed a link, they click the link, user is verified to login.
	- Code based verification: user is emailed a code, your application uses the "confirmSignUp" method to verify code, user is verified to login.

	### Tags
12. Add any necessary resource tags

	### Devices
13. Choose if you want to remember user devices – this is related to MFA.
    - Don't click "Always" unless you're using MFA! Doing so changes the required parameters for certain Cognito methods.

	### App clients
	![Image depicting the App client page in Cognito](https://dev-to-uploads.s3.amazonaws.com/i/qg9wukeudu86qnz3r3cr.png)
14. Click "add an app client" – fill in name and token refresh values
15. Make sure "Generate Client Secret" is checked
16. Under Auth Flows, make sure "ALLOW_ADMIN_USER_PASSWORD_AUTH" is checked.

	### Triggers
17. Assign any necessary lambda functions to certain triggers. 

	### Review
18. Make sure you've filled in all the fields mentioned in the previous steps
	- Pay special attention to Attributes, they can't be changed after Pool Creation!
19. Click "Create"

# Using Cognito in your application
### NPM Packages you'll need
- [aws-sdk](https://www.npmjs.com/package/aws-sdk)
- [jwk-to-pem](https://www.npmjs.com/package/jwk-to-pem)
- [jsonwebtoken](https://www.npmjs.com/package/jsonwebtoken)

### Environment Variables
- AWS_SECRET_ACCESS_KEY: grab this from the security credentials page of your AWS account (alternatively, you can create an [IAM User](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_users_create.html#id_users_create_console) and use its secret hash)
- AWS_ACCESS_KEY_ID: grab this from the security credentials page of your AWS account (alternatively, you can create an [IAM User](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_users_create.html#id_users_create_console) and use its access key)
- AWS_REGION: the region your user pool is in e.g. us-east-1.
- AWS_CLIENT_ID: grab this from your Cognito console. Can be found under General Settings → App clients or App Integration → App client settings.
- AWS_COGNITO_SECRET_HASH: grab this from your Cognito console. Found under General Settings → App clients. Click the show details button on your app client to show the field.
- AWS_USER_POOL_ID: grab this from your Cognito console. Found under General Settings.
- SERVER_NAME: The name you entered for your Cognito server.

### Important helper functions
```javascript 
//  Authentication flows require the value returned by this function

import {createHmac} from 'crypto';
const {AWS_COGNITO_SECRET_HASH, AWS_CLIENT_ID} from './environment';

function createSecretHash(username){
	return createHmac('sha256', AWS_COGNITO_SECRET_HASH)
            .update(username + AWS_CLIENT_ID).digest('base64');
}
```

```javascript
// Authentication flows require request headers to be formatted as an
// array of objects with the shape {headerName: string, headerValue: string}

// this tutorial assumes you're using express and formats the headers 
// according to that assumption

function formatHeaders(headers){
	let formattedHeaders = [ ];
	for(const headerName in headers){
		formattedHeaders.push({
            headerName, 
            headerValue:headers[headerName]
        });
	}
	return formattedHeaders;
}
```



### Validating the JWT signature
1. Create a jwks.json file at the root of your app
	- the contents of the file should be pulled from a url with the following structure: 
	`https://cognito-idp.{YOUR_AWS_REGION}.amazonaws.com/{YOUR_AWS_USER_POOL_ID}/.well-known/jwks.json`
	- alternatively, you can make a GET request from your app to the above URL and use the request results
2. Use the following function whenever you need to verify a JWT signature
```javascript 
const jsonwebtoken = require('jsonwebtoken');
const jwkToPem = require('jwkToPem');
const jwks = require('./jwks.json');

function verifyTokenSignature(token){
	// alternatively you can use jsonwebtoken.decode() 
	const tokenHeader = JSON.parse(
		Buffer.from(token.split('.')[0],  'base64').toString()
	);
	const properJwk = jwks.find(jwk => jwk.kid === tokenHeader.kid);
	const pem = jwkToPem(properJwk);
	return new Promise((resolve, reject) => {
		jsonwebtoken.verify(
            token, 
            pem, 
            {algorithms: ['RS256']}, 
            (err, decodedToken) => {
			    err ? reject(false): resolve(true);
		    }
        )
	});
}
```
One important thing to note: there are still steps you should take after verifying a JWT signature (read more [here](https://www.pingidentity.com/en/company/blog/posts/2019/jwt-security-nobody-talks-about.html) and [here](https://github.com/awslabs/aws-support-tools/tree/master/Cognito/decode-verify-jwt)).



### Example usage
```javascript 
const cognito = new CognitoIdentityServiceProvider({
	secretAccessKey:'YOUR_SECRET_ACCESS_KEY',
	accessKeyId:'YOUR_ACCESS_KEY_ID',
	region:'YOUR_COGNITO_POOL_REGION'
});

function register(Username, Password){
	const params = {
		ClientId: 'YOUR_AWS_CLIENT_ID',
		Username,
		Password,
		SecretHash: createSecretHash(username)
	}
	return cognito.signUp(params).promise()
}
```


### Things to watch out for
When the register method is called and a user already exists with the given username, Cognito returns the message 'An account with the given email already exists.' This gives bad actors the ability to mount a user enumeration action against your app ([read more](https://blog.rapid7.com/2017/06/15/about-user-enumeration/)). One possible solution is to check for this specific message whenever you're handling errors, and return the default success message for registration instead of an error.


When you're calling the refresh token flow of the adminInitiateAuth method, use the username field from the user's access token (looks like a random string of characters) instead of their email; otherwise you'll get a 'Failure to verify secret hash' message.


### Conclusion
Once you get the pool setup and some basic helper functions written out, using Cognito becomes as simple as passing in the correct parameters to the necessary function. Securing your server doesn't end with setting up Cognito; there are plenty of other important steps to take. OWASP's cheat sheets are a great place to learn more about securing your application. 


## Reference Links
- [AWS SDK documentation for Cognito](https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/CognitoIdentityServiceProvider.html) 
- [Cognito Sampler Repository](https://github.com/blake-buck/cognito-sampler)

## Further Reading
- [OWASP Cheat Sheet on NodeJS Security](https://cheatsheetseries.owasp.org/cheatsheets/Nodejs_Security_Cheat_Sheet.html)
- [OWASP Cheat Sheet on Authentication](https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html)