How To Build A Full Stack Web App Using AWS Amplify | Adding Backend And Frontend Authentication

November 27, 2020

How To Build A Full Stack Web App Using AWS Amplify | Adding Backend And Frontend Authentication

In our previous blog post, we tackled app presentation and how to use CoreUI to add an admin template to your app. At this point, you should have a strong app architecture, but as we all well know a chain is only as strong as its weakest link, which in this case refers to your app security vulnerabilities. Authentication and authorization problems are consistently ranked amongst the top three most critical security risks to web applications in the OWASP Top 10, thus to mitigate these risks we’ll be guiding you through the process of adding backend and frontend authentication to your application. 

Preparation

Before we can tackle authentication, we first need to create a new branch and environment where changes can safely be made. In your project folder, type the following command:

cd [project_folder] 
git checkout develop 
git checkout -b feature/auth 

This command will create a new git branch called feature/auth and switch context to that branch. Next we’ll create a new AWS environment where we can add and configure the backend authentication. Type the following command in the project root:

	amplify env add

At this point, you’ll be asked to answer a few questions. Simply copy the answers below and Amplify will start doing its magic to create the necessary CloudFormation code to build our new app environment.

Q: Do you want to use an existing AWS environment (Y/N)?

A: No. (We want to create a new environment where we can add and update cloud services without affecting our current working environment.

Q: Enter a name for the environment

A: auth

Q: Do you want to use an AWS profile?

A: Yes. (We’ll use the profile created in the configuration step).

Q: Please choose the profile you want to use

A: scribe

Adding Backend Authentication

We’ll be using AWS Cognito to add the authentication service to our app. We’ll be running the following command to add the authentication service to our new environment:

amplify add auth 

The command will trigger a set of configuration questions:

Q: How do you want users to be able to sign in

A: Username

Q: Do you want to configure advanced settings?

A: No, I am done. (For the time being we’ll just be going with the basic username option).

If you’re still up to speed, running the amplify status will display the following:

Amplify has added a folder (amplify/backend/auth/) to our project to keep the backend auth configuration settings. For the moment this is only a local change. Lets commit the changes to git and push the auth service into the cloud.

git add . 
git commit -am "Add auth service." 
git push --set-upstream origin feature/auth 
 
amplify push 

When asked if you want to continue, answer “yes” as this will push the changes through to AWS.

Configure Amplify In Angular

To integrate the frontend let’s install the aws-amplify and aws-amplify-angular packages. Aws-amplify is the AWS Amplify core library, while Aws-amplify-angular is one of the AWS Amplify library packages which provides building blocks for Angular App development.

npm install aws-amplify-angular 

If you are using an Angular 6+ version add the following code to the polofill.ts file.

(window as any).global = window; 
(window as any).process = { 
  env: { DEBUG: undefined }, 
}; 

Add “node” as a type to the compilerOptions.

// src/tsconfig.app.json 
"compilerOptions": { 
  ... 
  "types" : ["node"], 
  ... 
} 

When adding the backend authentication service using AWS Cognito, Amplify creates a file called aws-exports.js in the src directory. This file contains all the configuration needed for your angular app to connect to the AWS Cognito service. To implement this in your application add the following to your main.ts file:

	// src/main.ts
	import Amplify, { Auth } from 'aws-amplify';
	import amplify from './aws-exports';
	 
	Amplify.configure(amplify);

At this point we’ve configured Amplify and can use it in the rest of our Angular application. Add the following to include Amplify in the application:

// src/app/app.module.ts 
 
... 
import { AmplifyAngularModule, AmplifyService } from 'aws-amplify-angular'; 
... 
 
@NgModule({ 
  ... 
  imports: [ 
    ... 
    AmplifyAngularModule 
    ... 
  ], 
  providers: [ 
    ... 
    AmplifyService 
    ... 
  ] 
  ... 
}) 

Adding Frontend Authentication

In the previous segment there were two views included when we added CodeUI to our application, login and register views. Building on these views, we’ll register a new user first. 

Registration

Let’s create two components here to handle the registration flow; one component for registering and one for confirming the registration. AWS Cognito has built-in functionality for registering and confirming the account with a confirmation code, which is automatically sent to the account’s email address. To create the register form component, execute the following command in the root of the project: 

ng g component components/register-form

This will create a register form component at src/app/components/register-form.

Copy the following code into src/app/components/register-form/register-form.component.ts:

import { Component, EventEmitter, OnInit, Output } from '@angular/core'; 
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; 
import { Auth } from 'aws-amplify'; 
import { Router } from '@angular/router'; 

@Component({ 
  selector: 'app-register-form', 
  templateUrl: './register-form.component.html', 
  styleUrls: ['./register-form.component.css'] 
}) 
export class RegisterFormComponent implements OnInit { 
  registerForm: FormGroup; 
  submitted = false; 

  @Output() cognitoUser = new EventEmitter(); 

  constructor( 
    private formBuilder: FormBuilder, 
    private _router: Router 
  ) { } 

  ngOnInit() { 
    this.registerForm = this.formBuilder.group({ 
      email: ['', [Validators.required, Validators.email]], 
      password: ['', [Validators.required, Validators.minLength(6)]], 
      confirmPassword: ['', Validators.required], 
    }); 
  } 

  // Convenient getter for easy access to form fields 
  get f() { return this.registerForm.controls; } 

  onSubmit() { 
    this.submitted = true; 

    // stop here if form is invalid 
    if (this.registerForm.invalid) { 
        return; 
    } 
  
    // Call the Cognito signUp method 
    Auth.signUp({ 
      "username": this.registerForm.get('email').value, 
      "password": this.registerForm.get('password').value, 
      "attributes": { 
        "email": this.registerForm.get('email').value 
      } 
    }) 
    .then(data => { 
      console.log(data); 
    
      // Emit the successful response from Cognito 
      // This will be picked up by the view in order to show the 
      // confirm form. 
      this.cognitoUser.emit(data); 
    }) 
    .catch((error: any) => { 
      console.log(error); 
      
      switch (error.code) { 
        case 'UsernameExistsException': 
          this._router.navigate(['login']); 
          break; 
      } 
    }); 
  } 
} 

That takes care of the component. Add the below code to the src/app/components/register-form/register-form.component.html:

	<div class="app-body">
		<main class="main d-flex align-items-center">
			<div class="container">
				<div class="row">
					<div class="col-md-12 mx-auto">
						<form [formGroup]="registerForm" (ngSubmit)="onSubmit()">
							<div class="card mx-4">
								<div class="card-body p-4">
									<h1>Register</h1>
									<p class="text-muted">Create your account</p>
									<div class="input-group mb-3">
										<div class="input-group-prepend">
											<span class="input-group-text">@</span>
										</div>
										<input type="text" formControlName="email" class="form-control" placeholder="Email" [ngClass]="{ 'is-invalid': submitted && f.email.errors }" />
										<div *ngIf="submitted && f.email.errors" class="invalid-feedback">
											<div *ngIf="f.email.errors.required">Email is required</div>
											<div *ngIf="f.email.errors.email">Email must be a valid email address</div>
										</div>
									</div>
									<div class="input-group mb-3">
										<div class="input-group-prepend">
											<span class="input-group-text"><i class="icon-lock"></i></span>
										</div>
										<input type="password" formControlName="password" class="form-control" placeholder="Password" [ngClass]="{ 'is-invalid': submitted && f.password.errors }" />
										<div *ngIf="submitted && f.password.errors" class="invalid-feedback">
											<div *ngIf="f.password.errors.required">Password is required</div>
											<div *ngIf="f.password.errors.minlength">Password must be at least 6 characters</div>
										</div>
									</div>
									<div class="input-group mb-4">
										<div class="input-group-prepend">
											<span class="input-group-text"><i class="icon-lock"></i></span>
										</div>
										<input type="password" formControlName="confirmPassword" class="form-control" placeholder="Confirm Password" [ngClass]="{ 'is-invalid': submitted && f.confirmPassword.errors }" />
										<div *ngIf="submitted && f.confirmPassword.errors" class="invalid-feedback">
											<div *ngIf="f.confirmPassword.errors.required">Confirm Password is required</div>
											<div *ngIf="f.confirmPassword.errors.mustMatch">Passwords must match</div>
										</div>
									</div>
									<button type="submit" class="btn btn-block btn-success">Create Account</button>
								</div>
							</div>
						</form>
					</div>
				</div>
			</div>
		</main>
	</div>

This takes care of the view which contains a form with three fields (username, password, confirmPassword). We’ll be using an email for the username and pass that to AWS Cognito as the username and email for the new user.

Confirm Form Component

Create the confirm form component by executing the following command in the root of the project.

ng g component components/confirm-form 

This will create a confirm form component at src/app/components/confirm-form.

Copy the following code into src/app/components/confirm-form/confirm-form.component.ts:

import { Component, Input, OnInit } from '@angular/core'; 
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; 
import { Auth } from 'aws-amplify'; 
import { Router } from '@angular/router'; 

@Component({ 
  selector: 'app-confirm-form', 
  templateUrl: './confirm-form.component.html', 
  styleUrls: ['./confirm-form.component.css'] 
}) 
export class ConfirmFormComponent implements OnInit { 
  confirmForm: FormGroup; 
  submitted = false; 

  @Input() email: string; 

  constructor( 
    private formBuilder: FormBuilder, 
    private _router: Router 
  ) { } 

  ngOnInit() { 
    this.confirmForm = this.formBuilder.group({ 
      email: [this.email, [Validators.required]], 
      confirmation_code: ['', [Validators.required]] 
    }); 
  } 

  get f() { return this.confirmForm.controls; } 

  onSubmit() { 
    this.submitted = true; 

    // stop here if form is invalid 
    if (this.confirmForm.invalid) { 
        return; 
    } 

    Auth.confirmSignUp(this.confirmForm.get('email').value, this.confirmForm.get('confirmation_code').value) 
    .then(data => { 
      console.log(data); 
      
      this._router.navigate(["login"]); 
    }) 
    .catch((error: any) => { 
      console.log(error); 
      
      switch (error.code) { 
        case 'UsernameExistsException': 
          break; 
      } 

    }); 
  } 
} 

That takes care of the confirm form component code. Add the below code to the src/app/components/register-form/register-form.component.html:

  <div class="app-body">
    <main class="main d-flex align-items-center">
      <div class="container">
        <div class="row">
          <div class="col-md-12 mx-auto">
            <form [formGroup]="registerForm" (ngSubmit)="onSubmit()">
              <div class="card mx-4">
                <div class="card-body p-4">
                  <h1>Register</h1>
                  <p class="text-muted">Create your account</p>
                  <div class="input-group mb-3">
                    <div class="input-group-prepend">
                      <span class="input-group-text">@</span>
                    </div>
                    <input type="text" formControlName="email" class="form-control" placeholder="Email" [ngClass]="{ 'is-invalid': submitted && f.email.errors }" />
                    <div *ngIf="submitted && f.email.errors" class="invalid-feedback">
                      <div *ngIf="f.email.errors.required">Email is required</div>
                      <div *ngIf="f.email.errors.email">Email must be a valid email address</div>
                    </div>
                  </div>
                  <div class="input-group mb-3">
                    <div class="input-group-prepend">
                      <span class="input-group-text"><i class="icon-lock"></i></span>
                    </div>
                    <input type="password" formControlName="password" class="form-control" placeholder="Password" [ngClass]="{ 'is-invalid': submitted && f.password.errors }" />
                    <div *ngIf="submitted && f.password.errors" class="invalid-feedback">
                      <div *ngIf="f.password.errors.required">Password is required</div>
                      <div *ngIf="f.password.errors.minlength">Password must be at least 6 characters</div>
                    </div>
                  </div>
                  <div class="input-group mb-4">
                    <div class="input-group-prepend">
                      <span class="input-group-text"><i class="icon-lock"></i></span>
                    </div>
                    <input type="password" formControlName="confirmPassword" class="form-control" placeholder="Confirm Password" [ngClass]="{ 'is-invalid': submitted && f.confirmPassword.errors }" />
                    <div *ngIf="submitted && f.confirmPassword.errors" class="invalid-feedback">
                      <div *ngIf="f.confirmPassword.errors.required">Confirm Password is required</div>
                      <div *ngIf="f.confirmPassword.errors.mustMatch">Passwords must match</div>
                    </div>
                  </div>
                  <button type="submit" class="btn btn-block btn-success">Create Account</button>
                </div>
              </div>
            </form>
          </div>
        </div>
      </div>
    </main>
  </div>

This takes care of the confirm form view which contains a form with two fields (email, confirmation_code). The email will be returned from the registration code as part of the AWS Cognito signup response. The confirmation code, which is sent on signup, will need to be entered in the form in order to confirm the registration.We need to update the register view which was added to our application in the CodeUI step. Override the register component with the following code:

// src/app/views/register/register.component.ts 

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

@Component({ 
  selector: 'app-dashboard', 
  templateUrl: 'register.component.html' 
}) 
export class RegisterComponent { 

  cognitoUser: any; 

  constructor() { } 

  onRegister(cognitoUser: any) { 
    this.cognitoUser = cognitoUser;
  } 
} 
	// src/app/views/register/register.component.html
	 
	<div class="app-body">
		vmain class="main d-flex align-items-center">
			<div class="container">
				<div class="row">
					<div class="col-md-6 mx-auto">
						<app-register-form (cognitoUser)='onRegister($event)' *ngIf="!cognitoUser"></app-register-form>
	 
						<app-confirm-form [email]="cognitoUser.user.username" *ngIf="cognitoUser && !cognitoUser.userConfirmed"></app-confirm-form>
					</div>
				</div>
			</div>
		</main>
	</div>

At this point we’ve registered a user and confirmed the registration. The next thing we need to do is to hook up the login form to make use of AWS Cognito to authenticate the newly registered user.

Login

To create the login form component, execute the following command in the root of the project:

ng g component components/login-form 

This will create a login form component at src/app/components/login-form.

Copy the following code into src/app/components/login-form/login-form.component.ts

import { Component, OnInit } from '@angular/core'; 
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; 
import { Auth } from 'aws-amplify'; 
import { Router } from '@angular/router'; 

@Component({ 
  selector: 'app-login-form', 
  templateUrl: './login-form.component.html', 
  styleUrls: ['./login-form.component.css'] 
})
export class LoginFormComponent implements OnInit { 
  loginForm: FormGroup; 
  submitted = false; 

  constructor( 
    private formBuilder: FormBuilder, 
    private _router: Router 
  ) { } 

  ngOnInit() { 
    this.loginForm = this.formBuilder.group({ 
      username: ['', [Validators.required]], 
      password: ['', [Validators.required]] 
    }); 
  } 

  get f() { return this.loginForm.controls; } 

  onSubmit() { 
    this.submitted = true; 

    // stop here if form is invalid 
    if (this.loginForm.invalid) { 
        return; 
    } 

    Auth.signIn(this.loginForm.get('username').value, this.loginForm.get('password').value) 
      .then((data: any) => { 
        console.log(data); 
        
        this._router.navigate(['dashboard']) 
      }) 
      .catch((error: any) => { 
        console.log(error); 
      }); 
  } 
} 

Add the following code to the login form component’s html file to create the view for the component.

// components/login-form/login-form.component.html 
<form [formGroup]="loginForm" (ngSubmit)="onSubmit()"> 
  <h1>Login</h1> 
  <p class="text-muted">Sign In to your account</p> 
  <div class="input-group mb-3"> 
    <div class="input-group-prepend"> 
      <span class="input-group-text"><i class="icon-user"></i></span> 
    </div> 
    <input type="text" formControlName="username" class="form-control" placeholder="Username" [ngClass]="{ 'is-invalid': submitted && f.username.errors }" /> 
    <div *ngIf="submitted && f.username.errors" class="invalid-feedback"> 
      <div *ngIf="f.username.errors.required">Username is required</div> 
    </div> 
  </div> 
  <div class="input-group mb-4"> 
    <div class="input-group-prepend"> 
      <span class="input-group-text"><i class="icon-lock"></i></span> 
    </div> 
    <input type="password" formControlName="password" class="form-control" placeholder="Password" [ngClass]="{ 'is-invalid': submitted && f.password.errors }" /> 
    <div *ngIf="submitted && f.password.errors" class="invalid-feedback"> 
      <div *ngIf="f.password.errors.required">Password is required</div> 
    </div> 
  </div> 
  <div class="row"> 
    <div class="col-6"> 
      <button type="submit" class="btn btn-primary px-4">Login</button> 
    </div> 
    <div class="col-6 text-right"> 
      <button type="button" class="btn btn-link px-0">Forgot password?</button> 
    </div> 
  </div> 
</form> 

Now we want to make user of this component within the Login View which exists inside the CodeUI files we added. To do this we need to override the Login View as below:

// views/login/login.component.html
 
<div class="app-body"> 
  <main class="main d-flex align-items-center"> 
    <div class="container"> 
      <div class="row"> 
        <div class="col-md-8 mx-auto"> 
          <div class="card-group"> 
            <div class="card p-4"> 
              <div class="card-body"> 
                <app-login-form ></app-login-form> 
              </div> 
            </div> 
            <div class="card text-white bg-primary py-5 d-md-down-none" style="width:44%"> 
              <div class="card-body text-center"> 
                <div> 
                  <h2>Sign up</h2> 
                  <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p> 
                  <button type="button" class="btn btn-primary active mt-3">Register Now!</button> 
                </div> 
              </div> 
            </div> 
          </div> 
        </div> 
      </div> 
    </div> 
  </main> 
</div> 

Wrapping Up

Run the following command to add all the new files added during this topic and commit it to your git repository:

git add . 
git commit -am "Add authentication" 

Now we want to merge the code into our develop branch to bring all the authentication updates into our develop branch:

git checkout develop 
git merge --no-ff feature/auth 

Checkout the Amplify dev environment:

amplify env checkout dev 

Run amplify status to see the status of the current environment:

amplify status 

At this point, you should be able to see the following:

Run the following command to deploy the changes to the dev environment:

amplify push 

When asked if you are sure that you want to continue, answer “yes”. After this step, we’ll remove the resources from the feature branch by running the following command:

amplify env remove auth 

Testing

After following the steps outlined above, we can go ahead and test our app authentication features.

Enter your email address and password in the register form.

Successful registration will trigger an email containing the confirmation code:

Once you’ve confirmed your account registration you can login to the application. At the login screen, enter your email in the username field and enter your password. 

If you followed the steps correctly, successful login will direct you to the CoreUI dashboard. 

Where To From Here?

After adding backend and frontend authentication to your Angular application, there will still be a bit of work left such as handling incorrect login credentials and hooking up forgotten passwords, but the essential architecture will be in place. Keep an eye on our social media accounts or sign up for the Swipe iX newsletter to stay up to date with our latest tutorials, as we’ll be walking you through the process of guarding your application next. If you have any questions, get in touch, but for now we’ve concluded this tutorial on how to sign up and sign in, so for now - we’re signing out. 

Dirk Coetzer

Senior Engineer

Swipe iX Newsletter

Subscribe to our email newsletter for useful tips and valuable resources, sent out monthly.