# Access Control with AWS Cognito

Cognito is an services provided by Amazon for managing user profiles, tracking users sessions and, when combined with IAM roles, securing AWS resources.

The Cloudbash application uses AWS Cognito for the following functionality:

  • User Authentication (Sign up & Sign in)
  • Authorization of
  • Role based Authorization
  • Identity Verification (by SMS)
  • User Account Recovery (by email)

# Serverless Framework

Cognito User Pool - source

Type: AWS::Cognito::UserPool
Properties:
    UserPoolName: Cloudbash

User Pool Client - source

Type: AWS::Cognito::UserPoolClient
Properties:
    ClientName: CloudbashWebApp
    GenerateSecret: false
    UserPoolId:
        Ref: CloudbashCognitoUserPool

# Client-side

# Implementing user sign up and login with AWS- Cognito and Amplify

AWS Amplify is an Javascript library that provides tools for developing serverless web applications using amazon web services. Amplify has many features that will speed up your development. For this application we are using the Authentication API and Angular UI components for the user sign up and login flow.

The preferred way for using Cognito with AWS Amplify is to generate the user pool with the Amplify CLI. But for this project we will be using a user pool that we have created with the Serverless Framework. The ID and region of the user pool need to be provided in the aws-exports.js file in the source folder of your project.

Dependencies

app.module.ts - source
The AmplifyService is registered as an provider.

import { AmplifyService, AmplifyModules } from 'aws-amplify-angular';
import Auth from '@aws-amplify/auth'; 

providers: [
    {
        provide: AmplifyService,
        useFactory:  () => {
        return AmplifyModules({
            Auth
        });
        }
    }
],

authentication.service.ts - source
The authentication service contains logic that will listen to auth changes from the amplify service. When a user is succesfully logged in, the user information is stored for use in the application.
The getRole() function provides a fast way for retrieving the role from the signed in user.

import { Observable, BehaviorSubject } from 'rxjs';
import { Injectable } from '@angular/core';
import { AmplifyService } from 'aws-amplify-angular';
import { Auth } from 'aws-amplify';
import { fromPromise } from 'rxjs/observable/fromPromise';
import { map, catchError } from 'rxjs/operators';
import { of } from 'rxjs/observable/of';
import { Router } from '@angular/router';
import { Role } from '../models/user.model';

@Injectable({
    providedIn: 'root',
})
export class AuthenticationService {
    public loggedIn: BehaviorSubject<boolean>;
    public user: any;
    public currentUserRole: Role;
    signedIn: boolean;

    constructor(private amplifyService: AmplifyService,    
        private router: Router) {
        this.loggedIn = new BehaviorSubject<boolean>(false);
        this.amplifyService.authStateChange$
            .subscribe(authState => {                     
                if (!authState.user) {
                    this.user = null;
                } else {
                    this.user = authState.user;
                    this.currentUserRole = this.getRole(authState.user);
                }
                if(authState.state === 'signedIn'){
                    this.loggedIn.next(true);
                    this.signedIn = true;
                }
                if(authState.state === 'signedOut'){
                    this.loggedIn.next(false);
                    this.signedIn = false;
                }
            });
    }

    public isAuthenticated(): Observable<boolean> {
        return fromPromise(Auth.currentAuthenticatedUser())
            .pipe(
                map(result => {
                    this.loggedIn.next(true);
                    return true;
                }),
                catchError(error => {
                    this.loggedIn.next(false);
                    return of(false);
                })
            );
    }

    public signOut() {
        this.loggedIn.next(false);
        this.signedIn = false;
        fromPromise(Auth.signOut())
            .subscribe(
                result => {                   
                    this.router.navigate(['/sign-in']);
                },
            );
    }

    public isAdmin() : boolean {
        return this.currentUserRole === Role.Admin;
    }

    public getRole(user: any): Role {
        const roles: any[] = user.signInUserSession
            .idToken.payload['cognito:groups'];
        if (roles) {
            if (roles[0] == 'Admin') {
                return Role.Admin;
            } else {
                return Role.User;
            }
        }
    }

}

auth-page.component.ts - source
The Amplify Authenticator component implements forms for user sign up, log in and account recovery.

<amplify-authenticator></amplify-authenticator>

auth.guard.ts - source
The AutGuard can be used to authorize users when they visit a specific page. When the user is not authenticated, or does not have the correct role, it will be redirected to the log-in page.

import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router';
import { AuthenticationService } from '../services/authentication.service';
import { tap } from 'rxjs/internal/operators/tap';

@Injectable({
    providedIn: 'root',
  })
export class AuthGuard implements CanActivate {
    constructor(
        private router: Router,
        private auth: AuthenticationService
    ) { }

    canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
        return this.auth.isAuthenticated()
            .pipe(
                tap(loggedIn => {
                    if (!loggedIn) {
                        this.router.navigate(['/sign-in']);
                    } else {
                        const role = this.auth.currentUserRole;
                        if (route.data.roles && route.data.roles.indexOf(role) === -1) {
                            // role not authorised so redirect to home page
                            this.router.navigate(['/sign-in']);
                            return false;
                        }
                        // authorised so return true
                        return true;
                    }
                })
            );
    }
}

jwt.interceptor.ts - source
The JWT Interceptor automatically adds an Authorization header with the JWT Token when a HTTP call is made.

import { Injectable } from '@angular/core';
import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http';
import { AuthenticationService } from '../services/authentication.service';
import { Observable } from 'rxjs/Observable';

@Injectable()
export class JwtInterceptor implements HttpInterceptor {
    
    constructor(private authenticationService: AuthenticationService) { }

    intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {     
        let currentUser;
        if (this.authenticationService.user) {
            currentUser = this.authenticationService.user.signInUserSession.idToken.jwtToken;
        }

        if (currentUser) {
            request = request.clone({
                setHeaders: {
                    Authorization: `Bearer ${currentUser}`
                }
            });
        }

        return next.handle(request);
    }
}

app-routing.module.ts - source
Making use of the AuthGuard to secure routes. The example below makes sure that only authenticated users with the admin rule can visit the page on the /admin path.

 {
    path: '/admin',
    component: AdminDashboardPageComponent,
    canActivate: [AuthGuard],
    data: { roles: [Role.Admin] }
},

# Server-side

# Serverless Framework

ApiGateWayAuthorizer.yml - source
Most simple way of authorizing REST Endpoints against a Cognito user pool.

Type: AWS::ApiGateway::Authorizer
Properties: 
    AuthorizerResultTtlInSeconds: 300
    IdentitySource: method.request.header.Authorization
    Name: cognito-authorizer
    RestApiId: 
        Ref: "ApiGatewayRestApi"
    Type: COGNITO_USER_POOLS
    ProviderARNs: 
      - [COGNITO USER POOL ARN]

ApiGateWayAuthorizer.yml - source
Example of an Lambda (exposed as an REST Endpoint with API Gateway) secured with the ApiGateWayAuthorizer.

handler: Cloudbash.Lambda::Cloudbash.Lambda.Functions.Concerts.CreateConcertFunction::Run
events:
- http:
    path: concerts
    method: post
    cors:
        origin: '*'
        headers:
            - Authorization
    authorizer:
        type: COGNITO_USER_POOLS
        authorizerId: 
            Ref: apiGatewayAuthorizer

# Further reading