Last update on Saturday, January 12th 2019

Analyzing the Ionic AWS Full-Stack Starter: Configuration and Providers

I have used both Ionic and AWS for years now. Both technologies are (mostly) free and awesome.

Ionic is the badass framework to build cross-platform mobile applications and AWS gives us the tools to host, cache, manipulate files with authentication and many more services in the cloud.

So what's the result if both companies allied together to create a project: the Ionic AWS Starter!

Until now, I have only used the S3 service so this stack gave me a new look at AWS potential.

The AWS Mobile Hub

Instead of creating the backend in Java, Ruby or Node.js, AWS offers some services that can be easily configured by clicking some buttons.

Those services are centered in one place: the Mobile Hub.

AWS mobile hub

As you can see from this picture, there are nine types of services. Only five are used in our stack:

  1. Authentication
  2. NoSQL Database
  3. User Data Storage
  4. User Engagement
  5. Hosting and Streaming

As I said before, most of those services can be activated with just one click:

AWS mobile hub add service

We can start from zero, add the services one by one or import them, which brings us back to the starter project.

The installation instructions are available there. (You might have to configure your AWS credentials, to avoid this, I manually downloaded the aws-config.js file)

The Ionic project already have a mobile-hub-project.zip file that contains a configuration for the Mobile Hub, so most of the work is already done for us.

If we unzip the file, we can have a look at the yaml configuration:

--- !com.amazonaws.mobilehub.v0.Project
features:
  content-delivery: !com.amazonaws.mobilehub.v0.ContentDelivery
    attributes:
      enabled: true
      visibility: public-global
  database: !com.amazonaws.mobilehub.v0.Database
    components:
      database-nosql: !com.amazonaws.mobilehub.v0.NoSQLDatabase
        tables:
          - !com.amazonaws.mobilehub.v0.NoSQLTable
            attributes:
              description: S
              created: N
              taskId: S
              category: S
              userId: S
            hashKeyName: userId
            hashKeyType: S
            indexes:
              - !com.amazonaws.mobilehub.v0.NoSQLIndex
                hashKeyName: userId
                hashKeyType: S
                indexName: DateSorted
                rangeKeyName: created
                rangeKeyType: N
            rangeKeyName: taskId
            rangeKeyType: S
            tableName: ionic-mobile-hub-tasks
            tablePrivacy: private
  mobile-analytics: !com.amazonaws.mobilehub.v0.Pinpoint
    components:
      analytics: !com.amazonaws.mobilehub.v0.PinpointAnalytics {}
      messaging: !com.amazonaws.mobilehub.v0.PinpointMessaging {}
  sign-in: !com.amazonaws.mobilehub.v0.SignIn
    attributes:
      enabled: true
      optional-sign-in: true
    components:
      sign-in-user-pools: !com.amazonaws.mobilehub.v0.UserPoolsIdentityProvider
        attributes:
          alias-attributes:
            - email
          mfa-configuration: OFF
          name: userpool
          password-policy: !com.amazonaws.mobilehub.ConvertibleMap
            min-length: "8"
            require-lower-case: true
            require-numbers: false
            require-symbols: false
            require-upper-case: false
  user-files: !com.amazonaws.mobilehub.v0.UserFiles
    attributes:
      enabled: true
  user-profiles: !com.amazonaws.mobilehub.v0.UserSettings
    attributes:
      enabled: true
name: Ionic Mobile Hub Starter
region: us-east-1

This configuration is very concise. We only need 60 lines to create five features. Not bad right?

Let's focus on the sign-in feature, the attributes will specify the type of authentication (ex: email), how strong the password should be or if signing in is optional or required.

Taking a step back, the authentication is designed on the backend (the Mobile Hub) to be optional.

When launching our app:

aws login

This doesn't look optional 🤔.

Aside from that, what does the mobile app do?

In a nutshell, it allows a user to create an account, connect, add some tasks, change its profile picture and disconnect.

Those actions cover a good spectrum of use cases showcasing the use of:

  1. AWS S3 for stocking the profile picture
  2. Cognito for authentication
  3. DynamoDB for stocking the tasks details
  4. Ionic Native for the use of the camera

The architecture is as follow:

Ionic Aws stack architecture

Nothing new about the pages, each feature is separated in its own folders.
Provider wise, every AWS Services are available in the providers folder: Cognito, DynamoDb and User.

The providers.ts file exports those services:

import { Cognito } from "./aws.cognito";
import { DynamoDB } from "./aws.dynamodb";
import { User } from "./user";

export { Cognito, DynamoDB, User };

Special mention to the assets folder which contains the AWS libraries:

Ionic Aws stack assets folder

Why not using the package.json to acquire those files?

Two possible reasons:

  1. They use a custom AWS version, different from the public one
  2. They want AWS to be loaded before other Angular and Ionic files, in the index.html file:

Index.html load

Aside from the AWS core libraries, the aws.config.js file is very important, this file sets multiple global constants that will be used for configuration like the region, the bucket, etc.

Those constants are then acquired in the app.config.ts file:

import { Injectable } from '@angular/core';

declare var AWS: any;
declare const aws_mobile_analytics_app_id;
declare const aws_cognito_region;
declare const aws_cognito_identity_pool_id;
declare const aws_user_pools_id;
declare const aws_user_pools_web_client_id;
declare const aws_user_files_s3_bucket;

@Injectable()
export class AwsConfig {
  public load() {

    // Expects global const values defined by aws-config.js
    const cfg = {
      "aws_mobile_analytics_app_id": aws_mobile_analytics_app_id,
      "aws_cognito_region": aws_cognito_region,
      "aws_cognito_identity_pool_id": aws_cognito_identity_pool_id,
      "aws_user_pools_id": aws_user_pools_id,
      "aws_user_pools_web_client_id": aws_user_pools_web_client_id,
      "aws_user_files_s3_bucket": aws_user_files_s3_bucket
    };

    AWS.config.customUserAgent = AWS.config.customUserAgent + ' Ionic';

    return cfg;
  }
}

Note the:

declare var AWS: any;

We will encounter this again and again and again in this project.

Typically, this could have been placed once and for all in a declaration.d.ts file in order to be available everywhere.

The app.module.ts file is just business as usual, except:

IonicModule.forRoot(MyApp, new AwsConfig().load());

All our previous configurations are merged with Ionic's Config.

Moving on to the Providers.

Providers

Starting with the simplest one: DynamoDB.

Located in the aws.dynamodb.ts file:

import { Injectable } from '@angular/core';

declare var AWS: any;

@Injectable()
export class DynamoDB {

  private documentClient: any;

  constructor() {
    this.documentClient = new AWS.DynamoDB.DocumentClient();
  }

  getDocumentClient() {
    return this.documentClient;
  }

}

This Service creates the document client and an accessor.
The document client is our god object that will allow us to manipulate the database.
The documentation is there, with every traditional db commands: put, delete, get, query…

Followed by the Cognito service.

Located in the aws.cognito.ts file:

import { Injectable } from "@angular/core";
import { Config } from "ionic-angular";

declare var AWS: any;
declare var AWSCognito: any;

Which starts with some imports and declarations.

@Injectable()
export class Cognito {

  constructor(public config: Config) {
    AWSCognito.config.region = config.get('aws_cognito_region');
    AWSCognito.config.credentials = new AWS.CognitoIdentityCredentials({
      IdentityPoolId: config.get('aws_cognito_identity_pool_id')
    });
    AWSCognito.config.update({customUserAgent: AWS.config.customUserAgent});
  }
  .
  .
  .
}

In the constructor, we setup AWSCognito with the configuration we talked about earlier:

The region where the endpoints are located (ex: us-east-1)
The IdentityPoolId: A unique identifier pointing to the Cognito Service
The CustomUserAgent: In order to avoid user agent filters (ex: bot defense), the user agent is set to “MobileHub v0.1”

Followed by the getUserPool method:

  getUserPool() {
    let self = this;
    return new AWSCognito.CognitoIdentityServiceProvider.CognitoUserPool({
      "UserPoolId": self.config.get('aws_user_pools_id'),
      "ClientId": self.config.get('aws_user_pools_web_client_id')
    });
  }

Setting up the access to AWSCognito’s user pool.

Important workaround here, the this object is stocked in another variable named self in order to keep it available in the CognitoUserPool method.

Let’s avoid the confusion!

The UserPoolId represents where the clients are stocked whereas the ClientId authenticates applications that connect to this user pool:

Ionic Aws stack client ids

Those ids are available in the mobile-hub-project.yml configuration file and on the AWS Mobile Hub webpage.

Once we have access to the user pool, we can:

  • Access a user from the pool:
  makeUser(username) {
    return new AWSCognito.CognitoIdentityServiceProvider.CognitoUser({
      'Username': username,
      'Pool': this.getUserPool()
    });
  }
  • Get the current user:
  getCurrentUser() {
    return this.getUserPool().getCurrentUser();
  }
  • Create authentication data:
  makeAuthDetails(username, password) {
    return new AWSCognito.CognitoIdentityServiceProvider.AuthenticationDetails({
      'Username': username,
      'Password': password
    });
  }
  • Create a user attribute:
  makeAttribute(name, value) {
    return new AWSCognito.CognitoIdentityServiceProvider.CognitoUserAttribute({
      'Name': name,
      'Value': value
    });
  }

We are now done with the Cognito Service.

Moving on to the last one: the User Service.

This Service will use the Cognito Service’s methods.

import { Injectable } from '@angular/core';
import { Config } from 'ionic-angular';

import { Cognito } from './providers';

declare var AWS: any;

@Injectable()
export class User {

  private user: any;
  public loggedIn: boolean = false;

  constructor(public cognito: Cognito, public config: Config) {
    this.user = null;
  }
  .
  .
  .
}

Two interesting properties here:

  1. user: Will stock the Cognito user once logged in
  2. loggedId: A simple flag (not yet used)

Some accessors:

  getUser() {
    return this.user;
  }

  getUsername() {
    return this.getUser().getUsername();
  }

The registration part:

  register(username, password, attr) {
    let attributes = [];

    for (var x in attr) {
      attributes.push(this.cognito.makeAttribute(x, attr[x]));
    }

    return new Promise((resolve, reject) => {
      this.cognito
        .getUserPool()
        .signUp(username, password, attributes, null, function(err, result) {
        if (err) {
          reject(err);
        } else {
          resolve(result.user);
        }
      });
    });
  }

When a user registers, its attributes (name, address, etc.) must go through some transformations in the makeAttribute method.

A Promise is returned so the flow can progress.
Then the user is signed up to the user pool. Finally, if everything is good, the user is returned.

If the user forgot its registration code:

  resendRegistrationCode(username) {
    return new Promise((resolve, reject) => {
      let user = this.cognito.makeUser(username);
      user.resendConfirmationCode((err, result) => {
        if (err) {
          console.log('could not resend code..', err);
          reject(err);
        } else {
          resolve();
        }
      });
    });
  }

Using the username, the makeUser method is used to grab the matching user then the built-in resendConfirmationCode method is used (we didn’t code this one, many methods come from the Cognito user object #profit).

Once the user has the code:

   confirmRegistration(username, code) {
    return new Promise((resolve, reject) => {
      let user = this.cognito.makeUser(username);
      user.confirmRegistration(code, true, (err, result) => {
            if (err) {
              console.log('could not confirm user', err);
              reject(err);
            } else {
              resolve(result);
            }
        });
    });
  }

Same as before, the makeUser method gives us the Cognito user and we check if the code is good by using the confirmRegistration method.

More code in the login method:

  login(username, password) {
    return new Promise((resolve, reject) => {
      let self = this;
      let user = this.cognito.makeUser(username);
      let authDetails = this.cognito.makeAuthDetails(username, password);

      user.authenticateUser(authDetails, {
        'onSuccess': function(result) {
          var logins = {};
          var loginKey = 'cognito-idp.' +
                          self.config.get('aws_cognito_region') +
                          '.amazonaws.com/' +
                          self.config.get('aws_user_pools_id');
          logins[loginKey] = result.getIdToken().getJwtToken();

          AWS.config.credentials = new AWS.CognitoIdentityCredentials({
           'IdentityPoolId': self.config.get('aws_cognito_identity_pool_id'),
           'Logins': logins
          });

          self.isAuthenticated().then(() => {
            resolve();
          }).catch((err) => {
            console.log('auth session failed');
          });
        },

        'onFailure': function(err) {
          console.log('authentication failed');
          reject(err);
        }
      });
    });
  }

The login method will get the user matching the username.
It will compare it to the information from authDetails using the authenticateUser method.

If everything is good, we receive a result that contains a token.

The loginKey string can be scary, but in the end it’s just a path to the user pool located in our region.
This path is associated to the token we received. Using this path and our Cognito’s IdentityPoolId, we can now set the credentials for AWS.
Those credentials will automatically be added to our future requests.

The final touch is the isAuthenticated method:

  isAuthenticated() {
    var self = this;
    return new Promise((resolve, reject) => {
      let user = this.cognito.getCurrentUser();
      if (user != null) {
        user.getSession((err, session) => {
          if (err) {
            console.log('rejected session');
            reject()
          } else {
            console.log('accepted session');
            var logins = {};
            var loginKey = 'cognito-idp.' +
              self.config.get('aws_cognito_region') +
              '.amazonaws.com/' +
              self.config.get('aws_user_pools_id');
            logins[loginKey] = session.getIdToken().getJwtToken();

            AWS.config.credentials = new AWS.CognitoIdentityCredentials({
              'IdentityPoolId': self.config.get('aws_cognito_identity_pool_id'),
              'Logins': logins
            });

            self.user = user;
            resolve()
          }
        });
      } else {
        reject()
      }
    });
  }

First we grab the current user, if he does exist, we look if we can get a session.
Once again the loginKey variable is created. This time the token comes from the session itself.

Finally, the user is stocked in the user property of the User Class.

Remember, this one:

@Injectable()
export class User {

  private user: any;
  .
  .
  .
}

Let’s not forget the logout method!

  logout() {
    this.user = null;
    this.cognito.getUserPool().getCurrentUser().signOut();
  }

Cleaning up our local user and signing out the user from Cognito’s user pool.

And Voila we are done with our Providers!

Conclusion

In this tutorial we have seen the most complex parts of the application: AWS configuration and Providers creation.
The rest of the application will use those parts in order to communicate with the AWS Services to handle the users, the tasks and the profile pictures.

The AWS Mobile Hub is super cool to use!

It’s the first time that I can configure so many complex services by only using a few clicks. In the next tutorial we will have a look at how the Providers are integrated into the application’s business logic.

Until next time.

Analyzing the Ionic AWS Full-Stack Starter: Custom APIs

The last step to
mastering the Io...
...

Analyzing the Ionic AWS Full-Stack Starter: The Features

Discover how the
Ionic AWS Full-S...
...

ES6 Features That Can't Be Ignored (part 1)

This first part will
show you the m...
...

Stay up to date


Join over 4000 other developers who already receive my tutorials and their source code out of the oven with other free JavaScript courses and an Angular cheatsheet!
Designed by Jaqsdesign