Authentication using Amazon Cognito and Node.
jsJanitha TennakoonBlockedUnblockFollowFollowingJun 15What is Amazon CognitoAmazon Cognito provides authentication, authorization, and user management for your web and mobile apps.
This service was earlier used for mobile applications but now used for a variety of web applications as well.
It does the same functionality as many other popular authentication frameworks like Auth0, Identity server, and JWT web tokens.
But Cognito saves and synchronizes end-user data that enables an app developer to concentrate on writing code instead of managing the back-end.
Before going more further let’s clarify two main concepts in AWS Cognito.
User pools vs Identity poolsBelow are the definitions given by Amazon on both user pool and identity pool.
User pool Amazon Cognito User Pool makes it easy for developers to add sign-up and sign-in functionality to web and mobile applications.
It serves as your own identity provider to maintain a user directory.
It supports user registration and sign-in, as well as provisioning identity tokens for signed-in users.
Identity pool Amazon Cognito Federated Identities enables developers to create unique identities for your users and authenticate them with federated identity providers.
With a federated identity, you can obtain temporary, limited-privilege AWS credentials to securely access other AWS services such as Amazon DynamoDB, Amazon S3, and Amazon API Gateway.
According to the above definitions, we can identify that user registration, authentication, and account recovery are done by user pools whereas the identity pool authorizes your users to use the various AWS services.
Say you wanted to allow a user to have access to your S3 bucket so that they could upload a file; you could specify that while creating an Identity Pool.
In this tutorial, we are only going to need User pools.
Ok, now since we clarified what we need let’s jump into implementation part.
This tutorial will consist of two parts.
The first part will be creating our user pool on Cognito and the second part will be creating our server to use Cognito services.
Creating Cognito User PoolWhen you go to AWS services and to Cognito you will be greeted by the above page.
You can either Manage User Pools or either Manage Identity Pools.
Since we are going to create a user pool click on Manage User Pools and Create new User Pool.
Below shows the page you are directed after clicking on create a user pool.
Here you are given two options to select from, either use default settings for the user pool or customize settings.
Most of these default settings fit in most development scenarios, but for this tutorial lets select Step through settings.
Make sure to provide a pool name as well.
Next, you can select what is needed from the user when signing in signing up.
By default it selects Username, but we can optionally provide either email or phone number as well.
In the second section displays the attributes which will be needed in the signup process.
Next, we are directed to password customization page.
In the second section, you can give users to sign in themselves rather than only administrators doing it.
In the next page, we can configure whether we need users to use Multi-Factor Authentication(MFA).
MFA adds a secondary layer of security where users will be verified using multiple credentials rather than using only username and password.
MFA requires other — additional — credentials, such as a code from the user’s smartphone, the answer to a security question, a fingerprint, or facial recognition.
Next, we can customize how our invitation email can be sent to the users.
Here you can give the option to save user’s devices so the user will be always logged in or not.
Next, we need to create an app client.
App client is the client which our Nodejs server will be communicating with.
Make sure to tick Enable sign-in API for server-based authentication in order for our Nodejs server access Cognito user pool for authentication.
App clients can be created after the generation of user pools as well.
So if you forgot to add an app client when the creation of user pool do not worry, you can create a new one again.
In this step, we can assign lambda functions to events that Cognito will trigger.
For example, in Pre sign-up event we can write a lambda function to format the input before sending them to Cognito API.
In the last step, you can review all the settings that you have customized.
Make sure to go through settings before hitting Create pool.
Now you have successfully created a user pool and an app client which is connected to your user pool.
In General settings you can see now there is a Pool id created for your user pool.
In App clients, you will also see an App client id.
Both of these ids will be needed in our Nodejs server.
Creating Nodejs serverOk, we have now completed the first section of our tutorial which is to create a user pool in Amazon Cognito.
Now let's move on to the second section of this tutorial which is to create a Nodejs server where we will use Amazon Cognito to authenticate users.
First, install Nodejs in your machine.
Run the npm init command to initialize an npm package.
We will give an entry point as server.
js, but you can provide any entry point you need.
After that create a folder for your project and install the following packages.
npm install express –savenpm install nodemon –save -devWe are going to use express.
js for our routing of the Nodejs server.
Also, we are going to use nodemon which will automatically restart the server upon code changes.
We are going to add nodemon as only a dev dependency.
Let’s first create app.
js which we will use to server configurations such as middleware.
var express = require('express');var app = express();app.
get('/', (req, res) => res.
send('Hello Wfrom nodejs authentication server'));module.
exports = app;Then let’s use this server in our server.
jsvar app = require('.
/app');var server = app.
listen(3000, function(){ console.
log("Server is running on port 3000");});You can now run the server using node server.
js command and go to localhost:3000 to check whether our server is working.
We will need to implement three functions in our server for authentication purpose.
Sign upSign inToken validationFor each function, we will need separate API endpoints in our server as well.
Let’s create each function in three categories.
Before that create a new folder names Controllers and add a new file names AuthController.
js on it.
Also in a new folder named Services add a new file names AuthService.
js.
Install the following packages as well.
npm install body-parser –savenpm install amazon-cognito-identity-js –savenpm isntall node-fetch –savebody-parser is required to parse the body of the request sent to the server.
The second package is the Cognito SDK which we will be going to use for requests to Cognito API.
Sign upIn AuthService.
js add the following code.
global.
fetch = require('node-fetch');global.
navigator = () => null;const AmazonCognitoIdentity = require('amazon-cognito-identity-js');const poolData = { UserPoolId: "{pool_id}", ClientId: "{app_client_id}"};const pool_region = "{region}";const userPool = new AmazonCognitoIdentity.
CognitoUserPool(poolData);exports.
Register = function (body, callback) { var name = body.
name; var email = body.
email; var password = body.
password; var attributeList = []; attributeList.
push(new AmazonCognitoIdentity.
CognitoUserAttribute({ Name: "email", Value: email })); userPool.
signUp(name, password, attributeList, null, function (err, result) { if (err) callback(err); var cognitoUser = result.
user; callback(null, cognitoUser); })}In the above code first, we are going to define a CognitoUserPool giving our pool id and app client id.
Also, make sure to provide the region where this user pool was created.
After that, we are going to parse the body of our request for the username and password.
Since we requested email for signup we are going to attach it as a CognitoUserAttribute.
Then, we are going to call signUp method which will create our user.
Let’s add the needed controller code in AuthController.
js as follows.
var authService = require('.
/Services/AuthService');exports.
register = function(req, res){ let register = authService.
Register(req.
body, function(err, result){ if(err) res.
send(err); res.
send(result); })}Log inLet’s add login functionality in out AuthService.
js and controller logic to AuthController.
jsexports.
Login = function (body, callback) { var userName = body.
name; var password = body.
password; var authenticationDetails = new AmazonCognitoIdentity.
AuthenticationDetails({ Username: userName, Password: password }); var userData = { Username: userName, Pool: userPool } var cognitoUser = new AmazonCognitoIdentity.
CognitoUser(userData); cognitoUser.
authenticateUser(authenticationDetails, { onSuccess: function (result) { var accesstoken = result.
getAccessToken().
getJwtToken(); callback(null, accesstoken); }, onFailure: (function (err) { callback(err); }) })};AuthController.
jsexports.
login = function(req, res){ let login = authService.
Login(req.
body, function(err, result){ if(err) res.
send(err) res.
send(result); })}Token ValidationFor token validation, we are going to need several packages to install.
Install the following packages and import them to AuthService.
jsnpm install request –savenpm install jwk-to-pem –savenpm install jsonwebtoken –saveAuthService.
jsexports.
Validate = function(token, callback){ request({ url : `https://cognitoidp.
${pool_region}.
amazonaws.
com/${poolData.
UserPoolId}/.
well-known/jwks.
json`, json : true }, function(error, response, body){ if (!error && response.
statusCode === 200) { pems = {}; var keys = body['keys']; for(var i = 0; i < keys.
length; i++) { var key_id = keys[i].
kid; var modulus = keys[i].
n; var exponent = keys[i].
e; var key_type = keys[i].
kty; var jwk = { kty: key_type, n: modulus, e: exponent}; var pem = jwkToPem(jwk); pems[key_id] = pem; } var decodedJwt = jwt.
decode(token, {complete: true}); if (!decodedJwt) { console.
log("Not a valid JWT token"); callback(new Error('Not a valid JWT token')); } var kid = decodedJwt.
header.
kid; var pem = pems[kid]; if (!pem) { console.
log('Invalid token'); callback(new Error('Invalid token')); } jwt.
verify(token, pem, function(err, payload) { if(err) { console.
log("Invalid Token.
"); callback(new Error('Invalid token')); } else { console.
log("Valid Token.
"); callback(null, "Valid token"); } }); } else { console.
log("Error! Unable to download JWKs"); callback(error); } });}AuthController.
jsexports.
validate_token = function(req, res){ let validate = authService.
Validate(req.
body.
token,function(err, result){ if(err) res.
send(err.
message); res.
send(result); })}We have now completed implementing the AuthService and AuthController.
Next, let’s add our controller routes to a new file named route.
js and add these routes to our server in app.
jsroutes.
jsvar express = require('express');var router = express.
Router();var authController = require('.
/Controllers/AuthController');router.
post('/auth/register', authController.
register);router.
post('/auth/login', authController.
login);router.
post('/auth/validate', authController.
validate_token);module.
exports = router;app.
jsvar express = require('express');var bodyParser = require('body-parser');var app = express();app.
use(bodyParser.
urlencoded({ extended: false }));app.
use(bodyParser.
json());var routes = require('.
/routes');app.
use('/', routes);module.
exports = app;We have changed our app.
js to take routes from the routes.
js .
So future routes can be defined on the routes.
js without configuring app.
jsThat's it, we have now fully completed our server.
You can test the app by running node server.
js and trying out new API endpoints we have created.
As a bonus feature, I will next show how to authorize API endpoints by using our created authentication method.
First, create a simple controller and add something on it to test.
exports.
simple_hello = function (req, res) { res.
send("Hello from our node server");}Then, let’s create our authentication middleware.
exports.
Validate = function(req, res, next){var token = req.
headers['authorization'];request({ url : `https://cognitoidp.
${pool_region}.
amazonaws.
com/${poolData.
UserPoolId}/.
well-known/jwks.
json`, json : true }, function(error, response, body){ if (!error && response.
statusCode === 200) { pems = {}; var keys = body['keys']; for(var i = 0; i < keys.
length; i++) { var key_id = keys[i].
kid; var modulus = keys[i].
n; var exponent = keys[i].
e; var key_type = keys[i].
kty; var jwk = { kty: key_type, n: modulus, e: exponent}; var pem = jwkToPem(jwk); pems[key_id] = pem; } var decodedJwt = jwt.
decode(token, {complete: true}); if (!decodedJwt) { console.
log("Not a valid JWT token"); res.
status(401); return res.
send("Invalid token"); } var kid = decodedJwt.
header.
kid; var pem = pems[kid]; if (!pem) { console.
log('Invalid token'); res.
status(401); return res.
send("Invalid token"); } jwt.
verify(token, pem, function(err, payload) { if(err) { console.
log("Invalid Token.
"); res.
status(401); return res.
send("Invalid tokern"); } else { console.
log("Valid Token.
"); return next(); } }); } else { console.
log("Error! Unable to download JWKs"); res.
status(500); return res.
send("Error! Unable to download JWKs"); } });}Although this looks like the same as validate function there are a couple of differences.
Since this is a middleware function, parameters are req, res and next.
And our callback will be also next().
Next step is to add this to our routes.
For that in routes.
js add the newly created route as follows.
routes.
jsvar authMiddleware = require('.
/MIddleware/AuthMiddleware');router.
get('/hello', authMiddleware.
Validate, helloController.
simple_hello);That’s it.
Now when we try to get to localhost:3000/hello without providing Authorization header with a valid token we will get a status 401 Unauthorized.
You can implement many more functionalities using the SDK but this is it for this tutorial.
Thanks :).