Ionic 4 + AppSync: Set up Authentication with Amazon Cognito - Part 5

In this post we're going to continue building our app by adding authentication and authorization features to it. This isn't something you'd normally consider easy to implement, but with Amplify you can do this in a matter of minutes.

We're going to use Amazon Cognito to manage our users and we'll use the Amplify CLI to set it up on our AWS backend. Then, we'll use the Amplify library to add UI components for authentication to our Ionic app.

This tutorial is split up into these parts:
Part 1 - Introduction to GraphQL & AWS AppSync
Part 2 - Create a GraphQL API
Part 3 - Add Ionic pages and GraphQL queries
Part 4 - Use GraphQL mutations in Ionic
Part 5 - Set up Authentication with Amazon Cognito (this post)
Part 6 - Add ElasticSearch to GraphQL API
(more parts will be added later)

You can find the source code for this tutorial on my Github.

What is Amazon Cognito?

From the Developer Guide: Amazon Cognito provides authentication, authorization, and user management for your web and mobile apps. Your users can sign in directly with a user name and password, or through a third party such as Facebook, Amazon, or Google.

Cognito uses user pools for storing user information and identity pools to manage user access to other AWS resources.

We don't have to set these up ourselves though, the Amplify CLI will do all of that for us.

Enable Amazon Cognito

In part 2 of this tutorial, when we created our AppSync backend, we chose an API key as the authorization type. Now we are going to replace that with Amazon Cognito User Pools.

In order to do that we need to run the amplify update api command.

$ amplify update api
? Please select from one of the below mentioned services GraphQL
? Choose an authorization type for the API Amazon Cognito User Pool
Using service: Cognito, provided by: awscloudformation  
 The current configured provider is Amazon Cognito.
 Do you want to use the default authentication and security configuration? Yes, use the default configuration.

Add Authorization rules

We can now modify our GraphQL schema and add authorization rules to it with the @auth directive.

For now, we are going to specify that, for both the types Deck and Card, we will only allow their owner access to them.

So when a user signs in they will only see their own decks and cards.

type Deck @model @auth(rules: [{ allow: owner }])  {  
  id: ID!
  name: String!
  cards: [Card]! @connection(name: "DeckCards")
}

type Card @model @auth(rules: [{ allow: owner }]) {  
  id: ID!
  question: String!
  answer: String
  deck: Deck! @connection(name: "DeckCards")
}

You can set up more sophisticated rules, as in the example below. See the docs for more information.

@auth(
  rules: [
    { 
      allow: owner, 
      ownerField: "owner", 
      mutations: [create, update, delete], 
      queries: [get, list]
    },
  ]) 

Update backend

Now we need to push these changes to AWS so it can create all the resources we need to get authentication and authorization working.

$ amplify push

The CLI will ask you some questions about code generation, you can choose as below.

? Do you want to update code for your updated GraphQL API Yes
? Do you want to generate GraphQL statements (queries, mutations and subscription) based on your schema types? This will overwrite your current graphql queries, mutations and subscriptions Yes

Your aws-exports.js file should now have changed to include the settings needed for Amazon Cognito. You can't use the API key anymore to connect to the GraphQL API.

We need to update main.ts to configure the Amplify library to use these settings and then we'll go ahead and make our UI changes.

import Amplify from 'aws-amplify';

PubSub.configure(awsConfig);  
API.configure(awsConfig);  
Amplify.configure(awsConfig);  

Add Authentication UI components

We are now going to add authentication to our Ionic app. We'll need to install 2 packages to help us with this.

$ npm install --save aws-amplify-angular @aws-amplify/ui

Next, we'll create a Login page.

$ ionic g page login

Modify login.module.ts to import modules from the aws-amplify-angular package.

import { AmplifyAngularModule, AmplifyIonicModule, AmplifyService } from 'aws-amplify-angular'

@NgModule({
  imports: [
    CommonModule,
    FormsModule,
    IonicModule,
    RouterModule.forChild(routes),
    AmplifyAngularModule,
    AmplifyIonicModule
  ],
  declarations: [LoginPage],
  providers: [AmplifyService]
})

Next, we'll add the following code to login.page.html:

<ion-header no-border>  
  <ion-toolbar color="secondary">
    <ion-title>Quiz App</ion-title>
  </ion-toolbar>
</ion-header>

<ion-content padding>  
    <amplify-authenticator framework="ionic"></amplify-authenticator>
</ion-content>

<ion-footer no-border *ngIf="this.authState.signedIn">  
  <ion-toolbar color="secondary" text-center>
    <ion-button fill="outline" color="light" (click)="showDecks()">
        My Decks
      <ion-icon slot="end" name="albums"></ion-icon>
    </ion-button>
  </ion-toolbar>
</ion-footer>  

We're using the <amplify-authenticator> component here from the aws-amplify-angular package. This component contains the forms for sign in, sign up, forgot password, etc.

Now, we need to change login.page.ts to listen for changes in the authenticator component. This component will communicate with the AmplifyService and let us know when the user is signed in or out.

import { Component, AfterContentInit } from '@angular/core';  
import { Events } from '@ionic/angular';  
import { AmplifyService }  from 'aws-amplify-angular';  
import { Router } from '@angular/router';

@Component({
  selector: 'app-login',
  templateUrl: './login.page.html',
  styleUrls: ['./login.page.scss'],
})
export class LoginPage { 

  authState: any;

  constructor(
    public events: Events,
    public amplifyService: AmplifyService,
    public router: Router
  ) {
    this.authState = { signedIn: false };

    this.amplifyService.authStateChange$
      .subscribe(authState => {
        this.authState.signedIn = authState.state === 'signedIn';
        this.events.publish('data:AuthState', this.authState);
      });
    }

    showDecks() {
      this.router.navigate(['/home']);
    }
}

We're using Ionic's built-in Events service to publish these changes to a Route Guard so only authenticated users can access the rest of the app.

Add authentication guard

In order to restrict access to the rest of the app, we will use an Angular Route Guard.

This guard will let the router know whether it can allow navigation to a specific route. The interface for the guard is very simple, it just needs to implement a function canActivate which needs to return a boolean value.

Let's create the service with the Ionic CLI.

$ ionic g service auth-guard

Add the following code to auth-guard.service.ts:

import { Injectable } from '@angular/core';  
import { CanActivate, Router } from '@angular/router';  
import { Events } from '@ionic/angular'

@Injectable({
  providedIn: 'root'
})
export class AuthGuardService implements CanActivate {

  signedIn = false;

  constructor(public router: Router, public events: Events) {
    this.events.subscribe('data:AuthState', async (data) => {
      this.signedIn = data.signedIn;
    })
  }

  canActivate() {
    if (!this.signedIn) {
      this.router.navigate(['/login']);
    }
    return this.signedIn;
  }
}

Now we can let the router know on which routes it needs to check our guard.

Modify app.routing.module.ts to enable the guard:

const routes: Routes = [  
  { path: '', redirectTo: 'home', pathMatch: 'full' },
  { path: 'home', loadChildren: './home/home.module#HomePageModule', canActivate: [AuthGuardService] },
  { path: 'cards/:id', loadChildren: './cards/cards.module#CardsPageModule', canActivate: [AuthGuardService] },
  { path: 'login', loadChildren: './login/login.module#LoginPageModule' },
];

We're now done with the implementation of the authentication and authorization features. The only thing left to do now is change the styling of the <amplify-authenticator> component to match the styling of our Ionic app.

Change styling

The default styles for the <amplify-authenticator> component are located in this file: node_modules/aws-amplify-angular/theme.css.

You can import it into your global.scss to use the default styles. All it does is import 2 other stylesheets from the @aws-amplify/ui package.

@import "~@aws-amplify/ui/src/Theme.css";
@import "~@aws-amplify/ui/src/Angular.css";

However, I needed to change some of the styling to make it match with the other pages of the app.

In order to do that, I made a copy of the file /node_modules/@aws-amplify/ui/src/theme.css. I put my copy in src/theme/amplify.scss and changed some of the colors:

/** Angular Theme **/
--color-primary: #0bb8cc;
--color-primary-highlight: #0cd1e8;

/* Ionic Theme */

/** primary **/
--ion-color-primary: #0cd1e8;
--ion-color-primary-rgb: 12,209,232;
--ion-color-primary-contrast: #ffffff;
--ion-color-primary-contrast-rgb: 255,255,255;
--ion-color-primary-shade: #0bb8cc;
--ion-color-primary-tint: #24d6ea;

I also deleted the rest of the Ionic styling for the non-primary colors. You can see the entire file here: amplify.scss.

Make sure you add the following imports to global.scss to use these styles in the <amplify-authenticator> component:

@import "~@aws-amplify/ui/src/Angular.css";
@import "theme/amplify.scss"; 

You can make further style changes by overriding the styles in node_modules/aws-amplify/ui/src/Angular.css as well.

Test the app

Run ionic serve to start the app. Your login page should look like below.

Login Page

Click on the Create account link and sign up for an account. You will receive an email with a verification code. After verification you'll be signed in and you'll be able to view your decks.

Since we didn't add any functionality to add decks in the app we'll go into the AWS Console and add add a deck with a GraphQL mutation.

Sign in to the AWS Console.

Clear tables in DynamoDB

First, let's delete all the cards and decks in DynamoDB since these aren't associated with any user.

Go to Services > DynamoDB > Tables and select your Deck table. Go to Items and select all items and then go to Actions > Delete. Repeat this for the Cards table.

Sign in with user pool in AppSync

Go to Services > AWS AppSync, select your API and go to Queries.

You can now only run queries if you sign in with a user. We can do that by clicking on the button Login with User Pools on the top (next to the orange play button).

Login with User Pools

In the first field we need to input the User pool Client ID, you can find this in your aws-exports.js settings under aws_user_pools_web_client_id.

Next, sign in with the username and password for the user you just created in the app. After you've signed in, you can create a new deck with this mutation.

mutation addDeck {  
  createDeck(input: {
    name: "Ionic"
  }) {
   id 
  }
}

Now go back to the Ionic app and you'll see that this deck is displayed and you can now go ahead and create cards within the app.

If you go back into the DynamoDB tables, you'll see that each item has a field owner which contains the username. This is used to restrict access to the items as we've defined in our GraphQL schema.

Inspect users in Cognito

Go to Services > Cognito and click on Manage User Pools. You should see the user pool that was created by the Amplify CLI and when you click on it, you can inspect the users and other settings there.

You can find the source code for this tutorial on my Github.

What's next?

In the next part, we are going to have a look at ElasticSearch and how we can use that to search data through our GrahpQL API.

References

Building Ionic 4 apps with AWS Amplify
Angular Authentication: Using Route Guards