Overview

This post builds on the last post Add JWT Token to Angular HTTP Requests Using NGRX and will ultimately turn into “Part 2” of a series; it probably should’ve been “Part 1” as it provides the actual login UI form and complimentary authentication API with a real JWT, but at the time of writing the first post I didn’t have a series planned…so what’s this post about?

Series Table of Contents

  1. Add JWT Token to Angular HTTP Requests Using NGRX

  2. Angular 7 Login and Registration with JWT Node Authentication

  3. Angular 7 Authenticated Route Guard

Here we’ll create lazy-loaded routes for both the login and register forms on the client using the same “Smart Container and Dumb / Presentation Component” pattern we leveraged last time. We’ll also continue to use the AddTokenHeaderHttpRequestInterceptor to automatically add the JWT to secure API requests. This time, however, we’ll only apply the token to true API requests (aka we won’t add the token to CSS, JS, HTML, etc) and API requests that are not auth (not login or register). Another change is removing the hardcoded JWT from the NGRX store as we’ll receive it from the API upon successful login or registration. Finally, we’ll look at some minor changes to our mock Node.js backend that allow us to login and register users and pass back JWTs when successful.

If you want to skip the explanation and just start playing with the app, head on over to the GitHub repo for the application.

Assumptions

This post assumes familiarity with Angular and Angular CLI, Angular Material, TypeScript, JWTs, RxJS, NGRX (or possibly Redux), Smart Container and Dumb / Presentation Component, Basic Angular Routing, Node.js, Express, and json-server.

At the time of writing this my system leveraged the following versions:

  • Node.js: 10.14.2

  • NPM: 6.4.1

  • Angular: 7.0.3

  • Angular CLI: 7.0.5

  • OS: MacOS High Sierra v10.13.6

The rest of the versions can easily be looked up in the package.json.

Code is not production grade and is ONLY meant for explanation purposes.

We’ll focus on a simplistic implementation for this example that doesn’t use a real database — all data is served from mock JSON files and/or kept in memory when generating new users via registration. To that end, this example is also light on security; there’s no real notion of a true JWT secret, token expiration, token refreshing, password hashing or salting, etc. This example provides a high-level, simplistic Node and Express authentication server and nothing more.

There’s also no form validation (post forthcoming) or routing guards (post forthcoming) on the client to prevent navigating to protected routes, which /beer should be since the API behind it is also protected. This was done to demonstrate that the API will return HTTP statuses of 401 Unauthorized for non-authenticated clients that are displayed in the UI. These features will be implemented in a future post.

Finally, the UI components leverage Angular Material to provide a base set of components and theming style — this allows us to focus less on the design and more on the functionality for this blog post.

Quick Start

Let’s clone the repo and fire up the app so we can see it in action. Open up a terminal and enter the following commands:

wasi$ git clone https://github.com/webappsolution/angular-add-auth-token-ngrx.git
wasi$ git checkout feature/step-1-login-and-register-client-and-server
wasi$ npm i
wasi$ npm run dev-auth

The last command will concurrently start both the server and client — the server uses json-server to quickly scaffold an in-memory JSON database, while the client is the Angular app running via Angular CLI.

Developers can also choose to run the client and sever in 2 separate terminals in case you like reading the console a bit easier:

wasi$ npm run client
wasi$ npm run server-auth

Next navigate to http://localhost:4300/login to see the application in action. It should display the login view:

angular-add-auth-token-ngrx-ui-login.jpg

The Client App

Let’s breakdown the new features within the client app one at a time. The key features on the client are new UI components for login and register as well as accompanying NGRX boilerplate and services to make the HTTP requests. We’ll quickly highlight the new views and review how they handle server errors for authentication and user registration.

Login and Register Components

The login and register components are individual features, so we’ll create them as lazily loaded routes each in their own feature directory as seen below.

login-register-feature-dir.png

Since both components are so similar and follow the same “Smart Container and Dumb Component” pattern we’ll just take a look at Login’s “Dumb Component” LoginComponent and “Smart Container” LoginContainer. The job of the LoginComponent is twofold:

  1. Display and present the view with rendered @Input() data.

  2. Handle any and all user interactions and bubble up the event to a smart container using @Output().

This facilitates easier testing and possible reusability of this component since it’s not tied to the rest of the app.

While the job of the LoginContainer is to provide the LoginComponent its input data and handle its output events — both of these usually require interaction with other parts of the app via injected services, a larger event bus system, or in our case the NGRX store’s action dispatcher and selector functions.

Let’s take a look at the flow starting from the lowest level of the LoginComponent and then move up to the LoginContainer.

Login.Component.HTML

We’ll call out a couple key points here for the login HTML template:

  1. The form’s submit or login button is disabled if the form is invalid (not currently hooked up) OR if the flag pending is true (indicating that the login HTTP request is currently inflight.

  2. There’s an error that might display if the login HTTP request fails.

  3. Clicking the login button fires off the backing component’s onLogin(event) method.

  4. Clicking the register button fires off the backing component’s onRegister(event) method.

<div class="container">
    <div class="card-wrapper">
        <mat-card class="mat-elevation-z1">
            <mat-card-header>
                <mat-card-title>
                    <h2>Login</h2>
                </mat-card-title>
            </mat-card-header>
            <form [formGroup]="loginForm">
                <mat-card-content>
                    <mat-form-field>
                        <input formControlName="username"
                               matInput
                               name="username"
                               placeholder="Username"
                               type="text">
                    </mat-form-field>
                    <mat-form-field>
                        <input formControlName="password"
                               matInput
                               name="password"
                               placeholder="Password"
                               type="password">
                    </mat-form-field>
                </mat-card-content>
                <mat-card-actions>
                    <button (click)="onLogin($event)"
                            [disabled]="loginForm.invalid || pending"
                            color="primary"
                            mat-raised-button
                            type="button">Login
                    </button>
                    <button (click)="onRegister($event)"
                            color="primary"
                            mat-raised-button
                            type="button">Register
                    </button>
                </mat-card-actions>
            </form>
            <div *ngIf="error">{{ error }}</div>
        </mat-card>
    </div>
</div>

login.component.ts

Looking at the login backing component LoginComponent we can see that it provides @Input() and @Output() decorators for each of the numbered bullet points from the HTML template and the code comments should provide enough detail to warrant omitting it from here in detail.

One key point to note is the use of ChangeDetectionStrategy.OnPush for the component that eliminates unnecessary change detection cycles on this component’s DOM. With this change detection strategy in place it’ll only fire when new, immutable data is pushed into the component via the @Input() or events are fired from the component via the @Ouput() decorators.

import {
    ChangeDetectionStrategy,
    Component,
    EventEmitter,
    Input,
    OnInit,
    Output
} from "@angular/core";
import {
    FormBuilder,
    FormGroup
} from "@angular/forms";
import { LoginCredentials } from "../../core/domain/auth.model";
import * as FormUtil from "../../util/form.util";

@Component({
    selector: "blog-login",
    templateUrl: "./login.component.html",
    styleUrls: [ "./login.component.scss" ],
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class LoginComponent implements OnInit {
    /**
     * An optional error message to display if login failed.
     */
    @Input()
    public error = "";

    /**
     * Flag indicating if login is pending.
     */
    @Input()
    public pending = false;

    /**
     * Dispatches an event to perform login.
     */
    @Output()
    public login: EventEmitter<LoginCredentials> = new EventEmitter<LoginCredentials>();

    /**
     * Dispatches an event to switch to the registration view.
     */
    @Output()
    public register: EventEmitter<void> = new EventEmitter<void>();

    /**
     * Reference to the login form.
     */
    public loginForm: FormGroup;

    /**
     * Constructor
     */
    constructor(private formBuilder: FormBuilder) {
    }

    /**
     * Initializes the component by building the form.
     *
     * TODO: BMR: 01/10/2019: Add form validation in a future post.
     */
    public ngOnInit(): void {
        this.loginForm = new FormGroup(
            this.formBuilder.group({
                username: [
                    ""
                    // [
                    //     Validators.required,
                    //     Validators.email
                    // ]
                ],
                password: [
                    ""
                    // [
                    //     Validators.required,
                    //     Validators.maxLength(ValidationUtil.VALIDATION_RULE.PASSWORD.MAX_LENGTH)
                    // ]
                ]
            }).controls,
            {
                updateOn: "blur"
            }
        );
    }

    /**
     * Accessor for the form's value, aka the data container object representing the
     * form field's current values.
     */
    public getFormValue(): LoginCredentials {
        return {
            username: FormUtil.getFormFieldValue(this.loginForm, "username"),
            password: FormUtil.getFormFieldValue(this.loginForm, "password")
        };
    }

    /**
     * Handles the form submission and emits a login event with the user's credentials.
     * @param event
     */
    public onLogin(event: any) {
        const payload: LoginCredentials = this.getFormValue();
        console.log(`onLogin( username: ${payload.username}, password: ${payload.password} )`);
        this.login.emit(payload);
    }

    /**
     * Emits an event to route the user to the registration view.
     * @param event
     */
    public onRegister(event: any) {
        console.log(`onRegister()`);
        this.register.emit();
    }
}

login.container.ts

Moving up the chain to the last layer in the login UI is the “Smart Container” LoginContainer. As previously mentioned, it’s responsible for accessing data from the NGRX store and passing it to the LoginComponent as well as listening to events from the LoginComponent and dispatching them as store actions. We’ll also make use of the async pipe when accessing NGRX store data to subscribe, unsubscribe and to unwrap observable data passed into the LoginComponent.

There are 2 additional small items of importance here:

  1. This container will serve as the UI element used when routing to the login feature as it encapsulates the LoginComponent.

  2. We’ll combine the HTML and TS backing component into one file here (instead of separating the two like we did with LoginComponent) for simplicity sake. I used to always separate the two but as IDE’s started to provide the ability to intelligently handle HTML templates within TS or JS files (and for the sake of simplicity) I’ve been combining them to eliminate the extra boilerplate files.

import {
    Component,
    OnInit
} from "@angular/core";
import {
    select,
    Store
} from "@ngrx/store";
import { Observable } from "rxjs";
import { LoginCredentials } from "../../core/domain/auth.model";
import * as fromState from "../../core/state/";
import * as AuthActions from "../../core/state/auth/auth.action";

@Component({
    selector: "blog-login-container",
    template: `
        <blog-login
                [error]="error$ | async"
                [pending]="pending$ | async"
                (login)="login($event)"
                (register)="register($event)"
        >
        </blog-login>
    `
})
export class LoginContainer implements OnInit {
    /**
     * The possible login error.
     */
    public error$: Observable<string>;

    /**
     * Flag indicating if login is pending.
     */
    public pending$: Observable<boolean>;

    /**
     * Constructor.
     */
    public constructor(private store$: Store<any>) {
    }

    /**
     * Initialize the component.
     */
    public ngOnInit() {
        this.error$ = this.store$.pipe(select(fromState.getError));
        this.pending$ = this.store$.pipe(select(fromState.getPending));
    }

    /**
     * Attempt to login.
     */
    public login(event: LoginCredentials) {
        this.store$.dispatch(new AuthActions.Login(event));
    }

    /**
     * Switch to register view.
     */
    public register(event: any) {
        this.store$.dispatch(new AuthActions.NavigateToRegister());
    }
}

Login and Register in Action

Now that we’ve seen how the client works let’s go back to the browser and see it in action. As previously mentioned, there’s no client form validation and this was done on purpose to ensure that the client handles server errors correctly (and really even more for demo purposes).

Navigate to http://localhost:4300/login to see the application in action. Let’s try several use cases and see how the client responds; in each case the “Login” button should be temporarily disabled until the HTTP response is received (due to the pending flag) and if present an error will show in the login UI. The same will be true for the register flow.

Login: Invalid request

Navigate to http://localhost:4300/login. Click the “Login” button without filling out any of the form fields and you’ll see the following errors in the UI and in the console. This is due to some server HTTP request body data validation where we determine there’s no valid username or password.

invalid-login-request.png

Login: Incorrect Username or password

Navigate to http://localhost:4300/login. Fill out the username and password form fields respectively:

  1. “Foo”

  2. “Bar”

Click the “Login” button you’ll see the following errors in the UI and in the console. This is due to server processing that determines that the username and password don’t match a user in the user database.

login-failed-incorrect-username-or-password.png

Login: success

Navigate to http://localhost:4300/login. Fill out the username and password form fields respectively:

  1. “tom.brady@patriots.com”

  2. “goat”

Click the “Login” button you’ll see the UI route to “Beers”. In this case the username and password were accepted by the server and a JWT was generated and passed back to the client. In addition, the client routes to “Beers” on login success and stores the JWT in the NGRX auth store before using it to request the list of beers from the API.

login-success-route-to-beers.png

Register: Invalid request

Navigate to http://localhost:4300/register. Click the “Register” button without filling out any of the form fields and you’ll see the following errors in the UI and in the console. This is due to some server HTTP request body data validation where we determine there’s no valid first name, last name, username or password.

register-failed-invalid-request.png

Register: success

Navigate to http://localhost:4300/register. Fill out the first name, last name, username and password form fields respectively:

  1. “Julian”

  2. “Edelman”

  3. “julian.edelman@patriots.com”

  4. “je11”

Click the “Register” button you’ll see the UI route to “Beers”. In this case the user registration was accepted by the server and a JWT was generated and passed back to the client. In addition, the client routes to “Beers” on register success and stores the JWT in the NGRX auth store before using it to request the list of beers from the API.

register-success-route-to-beers.png

The Server App

The server is a Node.js and Express app built on top of json-server. Let’s start at the main app file and dig down into the details.

Main

The Express app entry point is the file server/index.js and sets up the json-server for simple beer data APIs, routes to handle our login and register API endpoints and a couple HTTP interceptors.

Create Json-server

Create the json-server, provide it the beers mock JSON data so it can create its API routes, and add in the bodyParser so we can access HTTP request body data (aka login credentials and register new user data).

// Create the json-server and provide it our database file so it can create
// API routes to access our in-memory data.
const server = jsonServer.create();
const router = jsonServer.router("./server/database/mock/db.json");

// Setup express request body parsing.
server.use(bodyParser.urlencoded({extended: true}));
server.use(bodyParser.json());
server.use(jsonServer.defaults());

Add HTTP interceptors

Create an authentication request interceptor and mock data request interceptor. The former inspects HTTP request headers for the authentication header with a valid JWT, while the second simply adds a delay to our mock API data requests to simulate real network latency. We’ll talk about authTokenHttpRequestInterceptor in detail for beer requests later in this post.

// Add a security filter to intercept and inspect requests for valid tokens.
server.use(authTokenHttpRequestInterceptor.intercept);

// Delay all mock data request.
server.use(mockDataHttpRequestInterceptor.intercept);

Routing And Start Server

Create the authentication routes and map them to the exported routes in the authController, create the API routes to our json-server mock data and start the server. Take note how the authController routes are prefixed with /api/auth so that when we define our login and register routes later in the controller they’ll only need to specify their exact endpoint.

// API routes.
server.use("/api/auth", authController);
server.use("/api", router);

// Start the server.
server.listen(config.port, () => {
  util.consoleReset();
  console.log(`----------------------------------------------------------------------`);
  console.log(`Running Auth API Server on: http://localhost:${config.port}`);
  console.log(`----------------------------------------------------------------------`);
  console.log("\n");
});

Authentication and Registration

The authentication and registration processes are broken down into two modules:

  1. AuthController: server/auth/auth.controller.js

  2. AuthService: server/auth/auth.service.js

Authentication controller

The controller is responsible for defining auth routes and route handlers. It is not responsible for business logic like authenticating the user and generating a JWT or registering and creating a new user. These responsibilities are delegated to the authService. Let’s breakdown the controller to understand its role.

First create our /api/auth/login and /api/auth/register routes. Take notice how the initial context of /api/auth is not defined below in our route definitions. As previously mentioned, the main app entry point defines the routes to the authController using /api/auth so that we can simplify the endpoints here and leave higher-level orchestration and routing up to the main app.

// Define the list of auth routes and matching handler methods.
router.post("/login", authenticate);
router.post("/register", register);

// Export the routes defined here in this controller to the main server setup.
module.exports = router;

Next we’ll define our route handlers and helper methods to validate the incoming request data, delegate business functionality to our authService and finally to create successful and failed responses to our client.

// Handles auth requests that don't contain the required data: username and password.
function invalidLoginRequest(res) {
  const message = `Invalid login request. Must contain a valid username and password. ${httpStatus["400_MESSAGE"]}`;
  console.log(`invalidLoginRequest( ${message} )`);
  return util.createErrorResponse(res, httpStatus.BAD_REQUEST, message);
}

// Handles registration requests that don't contain the required data.
function invalidRegisterRequest(res) {
  const message = `Invalid register request. Must contain a valid firstName, lastName, username and password. ${httpStatus["400_MESSAGE"]}`;
  console.log(`invalidRegisterRequest( ${message} )`);
  return util.createErrorResponse(res, httpStatus.BAD_REQUEST, message);
}

// Handles failed authentication attempts.
function incorrectLoginCredentials(res) {
  const message = `Incorrect username or password. ${httpStatus["401_MESSAGE"]}`;
  console.log(`incorrectLoginCredentials( ${message} )`);
  return util.createErrorResponse(res, httpStatus.UNAUTHORIZED, message);
}

// Handles failed user registration attempts.
function failedToRegister(res) {
  const message = `Cannot register user. ${httpStatus["400_MESSAGE"]}`;
  console.log(`failedToRegister( ${message} )`);
  return util.createErrorResponse(res, httpStatus.BAD_REQUEST, message);
}

// Handles the requests that attempt to authenticate the user.
function authenticate(req, res, next) {

  // Attempt to grab the username and password from the request.
  let username = null;
  let password = null;
  try {
    username = req.body.username;
    password = req.body.password;
    console.log(`authenticate( ${username} / ${password} )`);
    if(!username || !password) {
      invalidLoginRequest(res);
      return;
    }
  } catch (err) {
    invalidLoginRequest(res);
    return;
  }

  // Attempt to authenticate the user.
  const token = authService.authenticate(username, password);
  if (!token) {
    incorrectLoginCredentials(res);
    return;
  }

  // We have a valid token so return a success response with a token.
  util.createSuccessResponse(res, {accessToken: token});
}

// Handles the requests that attempt to register a new user.
function register(req, res, next) {
  let username = null;
  let password = null;
  let firstName = null;
  let lastName = null;
  try {
    username = req.body.username;
    password = req.body.password;
    firstName = req.body.firstName;
    lastName = req.body.lastName;
    console.log(`register( Registering "${firstName} ${lastName}" with username and pw: ${username} / ${password} )`);
    if(!username || !password || !firstName || !lastName) {
      invalidRegisterRequest(res);
      return;
    }
  } catch (err) {
    invalidRegisterRequest(res);
    return;
  }

  // Attempt to register the user.
  const token = authService.register(username, password, firstName, lastName);
  if (!token) {
    failedToRegister(res);
    return;
  }

  // We have a valid token so return a success response with a token.
  util.createSuccessResponse(res, {accessToken: token});
}

Authentication Service

The service is responsible for business logic like authenticating the user and generating a JWT, registering and creating a new user, and / or interacting with a database. It is not responsible for handling HTTP requests from clients as that is the job of the aforementioned controller. Let’s breakdown the service to understand its role.

First create a reference to our in-memory users database along with some help methods we’ll use to determine if a user and a matching user and password exist in said database. Notice how we’re simply loading a JSON file into memory and accessing its users array at runtime.

NOTE: In a production app we’d use a real database, but for demo purposes this more that suits our needs and is considerably easier to setup.

// Create a reference to our mocked out users database.
const usersDatabase = JSON.parse(fs.readFileSync("./server/database/mock/users.json", "UTF-8")).users;

// Check if the user exists in database (matching username and password) which we'll say is good enough to be authenticated.
function doesUsernameAndPasswordExist({username, password}) {
  return usersDatabase.findIndex(user => user.username === username && user.password === password) !== -1;
}

// Check if the user exists in database (matching username).
function doesUsernameExist({username}) {
  return usersDatabase.findIndex(user => user.username === username) !== -1;
}

Next we’ll look at the business logic methods. Using the helper methods above we’ll determine if a user matching the username and password exist in the user database when calling the authenticate() method and then use our createToken() method to generate a JWT. Similarly we’ll “create” and register a new user with the register() method ensuring that the user doesn’t already exist. In both cases we’ll return a valid JWT for success or null for failures.

Remember, it's the job of our authController to send back either successful or failed HTTP responses to the client by handling the results from the service.

// Create a token from a payload.
function createToken(payload) {
  const jwtConfig = {
    expiresIn: config.jwt.expiresIn
  };
  return jwt.sign(payload, config.jwt.secretKey, jwtConfig)
}

function authenticate(username, password) {
  console.info(`authenticate( ${username} / ${password} )`);

  if (doesUsernameAndPasswordExist({username, password})) {
    console.info(`authenticateSuccess( ${username} )`);
    return createToken({username, password})
  } else {
    console.warn(`authenticateFault()`);
    return null;
  }
}

function register(username, password, firstName, lastName) {
  console.log(`register( register "${firstName} ${lastName}" with username and pw: ${username} / ${password} )`);

  if (!doesUsernameExist({username})) {
    console.info(`registerSuccess( ${username} )`);
    const newUser = {
      id: usersDatabase.length + 1,
      username: username,
      password: password,
      firstName: firstName,
      lastName: lastName
    };
    usersDatabase.push(newUser);
    return createToken({username, password})
  } else {
    console.warn(`registerFault( User "${username}" already exists. )`);
    return null;
  }
}

module.exports = {
  authenticate,
  register
};

Securing Endpoints

All of this was setup to enable secure connections to our /api/beers API endpoint with a valid JWT in HTTP request headers…so where do we check for valid JWTs in our server code? Enter the interceptor or HTTP middleware.

auth-Token.Http-Request-Interceptor

The authTokenHttpRequestInterceptor is responsible for inspecting all requests that require a valid JWT before allowing them to access secure endpoints like /api/beer. If an incoming request has an invalid JWT we’ll kick back a response error of 401 to the client before the request even makes it to the requested API endpoint. If it’s valid we’ll allow it to continue on to either the next interceptor or the final requested route.

Let’s start by looking at the intercept(req, res, next) method that handles the incoming request.

module.exports = {
  intercept: (req, res, next) => {
    if (requiresTokenValidation(req.url)) {
      console.log(`intercept( Validate token. )`);
      if (isAuthHeaderInvalid(req)) {
        invalidAuthHeader(res);
        return;
      }
      try {
        verifyToken(getTokenFromHeader(req));
        next();
      } catch (err) {
        console.warn(err);
        invalidToken(res);
      }
    } else {
      // console.log(`intercept( Don't validate token; either auth API endpoint or not an API endpoint. )`);
      next();
    }
  }
};

Now we don’t want to validate the token on every request; we only want to validate it for actual API requests — those that hit /api/* — but not for auth endpoints — those that hit /api/auth/*. This is handled by the requiresTokenValidation(url) method. If the API endpoint doesn’t require a token we’ll simply allow the request to move along in the chain via the next() method. Otherwise we’ll attempt to validate the toke.

function isAuthApiEndpoint(url) {
  return url.indexOf("/auth") !== -1;
}

function isApiEndpoint(url) {
  return url.indexOf("/api") !== -1;
}

function requiresTokenValidation(url) {
  return !isAuthApiEndpoint(url) && isApiEndpoint(url);
}

Finally we’ll attempt to validate the JWT for required API endpoints using the verifyToken() method. If it’s valid we’ll again allow the request to move along in the chain, otherwise we’ll kick back an error to the client.

// Verify the token.
function verifyToken(token) {
  jwt.verify(token, config.jwt.secretKey, function(err, decoded) {
    if (err) {
      console.log(`verifyToken( INVALID )`);
      throw new Error(`${err.name}: ${err.message}`);
    } else {
      console.log(`verifyToken( VALID )`);
      return decoded;
    }
  });
}

Conclusion

In this tutorial we saw how to create an Angular UI for login and registration using Smart Containers and Dumb Components with NGRX and how to create a Node.js + Express app with authentication and registration that generates and validates JWTs. Key points to the example are:

  1. Create Login and Register components with smart containers.

  2. Use NGRX framework to bubble up user events to effects and finally HTTP service calls.

  3. Save the authentication token in the store on successful login and registration calls.

  4. Use the JWT on secure API requests from the client to the server.

  5. Check for a valid token to secure API endpoints using Express middleware or interceptors.

See the code in action by grabbing the GitHub repo for the application.

2 Comments