DEV Community

Cover image for Amplication & Angular: Frontend Authentication
Michael Solati for Amplication

Posted on • Updated on • Originally published at docs.amplication.com

Amplication & Angular: Frontend Authentication

Welcome to this tutorial on how to build a full-stack application with Amplication.

What we will do is go step by step to create a Todos application using Angular for your frontend and Amplication for your backend.

If you get stuck, have any questions, or just want to say hi to other Amplication developers like yourself, then you should join our Discord!


Table of Contents

  • Step 1 - Add HttpClientModule
  • Step 2 - Authorization Requests
  • Step 3 - The Auth Component
  • Step 4 - Login
  • Step 5 - Wrap Up

Step 1 - Add HttpClientModule

To allow users to sign in to the Todos application we'll need to prompt them for the username and password and then verify it with the backend. To make the HTTP request to the backend we'll use the Angular HttpClientModule. First open web/src/app/app.module.ts and add import the HttpClientModule:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { ReactiveFormsModule } from '@angular/forms';
+ import { HttpClientModule } from '@angular/common/http';
Enter fullscreen mode Exit fullscreen mode

Then add the HttpClientModule to the imports in the @NgModule decorator:

@NgModule({
   declarations: [
      AppComponent,
      TaskComponent,
      TasksComponent,
      CreateTaskComponent
   ],
   imports: [
      BrowserModule,
      ReactiveFormsModule,
+      HttpClientModule
   ],
   providers: [],
   bootstrap: [AppComponent]
})
export class AppModule { }
Enter fullscreen mode Exit fullscreen mode

We'll want to abstract some variables, such as our API url, into a reusable resource. In web/src/environments/environment.ts and web/src/environments/environment.prod.ts add the following properties to the environment export:

export const environment = {
   production: false,
+   apiUrl: 'http://localhost:3000',
+   jwtKey: 'accessToken',
};
Enter fullscreen mode Exit fullscreen mode

We'll want to configure the Angular HttpClientModule to use a user's access token when making requests to the backend and have easy access to the axios library, so we'll need to set up an interceptor as well as some other functions. In your terminal navigate to the web directory and run:

ng g s JWT
Enter fullscreen mode Exit fullscreen mode

Then replace the content of the generated file (web/src/app/jwt.service.ts) with the following code:

import { Injectable } from '@angular/core';
import {
   HttpInterceptor,
   HttpEvent,
   HttpRequest,
   HttpHandler,
} from '@angular/common/http';
import { Observable } from 'rxjs';
import { environment } from '../environments/environment';

@Injectable({
   providedIn: 'root',
})
export class JWTService implements HttpInterceptor {
   get jwt(): string {
      return localStorage.getItem(environment.jwtKey) || '';
   }

   set jwt(accessToken: string) {
      localStorage.setItem(environment.jwtKey, accessToken);
   }

   get isStoredJwt(): boolean {
      return Boolean(this.jwt);
   }

   intercept(
      request: HttpRequest<any>,
      next: HttpHandler
   ): Observable<HttpEvent<any>> {
      if (request.url.startsWith(environment.apiUrl)) {
         request = request.clone({
            setHeaders: { Authorization: `Bearer ${this.jwt}` },
         });
      }

      return next.handle(request);
   }
}
Enter fullscreen mode Exit fullscreen mode

Now every request that the Angular HttpClientModule makes will take the user's JWT access token, which will be stored in local storage, and assign it to the Authorization header of every request.

In addition we've added a getter that checks if an access token already exists in local storage and a setter to save an access token in local storage.

Finally we'll need to configure the JWTService in the AppModule. Open web/src/app/app.module.ts and import JWTService and HTTP_INTERCEPTORS:

- import { HttpClientModule } from '@angular/common/http';
+ import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';

+ import { JWTService } from './jwt.service';

import { AppComponent } from './app.component';
Enter fullscreen mode Exit fullscreen mode

Then add and configure the JWTService in the providers of the @NgModule decorator:

-  providers: [],
+  providers: [
+     { provide: HTTP_INTERCEPTORS, useClass: JWTService, multi: true },
+  ],
   bootstrap: [AppComponent]
})
export class AppModule { }
Enter fullscreen mode Exit fullscreen mode

Step 2 - Authorization Requests

Instead of calling our API endpoints directly from our components, we will abstract the logic of the requests so if we ever need to make changes to the behavior of the code we can do it in just one place.

In your terminal navigate to the web directory and run:

ng g s auth
Enter fullscreen mode Exit fullscreen mode

And at the top of the newly created file (web/src/app/auth.service.ts), we'll import the JWTService and HttpClient and some other dependencies.

import { Injectable } from '@angular/core';
+ import { HttpClient } from '@angular/common/http';
+ import { of } from 'rxjs';
+ import { catchError, mergeMap } from 'rxjs/operators';
+ import { JWTService } from './jwt.service';
+ import { environment } from '../environments/environment';
Enter fullscreen mode Exit fullscreen mode

In the AuthService set the JWTService and HttpClient as arguments for the constructor:

export class AuthService {
   constructor(private http: HttpClient, private jwt: JWTService) { }
}
Enter fullscreen mode Exit fullscreen mode

Now, add the me method:

me() {
   const url = new URL('/api/me', environment.apiUrl).href;
   return this.jwt.isStoredJwt
      ? this.http.get(url).pipe(catchError(() => of(null)))
      : of(null);
}
Enter fullscreen mode Exit fullscreen mode

me will check if we have an access token stored, because if there is none then there is no way this request would succeed. If the token exists, it will make a GET request to the /api/me endpoint we created in Tutorial Step 3. On the success of the request, the current user's user object will be returned.

Next, add the login method:

login(username: string, password: string) {
   const url = new URL('/api/login', environment.apiUrl).href;
   return this.http
      .post(url, {
         username,
         password,
      })
      .pipe(
         catchError(() => of(null)),
         mergeMap((result: any) => {
            if (!result) {
               alert('Could not login');
               return of();
            }
            this.jwt.jwt = result.accessToken;
            return this.me();
         })
      );
}
Enter fullscreen mode Exit fullscreen mode

login will make a POST request to the /api/login endpoint, sending the username and password of our user. If the request fails, like when a user doesn't exist, an alert will pop up notifying the user of the failure. If the request succeeds the access token will be saved into local storage, and then the me function will be called to return the current user's user object.

Then, add the signup method:

signup(username: string, password: string) {
   const url = new URL('/api/signup', environment.apiUrl).href;
   return this.http
      .post(url, {
         username,
         password,
      })
      .pipe(
         catchError(() => of(null)),
         mergeMap((result: any) => {
            if (!result) {
               alert('Could not sign up');
               return of();
            }
            this.jwt.jwt = result.accessToken;
            return this.me();
         })
      );
}
Enter fullscreen mode Exit fullscreen mode

signup will make a POST request to the /api/signup endpoint, which we also created in Tutorial Step 3, sending the username and password of our new user. If the request fails, like if the username is already used, an alert will pop up notifying the user of the failure. If the request succeeds the access token will be saved into local storage, and then the me function will be called to return the current user's user object.

Finally we'll need to add the AuthService to the AppModule. Open web/src/app/app.module.ts and import AuthService:

+ import { AuthService } from './auth.service';
import { JWTService } from './jwt.service';
Enter fullscreen mode Exit fullscreen mode

Then add and configure the AuthService to the providers in the @NgModule decorator:

   providers: [
      { provide: HTTP_INTERCEPTORS, useClass: JWTService, multi: true },
+      AuthService,
   ],
   bootstrap: [AppComponent]
})
export class AppModule { }
Enter fullscreen mode Exit fullscreen mode

Step 3 - The Auth Component

We need a component that can collect the username and password from the user and then make the appropriate request with the functions we just added. In your terminal navigate to the web directory and run:

ng g c auth
Enter fullscreen mode Exit fullscreen mode

Open the following files and replace the contents of those files with the following:

web/src/app/auth/auth.component.ts

import { Component, Output, EventEmitter } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { AuthService } from '../auth.service';

@Component({
   selector: 'app-auth',
   templateUrl: './auth.component.html',
   styleUrls: ['./auth.component.css'],
})
export class AuthComponent {
   @Output() setUser = new EventEmitter<string>();
   authForm = this.fb.group({
      username: '',
      password: '',
      confirm: '',
   });
   isLogin = true;

   constructor(private fb: FormBuilder, private auth: AuthService) {}

   onSubmit() {
      const { username, password, confirm }: { [key: string]: string } =
         this.authForm.getRawValue();

      if (!username || !password) return;

      let authResult;

      if (!this.isLogin && password !== confirm) {
         return alert('Passwords do not match');
      } else if (!this.isLogin) {
         authResult = this.auth.signup(username.toLowerCase(), password);
      } else {
         authResult = this.auth.login(username.toLowerCase(), password);
      }

      authResult.subscribe({ next: (result: any) => this.setUser.emit(result) });
   }
}
Enter fullscreen mode Exit fullscreen mode

web/src/app/auth/auth.component.html

<form [formGroup]="authForm" (ngSubmit)="onSubmit()">
   <h2>{{isLogin ? "Login" : "Sign Up"}}</h2>
   <input name="username" type="text" placeholder="username" formControlName="username" required />
   <input name="password" type="password" placeholder="password" formControlName="password" required />
   <input *ngIf="!isLogin" name="confirmPassword" type="password" placeholder="confirm password"
    formControlName="confirm" required />

   <button type="submit">Submit</button>
   <button type="button" (click)="isLogin = !isLogin">
      {{isLogin ? "Need an account?" : "Already have an account?"}}
   </button>
</form>
Enter fullscreen mode Exit fullscreen mode

This component renders a form to the user prompting them for their username and password to log in. If they don't have an account yet then a button on the bottom of the page will toggle the form to be for creating a new account, which adds a new field for a user to confirm their password.

On submit the login or signup method from the AuthService is called, and the result is bubbled up by the @Output() setUser event emitter.

Step 4 - Login

With the authentication component created we just need to show it to users. Start by adding a user property to the AppComponent in web/src/app/app.component.ts like:

export class AppComponent {
   tasks: any[] = [];
+   user: any;
Enter fullscreen mode Exit fullscreen mode

Next we will add a method to the AppComponent to set the user property. While we could directly set the value, we will eventually want to trigger some code when a user is set, so we implement it this way.

setUser(user: any) {
   this.user = user;
}
Enter fullscreen mode Exit fullscreen mode

Then update the AppComponent's template (web/src/app/app.component.html) to look like this:

<ng-container *ngIf="user; else auth">
   <app-create-task (addTask)="addTask($event)"></app-create-task>
   <app-tasks [tasks]="tasks" (completed)="completed($event)"></app-tasks>
</ng-container>

<ng-template #auth>
   <app-auth (setUser)="setUser($event)"></app-auth>
</ng-template>
Enter fullscreen mode Exit fullscreen mode

So, at the top level of the component's template we have two sibling elements, <ng-container> and <ng-template>. The behavior of <ng-container> is much like how <> is used in React, where we are holding elements without adding any extra elements to the DOM. The <ng-container> is displayed if the user property exists in the AppComponent, otherwise the content of the <ng-template> is shown. Inside <ng-template> we've added the app-auth element. When the app-auth element (AuthComponent) emits a setUser event the user property of the AppComponent is assigned by it's setUser method. If there is a user value then we'll toggle the template to show the todo list.

User's aren't expected to log in every time, especially considering we're storing the user's JWT access token. We'll update the AppComponent to call the me method of the AuthService when the component initiates. That way we can assign the user property as soon as possible.

Start by importing OnInit and AuthService, and then set the AppComponent to implement the OnInit lifecycle hook.

- import { Component } from '@angular/core';
+ import { Component, OnInit } from '@angular/core';
+ import { AuthService } from './auth.service';

@Component({
   selector: 'app-root',
   templateUrl: './app.component.html',
   styleUrls: ['./app.component.css']
})
- export class AppComponent {
+ export class AppComponent implements OnInit {
Enter fullscreen mode Exit fullscreen mode

Next add a constructor where the AuthService is set as the only argument.

constructor(private auth: AuthService) {}
Enter fullscreen mode Exit fullscreen mode

Then add this implementation of the OnInit lifecycle hook:

ngOnInit(): void {
   this.auth.me().subscribe({ next: (user) => (this.user = user) });
}
Enter fullscreen mode Exit fullscreen mode

Now if the user property has a value, which only occurs when they're logged in, the application will show the user's tasks. If the user property doesn't have a value they are shown the auth screen, which when a user logs in or signs up, will set the user property with the setUser event of the app-auth element (AuthComponent).

Step 5 - Wrap Up

Run the application and try creating a new account.

a working todo list app

Check back next week for step five, or visit the Amplication docs site for the full guide now!

To view the changes for this step, visit here.

Discussion (0)