Build a PWA with Ionic, Angular, and the WordPress REST API – Part 5

In this weekend side-hustle series, we’re building a Progressive Web App (PWA) for desktop and mobile that delivers WordPress content using Ionic, Angular, and the WordPress API. In Part 1, we got everything setup and working. In Part 2, we created a richer presentation of the WordPress Posts using Ionic cards. In Part 3, we implemented the single post view. In Part 4 we implemented paging functionality using Ionic’s Infinite Scroll (ion-infinite-scroll). Today, we’ll implement authentication so that we can login to WordPress. For now, this will just enable users to view private posts, but it will also be necessary in the future if we choose to allow administration from within the client app or other things like adding comments.

Part 4 Recap

Here’s a quick recap of what we accomplished in Part 4.

  • Updated the DataService to get more posts
  • Updated the HomePage component to use infinite scroll
  • Added ion-infinite-scroll to the HomePage component HTML
  • Made revised source code available on GitHub at https://github.com/codyburleson/ionic-ng-wp-client.

The final result of Part 4 still looks the same as what we ended with in Part 3, except that now, scrolling to the bottom of the list of posts loads more posts.

Create a Login page

To implement authentication with WordPress, we’re now going to need a login page, so we’ll start with that. Run  the following command:

ionic generate

Choose the page type from the list of options and enter “Login” for the name/path. The Ionic CLI will generate the following files:

  • src/app/login
    • login.module.ts
    • login.page.html
    • login.page.scss
    • login.page.spec.ts
    • login.page.ts

We’ll get back to this component in a bit, but first, let’s add the Login page to the menu.

Add the new Login page to the menu

Our menu of pages is generated from the public appPages array in AppComponent. Modify src/app/app.component.ts so that the appPages array now looks like this…

public appPages = [
    {
        title: 'Home',
        url: '/home',
        icon: 'home'
    },
    {
        title: 'List',
        url: '/list',
        icon: 'list'
    },
    {
        title: 'Login',
        url: '/login',
        icon: 'unlock'
    }
]

Now, if you open src/app/app-routing.module.ts, you will find that the Ionic CLI already placed a route entry. We only need to change that entry so that the route uses all lower case for the login route and we want to move it above the :slug route as follows…

const routes: Routes = [
    {
        path: '',
        redirectTo: 'home',
        pathMatch: 'full'
    },
    {
        path: 'home',
        loadChildren: './home/home.module#HomePageModule'
    },
    {
        path: 'list',
        loadChildren: './list/list.module#ListPageModule'
    },
    {path: 'login', loadChildren: './login/login.module#LoginPageModule'},
    {path: ':slug', loadChildren: './post/post.module#PostPageModule'}
];

Now if you run the app with ionic serve, we can see the new menu item and we can navigate the the Login page.

Since this is a stable check-point, we’ll do a Git commit so that we can roll back if we screw something up later. Here, I am committing with the following Git commit message.

Add login page (stub)

Create the AuthenticationService

Run the ionic generate command, select the service type from the list of options, and then enter “Authentication” for the Name/path of the service. Now open the newly generated file, src/app/shared/authentication.service.ts and modify it as follows…

import {Injectable} from '@angular/core';
import {environment} from '../../environments/environment';
import {HttpClient, HttpHeaders} from '@angular/common/http';

const ENDPOINT_URL = environment.endpointURL;

@Injectable({
    providedIn: 'root'
})
export class AuthenticationService {

    private user: any;

    constructor(private http: HttpClient) {
    }

    /**
     * Login to WordPress via JWT. Returns object with the following shape:
     * {
     *      token: "eyJ0eXAiOiJKV1QiLCJhbGci...",
     *      user_email: "someuser@somewhere.com",
     *      user_nicename: "wordpress",
     *      user_display_name: "wordpress"
     * }
     */
    doLogin(username, password) {
        return this.http.post(ENDPOINT_URL + 'jwt-auth/v1/token', {
            username: username,
            password: password
        });
    }

    validateAuthToken(token) {
        let headers = new HttpHeaders();
        headers = headers.set('Authorization', 'Basic ' + token);
        return this.http.post(ENDPOINT_URL + 'jwt-auth/v1/token/validate?token=' + token,
            {}, {headers: headers});
    }

    getUser() {
        return this.user;
    }

    setUser(user) {
        this.user = user;
    }


}

This service is written to use a JSON web token based login, which requires the installation and configuration of a WordPress plugin, which is described next.

Install and configure JWT Authentication for WP REST API

To make the authentication service work, we must install the wp-api-jwt-auth plugin which allows us to generate an authentication token. Carefully follow all of the instructions for installation and configuration provided on the plugin’s information page. In general this involves:

  • Installing the plugin,
  • Editing the .htaccess file to enable the HTTP Authorization Header if it is not enabled,
  • Enabling a secret key by adding a constant to the wp-config.php file,
  • Enabling CORS support by adding a constant to the wp-config.php file,
  • And finally, activating the plugin within wp-admin.

If you’ve been using the Docker container-based local development environment that was described in Part 1, then you may want to know how to access the .htaccess file and the wp-config.php file that is within the container. You can get container shell access with root privileges using the following command:

docker exec -ti -u root <container-name> bash

In my case, for example, the command is:

docker exec -ti -u root ionpress_my-wp_1 bash

To find the names of your running containers, use the docker ps -a command.

To install the nano editor, you can run the following command in the container.

apt-get update && apt-get -y install nano

Then you can run the following command to open and edit the .htaccess file using nano.

nano ./.htaccess

After editing the .htaccess file for the JWT plugin, mine now looks like this:

# BEGIN WordPress
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^index\.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{HTTP:Authorization} ^(.*)
RewriteRule . /index.php [L]
RewriteRule ^(.*) - [E=HTTP_AUTHORIZATION:%1]
</IfModule>
# END WordPress

Next, you can then run the following command to edit the wp-config.php file…

nano ./wp-config.php

Add the following to the file:

/** WordPress JWT Authentication for WP REST API **/
/** You can generate secret keys with this URL: https://api.wordpress.org/secret-key/1.1/salt/ **/
define('JWT_AUTH_SECRET_KEY', 'EBgvg4a,bmBYrTf&|-f5{~W0WoZH]&([JCp{GN`4[Zg4- vX-W|(3!a Ck.-OPd5');
define('JWT_AUTH_CORS_ENABLE', true);

Note that as stated in the comment in the snippet above, you can generate secret keys with this URL: https://api.wordpress.org/secret-key/1.1/salt/

Once the .htaccess file and the wp-config.php file have been edited, you should then activate the plugin within wp-admin. I’m not sure if it is necessary to stop and restart the container after these changes, but I did, just in case.

At this point, we want to update the README.md file to let users and developers know that the JWT plugin is necessary for our client app to work. I’ve done that and am commiting that change with the following Git commit message.

Update README.md with instructions for WordPress JWT plugin

Create a private post

To test the authentication, we’re also going to need a private post in WordPress that will help us validate that the login works. If we login and can see the private post, we’ll know the authentication works. In WordPress, create a post and in the Publish widget, set the visibility to private.

Modify Login page HTML

Now, we need a login form on the Login page. Edit src/app/login/login.page.html to be as follows…

<ion-header>
  <ion-toolbar>
    <ion-buttons slot="start">
      <ion-menu-button></ion-menu-button>
    </ion-buttons>
    <ion-title>Login</ion-title>
  </ion-toolbar>
</ion-header>

<ion-content padding>
  <div class="alert alert-danger" role="alert" *ngIf="error_message">{{error_message}}</div>
  <form [formGroup]="login_form" (ngSubmit)="login(login_form.value)">
    <ion-item>
      <ion-label color="primary">Username</ion-label>
      <ion-input type="text" formControlName="username" placeholder="Enter username" required></ion-input>
    </ion-item>
    <ion-item>
      <ion-label color="primary">Password</ion-label>
      <ion-input type="password" formControlName="password" placeholder="Enter password" required></ion-input>
    </ion-item>
    <ion-button expand="full"  type="submit">Login</ion-button>
  </form>
</ion-content>

This will give us a simple login form like this…

Modify Login page style

When the user enters invalid credentials, we want the error message to show in a nice red alert block. For this, I’ve borrowed a styles and class names from Bootstrap 4 (even though Bootstrap is not included in the project). Edit src/app/login/login.page.scss to be as follows.

.alert {
  position: relative;
  padding: .75rem 1.25rem;
  margin-bottom: 1rem;
  border: 1px solid transparent;
  border-radius: .25rem;
}
.alert-danger {
  color: #721c24;
  background-color: #f8d7da;
  border-color: #f5c6cb;
}

Of course, we might move these styles in the future to make them global (for now, they apply only to the LoginPage component). This will give us a simple red-boxed error prompt like this…

Import the ReactiveFormsModule

Since we’re using a form now, we’ll need to import the ReactiveFormsModule into the module for the LoginPage component. Edit src/app/login/login.module.ts. We just need to add ReactiveFormsModule to the @NgModule imports section. The full file should then look like this…

import {NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';
import {ReactiveFormsModule} from '@angular/forms';
import {Routes, RouterModule} from '@angular/router';

import {IonicModule} from '@ionic/angular';

import {LoginPage} from './login.page';

const routes: Routes = [
    {
        path: '',
        component: LoginPage
    }
];

@NgModule({
    imports: [
        CommonModule,
        ReactiveFormsModule,
        IonicModule,
        RouterModule.forChild(routes)
    ],
    declarations: [LoginPage]
})
export class LoginPageModule {
}

Modify the LoginPage component

Edit src/app/login/login.page.ts to be as follows…

import {Component, OnInit} from '@angular/core';
import {Validators, FormBuilder, FormGroup, FormControl} from '@angular/forms';
import {Router} from '@angular/router';
import {LoadingController} from '@ionic/angular';
import {AuthenticationService} from '../shared/authentication.service';
import {DataService} from '../shared/data.service';

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

    login_form: FormGroup;
    error_message: string;

    constructor(
        public formBuilder: FormBuilder,
        public loadingController: LoadingController,
        public authenticationService: AuthenticationService,
        public dataService: DataService,
        private router: Router) {
    }

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

    async login(value) {

        const loading = await this.loadingController.create({
            duration: 5000,
            message: 'Please wait...'
        });

        loading.present();

        this.authenticationService.doLogin(value.username, value.password)
            .subscribe(res => {
                    this.authenticationService.setUser(res);
                    // Reset the post items so that next time, they are completely
                    // reloaded for the newly authenticated user...
                    this.dataService.items = [];
                    loading.dismiss();
                    this.router.navigateByUrl('home');

                },
                err => {
                    this.error_message = 'Invalid credentials.';
                    loading.dismiss();
                    console.log(err);
                });

    }

}

What’s going on here?

  • In the login() function, we display an Ionic loading controller.
  • We then pass credentials from the form to the authentication service.
  • If the login is successful…
    • The service returns a user object. For now, we give the user object back to the singleton service (with setUser()). Probably, in the future, we should make this step unnecessary by automatically setting the user in the service itself. But… we’re going in iterative steps here. We’ll get to that someday soon.
    • We clear out the list of posts from the DataService so that they’ll be completely reloaded. This ensures that any private posts that may now be available to the authenticated user will be loaded when we return to the Home page.
    • We dismiss the loading spinner.
    • Then finally, we navigate Home, which reloads the first page of posts (this time passing the token and thus getting public and private posts).
  • If the login is unsuccessful, we set an error message, which will appear on the login form and in the browser console.

Modify the DataService

Finally, we just need to modify the DataService. Edit src/app/shared/data.service.ts to be as follows…

import {Injectable} from '@angular/core';
import {environment} from '../../environments/environment';
import {HttpClient, HttpResponse} from '@angular/common/http';

import 'rxjs/add/operator/map';
import {of} from 'rxjs/observable/of';
import {Observable} from 'rxjs';
import {AuthenticationService} from './authentication.service';

const ENDPOINT_URL = environment.endpointURL;

@Injectable({
    providedIn: 'root'
})
export class DataService {

    items: any[] = [];
    page = 1;
    totalPages = 1;

    constructor(private http: HttpClient, public authenticationService: AuthenticationService) {
    }

    /**
     * Gets a page of posts or all posts formerly fetched
     */
    getPosts(): any {
        if (this.items.length > 0) {
            return of(this.items);
        } else {

            const user = this.authenticationService.getUser();
            if (user) {
                return this.http.get(ENDPOINT_URL + 'wp/v2/posts?_embed&status=any&token=' + user.token,
                    {observe: 'response', headers: {'Authorization': 'Bearer ' + user.token}})
                    .map(this.processPostData, this);
            } else {
                return this.http.get(ENDPOINT_URL + 'wp/v2/posts?_embed', {observe: 'response'})
                    .map(this.processPostData, this);
            }

        }
    }

    /**
     * Gets the next page of posts
     */
    getMorePosts(): any {
        this.page++;
        return this.http.get(ENDPOINT_URL + 'wp/v2/posts?_embed&page=' + this.page, {observe: 'response'})
            .map(this.processPostData, this);
    }

    // A place for post-processing, before making the fetched data available to view.
    processPostData(resp: HttpResponse<any[]>) {
        this.totalPages = +resp.headers.get('X-WP-TotalPages'); // unary (+) operator casts the string to a number
        resp.body.forEach((item: any) => {
            this.items.push(item);
        });
        return this.items;
    }

    getPostBySlug(slug): any {
        return this.items.find(item => item.slug === slug);
    }

    hasMorePosts() {
        return this.page < this.totalPages;
    }

}

In the getPosts() function, we now look for a user object. If we find it, that means we are authenticated (we have a token to use). If we have a user, we use the following snippet instead of the former one.

return this.http.get(ENDPOINT_URL + 'wp/v2/posts?_embed&status=any&token=' + user.token,
    {observe: 'response', headers: {'Authorization': 'Bearer ' + user.token}})
    .map(this.processPostData, this);

We’re doing three things different here now.

  1. We’re using the URL parameter and value, status=any, which indicates to WordPress that we now want posts with any status (i.e. public and private).
  2. We’re using the URL parameter, token, with the value of the token given back by the JWT plugin when we logged in.
  3. We’re also passing the Authorization HTTP header with the token value; that’s the part written as headers: {'Authorization': 'Bearer ' + user.token}

And that is it! Now, if you run the app with ionic serve, you will not see any private posts at first. But, if you login successfully, you should then see all posts (both public and private). In the screenshot below, for example, the post your reading now is indicated as Private, which is how I keep them until I’m ready to make them public.

I’m committing the code to Github at this point with the following message:

Implement authentication

Wrapping up

Today we took a big step in terms of the necessary plumbing. We implemented authentication with a little help from the WordPress plugin, JWT Authentication for WP REST API. We proved this authentication using a private post, which will only display after we’re authenticated.

There are still so many more things to be done, I’m not quite sure what’s best to start on next, but I will be continuing the series, so stay tuned so stay tuned and remember that the evolving source code can always be cloned from GitHub at https://github.com/codyburleson/ionic-ng-wp-client.