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
- Navigate to the Cognito service in the AWS console
-
Click "Manage user pools", then "Create a user pool"
Create your User Pool
-
Enter your pool name and click "step through settings"
Attributes
-
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
-
Select any required attributes you wish each user to have
Policies
Choose your password requirements
-
Choose if users can sign themselves up, or if admins need to register users
MFA and verifications
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.-
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
-
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
-
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
-
Add any necessary resource tags
Devices
-
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
Click "add an app client" – fill in name and token refresh values
Make sure "Generate Client Secret" is checked
-
Under Auth Flows, make sure "ALLOW_ADMIN_USER_PASSWORD_AUTH" is checked.
Triggers
-
Assign any necessary lambda functions to certain triggers.
Review
-
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!
Click "Create"
Using Cognito in your application
NPM Packages you'll need
Environment Variables
- AWS_SECRET_ACCESS_KEY: grab this from the security credentials page of your AWS account (alternatively, you can create an IAM User 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 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
// 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');
}
// 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
- 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
- the contents of the file should be pulled from a url with the following structure:
- Use the following function whenever you need to verify a JWT signature
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 and here).
Example usage
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). 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.
Top comments (0)