Using Cognito, Angular and Node.js together (3/3)

Application configuration

Objective: Configure a Node.js service using Express.js to authenticate Cognito tokens.

As prerequisites, we are one npm package, so please install the one listed below:

  • cognito-express [1]: Cognito-express authenticates API requests by verifying the Json Web Tokens signatures generated by Amazon Cognito.

Configure application back-end

To secure the back-end application, we need to setup a middle-ware method that will be applied to all routings where it's needed and a configuration file that returns variables depending on the environment.

The configuration file will return 3 variables, the user pool identifier, the application client identifier and the region where those 2 resources are located in AWS. The code for it looks like this:

module.exports = (function (env) {
    switch (env) {
        case 'prod':
            return {
                userPoolId: 'us-east-2_XYZXYZXYS',
                clientId: 'ABCDEFGHIJKLMNOPQ123',
                region: 'us-east-2'
            };
        case 'uat':
            return {
                userPoolId: 'us-east-2_XYZXYZXYS',
                clientId: 'ABCDEFGHIJKLMNOPQ123',
                region: 'us-east-2'
            };
        default:
            return {
                userPoolId: 'us-east-2_XYZXYZXYS',
                clientId: 'ABCDEFGHIJKLMNOPQ123',
                region: 'us-east-2'
            };
    }
})(process.env.NODE_ENV);

The middle-ware method uses the configuration file and the cognito-express package installed before. This method will be executed in all routings to validate that the token provided is valid according to Cognito. In case the Cognito User Pool can't validate the token, this middle-ware method will return a 401 status code for the request. The code for this method looks like this:

const CognitoExpress = require("cognito-express");
const awsConfig = require('../helpers/awsConfig');

const cognitoExpress = new CognitoExpress({
    region: awsConfig.region,
    cognitoUserPoolId: awsConfig.userPoolId,
    tokenUse: "id",
    tokenExpiration: 3600000
});

function validateAdmin(req, res, next) {
    let accessTokenFromClient = req.headers.authorization;
 
    if (!accessTokenFromClient) return res.status(401).send("Access Token missing from header");

    cognitoExpress.validate(accessTokenFromClient, function (err, response) {
        if (err) return res.status(401).send(err);
        res.locals.user = response;
        next();
    });
}

module.exports = validateAdmin;

Finally, we will apply to the routings the middle-ware method using the "use" method from the ExpressJS router.

var express = require('express');
var router = express.Router();
var cognitoValidator = require('../helpers/cognitoValidator');

var dynamo = require('../helpers/dataService');
router.use(cognitoValidator);

router.get('/'
    , function (req, res) {
        dynamo.getData().then(data => {
            res.json(data);
        }).catch(err => {
            console.log(err);
            res.sendStatus(500);
        });
    });

module.exports = router;

Summary

After finishing all 3 parts of this tutorial, you should have completed the configuration to use Cognito in an Angular application with a Node.js backend service.

Using Cognito, Angular and Node.js together (2/3)

Application configuration

Objective: Configure an angular application to authenticate Cognito users.

As prerequisites, we are using several npm packages, so please install the ones listed below:

  • ngx-toastr [1]: to show messages to the user.
  • amazon-cognito-identity-js [2]
  • aws-sdk [3]

Configure application front-end

To secure the front-end application, we need to setup a couple of services and utilities first. Specifically, we will create:

  • An authentication service that will validate if there is a valid session
  • An authentication guard service that will be used by the Angular Router Module to allow requests
  • A token interceptor to detect all HTTP requests and attach an authentication token. This same interceptor will analyze responses and if there is an error, it will redirect to an appropriate screen.
  • A user service that will handle all the interactions with Cognito

The first authentication service uses a custom made Local Storage Service that is not shown in this tutorial, but basically, under the hood it will use some storage mechanism to persist the tokens and retrieve them.

The code for this service looks like this:

import { AdminLocalStorageService } from "./admin-local-storage.service";
import { Injectable } from "@angular/core";

@Injectable()
export class AuthenticationService {
    constructor(
        private localStorage: AdminLocalStorageService) {
    }

    isAuthenticated(){
        var token = this.localStorage.getToken();
        return token != null;
    }
}

The authentication guard is a special class that implements the CanActivate class from the angular router modules. This service will use our authentication service to verify that the session tokens exist, and if these tokens are missing, it will redirect the user automatically to the login screen.

The code for this service looks like this:

import { Injectable } from '@angular/core';
import { Router, CanActivate } from '@angular/router';
import { AuthenticationService } from './authentication.service';
import { ToastrService } from 'ngx-toastr';

@Injectable()
export class AuthGuardService implements CanActivate {

    constructor(private auth: AuthenticationService
        , private router: Router
        , private toastr: ToastrService) {}

    canActivate(): Promise<boolean> {
        return new Promise(resolve => {
            if(this.auth.isAuthenticated()){
                resolve(true);
            } else {
                this.toastr.error("Please login...", "Unauthorized");
                this.router.navigate(['/']);
                resolve(false);
            }
        });
    }
}

The token interceptor, as mentioned before, will be used by the application module to intercept all HTTP requests and attach the authentication token. Since we are doing authentication only, we are adding only one header to the requests that contains the ID token retrieved from Cognito.

The code for the interceptor looks like this:

import { Injectable } from '@angular/core';
import {
  HttpRequest,
  HttpHandler,
  HttpEvent,
  HttpInterceptor,
  HttpResponse,
  HttpErrorResponse
} from '@angular/common/http';
import { Observable } from 'rxjs/Observable';
import { Router } from '@angular/router';
import 'rxjs/add/operator/do';

import { AuthenticationService } from './services/authentication.service';
import { AdminLocalStorageService } from './services/admin-local-storage.service';
import { ToastrService } from 'ngx-toastr';


@Injectable()
export class TokenInterceptor implements HttpInterceptor {

  constructor(public auth: AuthenticationService
    , private localStorage: AdminLocalStorageService
    , private router: Router
    , private toastr: ToastrService) {}

  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        const authData = this.localStorage.getToken();
        let requestItem = request;
        if (authData) {
            requestItem = request.clone({
                headers: request.headers.set("Authorization",
                    authData.jwtToken)
            });
        }
        return next.handle(requestItem).do((event: HttpEvent<any>) => {
            if (event instanceof HttpResponse) {
              //letting it pass
            }
        }, (err: any) => {
            if (err instanceof HttpErrorResponse) {
                if (err.status === 401) {
                    this.localStorage.deleteToken();
                    this.toastr.error("Please login again.", "Session ended");
                    this.router.navigate(['/']);
                }
            }
        });
    }
}

Finally, the last service needs two classes, the service that will be doing the authentication and a utils class that will help processing classes and attributes for Cognito.

The Cognito utils class looks like this:

import { IAuthenticationDetailsData, CognitoUserPool, CognitoUserAttribute, ICognitoUserAttributeData } from "amazon-cognito-identity-js";
import { environment } from "../../environments/environment";
import { AttributeListType } from "aws-sdk/clients/cognitoidentityserviceprovider";
import { AttributeType } from "aws-sdk/clients/elb";

export class CognitoUtils {
    public static getAuthDetails(email: string, password: string): IAuthenticationDetailsData {
        return {
            Username: email,
            Password: password,
        };
    }

    public static getUserPool() {
        return new CognitoUserPool(environment.cognitoSettings);
    }

    public static getAttribute(attrs: CognitoUserAttribute[], name: string): CognitoUserAttribute {
        return attrs.find(atr => atr.getName() === name);
    }

    public static getAttributeValue(attrs: AttributeListType, name: string, defValue: any): string {
        const attr = attrs.find(atr => atr.Name === name);
        return attr ? attr.Value : defValue;
    }

    public static getActiveAttribute(attrs: AttributeListType): boolean {
        return CognitoUtils.getAttributeValue(attrs, 'custom:active', '1') === '1';
    }

    public static createNewUserAttributes(request): CognitoUserAttribute[] {
        const emailAttribute = new CognitoUserAttribute({Name : 'email', Value : request.email });
        const emailVerifiedAttribute = new CognitoUserAttribute({Name : 'email_verified', Value : 'true' });
        const activeAttribute = new CognitoUserAttribute({Name : 'custom:active', Value : (request.active ? 1 : 0).toString() });
        return [
            emailAttribute, activeAttribute
        ];
    }

    public static createUpdatableUserAttributesData(request): AttributeListType {
        const preferedUsername = {Name : 'preferred_username', Value : request.username };
        const emailAttribute = {Name : 'email', Value : request.email };
        const emailVerifiedAttribute = {Name : 'email_verified', Value : 'true' };
        const activeAttribute = {Name : 'custom:active', Value : (request.active ? 1 : 0).toString() };
        return [
            preferedUsername, emailAttribute, emailVerifiedAttribute,
            activeAttribute
        ];
    }
}

And the code for the service looks like this:

import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { HttpClient, HttpHeaders } from '@angular/common/http';

import { Observable } from 'rxjs/Observable';
import { Observer } from 'rxjs/Observer';
import 'rxjs/add/observable/from.js';
import { IntervalObservable } from 'rxjs/observable/IntervalObservable';
import { CognitoUserSession, CognitoUserPool, CognitoUser, AuthenticationDetails } from 'amazon-cognito-identity-js';
import * as AWS from 'aws-sdk';
import { ListUsersRequest } from 'aws-sdk/clients/cognitoidentityserviceprovider';

import { environment } from '../../../environments/environment';
import { CognitoUtils } from '../cognitoUtils';
import { User } from '../models/user';
import { AdminLocalStorageService } from './admin-local-storage.service';

@Injectable()
export class UsersService {
    session: CognitoUserSession;
    cognitoAdminService: AWS.CognitoIdentityServiceProvider;
    userPool: CognitoUserPool;

    constructor(private http: HttpClient, private router: Router, private adminLocalStorage: AdminLocalStorageService) {
        this.cognitoAdminService = new AWS.CognitoIdentityServiceProvider({
            accessKeyId: environment.awsConfig.accessKeyId,
            secretAccessKey: environment.awsConfig.secretAccessKey,
            region: environment.awsConfig.region
        });
        this.userPool = CognitoUtils.getUserPool();
    }

    public login(login: string, password: string): Observable<User | false> {
        const cognitoUser = new CognitoUser(this.getUserData(login));
        cognitoUser.setAuthenticationFlowType('USER_PASSWORD_AUTH');
        const authenticationDetails = new AuthenticationDetails(CognitoUtils.getAuthDetails(login, password));
        return Observable.create(obs => {
            cognitoUser.authenticateUser(authenticationDetails, {
                onSuccess: result => {
                    this.session = result;
                    const token = result.getIdToken();
                    const accessToken = result.getAccessToken();
                    this.adminLocalStorage.setToken(token);
                    this.adminLocalStorage.setAccessToken(accessToken);
                    this.router.navigate(['managereps']);
                },
                onFailure: err => {
                    console.error(err);
                    obs.next(false);
                },
                newPasswordRequired: (userAttributes, requiredAttributes) => {
                    this.router.navigate(['dashboard/login', { username: login }]);
                    obs.next(false);
                }
            });
        });
    }

    private getUserData(email: string) {
        return {
            Username: email,
            Pool: this.userPool
        };
    }

    public addUser(newUser: User): Observable<Object> {
        return Observable.create(obs => {
            const attrs = CognitoUtils.createNewUserAttributes(newUser);
            const cognitoUser = new CognitoUser(this.getUserData(newUser.username));
            this.userPool.signUp(newUser.username, newUser.password, attrs, [], (error, data) => {
                    if (error) {
                        console.error(error);
                        obs.next(false);
                        return;
                    }
                    this.cognitoAdminService.adminConfirmSignUp({
                        Username: newUser.username,
                        UserPoolId: this.userPool.getUserPoolId()
                    }, (e, d) => this.defaultAdminCallback(e, d, obs));
            });
        });
    }

    private defaultAdminCallback(error, data, obs, ok: any = true, no: any = false) {
        if (error) {
            console.error(error);
            obs.next(no);
            return;
        }
        obs.next(ok);
    }
}

As you can see, we have 2 main public methods, one for login and the other one to create users. The user class reference  above, it's just a class with 3 properties for username, password and email.

The login method is used in a login page that accepts the credentials used for authentication. And the addUser method is used in a sign up page for users. Both of these pages are not provided in this tutorial. 

Finally, we need to add the interceptor and the guard service to the application module. Note: since this tutorial was created using a small application, only the app module was created, however, the interceptor module must be applied in any module that the HTTPClientModule is imported and the guard must be applied in any routing configuration.

To apply the guard configuration in the module, we override the canActivate property for the routes as shown next:

const routes: Routes = [
  {
    path: 'page1', component: Page1Component,
    canActivate: [AuthGuardService]
  },
  { path: '', component: LoginComponent }
];

For the interceptor, we need to add it to the providers property configuration as shown next:

providers: [
    {
      provide: HTTP_INTERCEPTORS,
      useClass: TokenInterceptor,
      multi: true
    },
    AdminLocalStorageService,
    AuthenticationService,
    AuthGuardService,
    UsersService
  ]

With this, the configuration for the angular application is complete, and it should be ready for testing. So, finish creating the login and sign up component and start authenticating users.

Continue with the third part of this tutorial here

Using Cognito, Angular and Node.js together (1/3)

Today, I'm writing a new tutorial after two weeks of continuing working on AWS. However, this time I'm going to dive a little bit more on development than the last time. 

This time, I would like to explain how to use Cognito, Angular and Node.js together. The first one to be used as user and access management system, Angular as the framework for the front end application, and obviously, Node.js as a backend service using Express.js also.

Cognito

Objective: create a user pool to sign-up and authenticate users

The first thing we need to configure is a User Pool that will be used as a user directory. This directory will allow us sign up and sign in users. 

User Pools also provide integration with third party providers such as Facebook, Google, Amazon, and Microsoft Active Directory. [1]

Amazon Cognito - User Pools

In this tutorial, we won't enable third party integration with any provider. Our user pool will contain our users and some information such as the email, user name, password and an active indicator.

Creating the user pool

On the Amazon console, go to the Security section and click on Cognito. On the screen that appears, click on "Manage User Pools" and then "Create User Pool".

Right after, the first step is to type the name of the user pool. 

User Pool Creation - Type name

After typing the name, click on the "Step Through Settings" option, we will go step by step through all the configuration.

The next section will ask us to select the attributes for our users. In there, select the option to use an username to sign up and sign in, and mark the check boxes to sign in with a verified email address and preferred username. This two options will provide flexibility for our end users and validate that they are using valid email addresses. 

User Pool Creation - Attributes

Below this section, you can add more attributes that will be used for the user profile, for example zip code, among others. Also, there is the possibility to add custom attributes that are not supported out of the box.

Continuing with the wizard, the next section that we will configure are the policies. These policies will specify the level of security of the password, for our case we will leave all options by default.

Also, we will allow users to sign themselves in the User Pool. Note: This is important, if we allow only administrators to create users, Amazon leaves users with a FORCE_CHANGE_PASSWORD state that is not easily changed, therefore, we will use the AWS SDK later to add users manually without the console.

User Pool Creation - Policies

Continue with the wizard and in the MFA and Verification section, create a role that will be used automatically by AWS to send SMS messages. Even though this is not going to be used in our tutorial, it's better to set this up right away. Note: the role created here is not the same to the roles we can assign to our users. To assign roles to a user, first you must create a group after the user pool is created, and assign roles to that group. Then, all the users that belongs to a particular group, will inherit those roles.

User Pool Creation - MFA and Verifications

The rest of the options were left with the default values. Continuing with the wizard, we only have two more sections that we are interested in. Navigate until reaching the Message Customizations section and change the verification type radio button to use links instead of codes. The only difference you will see is that the emails sent to users will contain a Url to verify the account, instead of using a pass code to be validated somewhere else (this will make the signup process much faster).

User Pool Creation - Message Customizations

Finally, navigate to the App Client section in the wizard, this will be a key part in the creation of the user pool because this will provide us the identification keys that our application will use to validate users.

Click on Create App Client and type a client name. Leave the Generate Client Secret unchecked, otherwise authentication does not work as expected. Besides, mark the enable username-password for app-based authentication, this feature allows our application to use a combination of username and password to authenticate our users. Click again on Create App Client and finish creating the user pool. Note: the last section of the wizard will show a review of all the configurations added through the wizard, if there is something that needs to be changed, change it now. 

User Pool Creation - App Client

Continue with the second part of this tutorial here