Node.js and AWS Cognito Part #1
User management is part of nearly every application and it is not easy to do it in a proper secure way. One of the possibilities is to use an existing solution such as AWS Cognito.
Nearly every project I worked on in the past has this part done inhouse. In these cases it is not unusual to see plain text passwords stored in the database. Or passwords hashed with MD5
hash which is deprecated. And even if everything was done correctly, there are still many other things that can pop up.
For example, a larger company tests your product and their security policy tells them to use only multi-factor authentication. Another company asks if they can set rules for passwords or setup single sign-on. How many hours would it take to implement it, test it and fix bugs?
Build your product and stop wasting time for things you can buy for fraction of the price you would spend on development.
Install required dependencies
yarn add @aws-sdk/client-cognito-identity-provider
Sign up new user
Use SignUpCommand to register a new user in an existing user pool. If email
was selected as user pool sign in option, every new user will get an email with code to confirm the registration and verify email address.
You can easily change the email template and use HTML
if you want. Just go to your user pool, then choose Messaging tab and on the bottom there are Message templates.
import {
SignUpCommand,
UsernameExistsException,
InvalidPasswordException,
} from '@aws-sdk/client-cognito-identity-provider';
class AWSUserController {
// ...
public async signUp(username: string, password: string, meta: Record<string, unknown>): Promise<User> {
const command = new SignUpCommand({
ClientId: CLIENT_ID,
SecretHash: this.getSecretHash(username),
Username: username,
Password: password,
UserAttributes: [{ Name: 'email', Value: username }],
});
try {
const response = await this.cognitoClient.send(command);
console.log('AWSUser.signUp response', response);
const user = {
id: response.UserSub,
username: username,
};
return user as User;
} catch (err) {
console.error(err);
switch (err.constructor) {
case UsernameExistsException:
throw new Error('Email address already exists!');
case InvalidPasswordException:
throw new Error('Password does not match the requirements!');
default:
throw new Error('Unable to create user!');
}
}
}
// ...
}
Confirm sign up
To verify email address and confirm the registration use ConfirmSignUpCommand. It takes two values - Username
and ConfirmationCode
(which was sent to the user by email).
User is not able to log in before this confirmation. Confirmation code is valid for 24 hours.
import {
ConfirmSignUpCommand,
} from '@aws-sdk/client-cognito-identity-provider';
class AWSUserController {
// ...
public async confirmSignUp(username: string, code: string): Promise<User> {
const command = new ConfirmSignUpCommand({
ClientId: CLIENT_ID,
SecretHash: this.getSecretHash(username),
Username: username,
ConfirmationCode: code,
});
const response = await this.cognitoClient.send(command);
console.log('AWSUser.confirmSignUp response', response);
return null;
}
// ...
}
Sign in existing user
When user sucessfuly confirmed email address, he is allowed to log in and get accessToken
along with refreshToken
. Use InitiateAuthCommand to start the process. It is called "initiate" because the user may have two factor authentication enabled and in that case the sign in process will take more steps.
If the user does not have two factor authentication enabled, then the response contains accessToken
, refreshToken
and idToken
. It is considered a good practice or recommended to store refreshToken
in HttoOnly
cookie and accessToken
in memory.
import {
InitiateAuthCommand,
NotAuthorizedException,
UserNotConfirmedException,
} from '@aws-sdk/client-cognito-identity-provider';
class AWSUserController {
// ...
public async signIn(username: string, password: string): Promise<{ user: User, refreshToken: string, accessToken: string }> {
const command = new InitiateAuthCommand({
AuthFlow: 'USER_PASSWORD_AUTH',
ClientId: CLIENT_ID,
AuthParameters: {
USERNAME: username,
PASSWORD: password,
SECRET_HASH: this.getSecretHash(username),
},
});
try {
const response = await this.cognitoClient.send(command);
console.log('AWSUser.signIn response', response);
const data = this.serviceToken.decode(response.AuthenticationResult.IdToken) as any;
const user = {
id: data.payload.sub,
email: data.payload.email,
}
return {
user: user as unknown as User,
accessToken: response.AuthenticationResult.AccessToken,
refreshToken: response.AuthenticationResult.RefreshToken,
};
} catch (err) {
console.error(err);
switch (err.constructor) {
case NotAuthorizedException:
throw new Error('Wrong email or password!');
case UserNotConfirmedException:
throw new Base('User is not verified!');
default
throw new Error('Unknown error!');
}
}
}
// ...
}
Next steps
Now the user can register, confirm registration and log into the application. In the second part there will be middleware to check the access token, get the current user detail and sign out.
You can find the whole project on GitHub, or you can use this User Service available as Docker container in Docker Hub.
Do you like this post? Is it helpful? I am always learning and trying new technologies, processes and approaches. When I struggle with something and finally manage to solve it, I share my experience. If you want to support me, please use button below. If you have any questions or comments, please reach me via email juffalow@juffalow.com.
I am also available as a mentor if you need help with your architecture, engineering team or if you are looking for an experienced person to validate your thoughts.