Software Engineering

JWT Authentication in an Angular-WordPress App


In the context of modern software engineering, decoupling—breaking an application into distinct parts—has emerged as an industry standard. Companies and software engineers alike favor decoupling because it allows for a clear separation of concerns between an application’s presentation layer (front end) and its data access layer (back end). This approach enhances an app’s efficiency by allowing for parallel development by multiple teams while also offering the flexibility to choose optimal technologies for each side.

Given its modular nature, a decoupled system’s independent components can be targeted for scaling, modification, or outright replacement as the system’s needs evolve. This practice extends across diverse digital platforms, including areas like e-commerce, online banking, community-driven portals, and social media.

While a decoupled system offers many advantages, it also carries potential drawbacks. The system’s communication occurs across different modules or services and can introduce latency, which slows system performance. In addition, traditional browser cookie and server-side authentication methods designed for monolithic applications become challenging.

To address these concerns, developers can leverage protocols like GraphQL, REST, and gRPC to facilitate excellent intercomponent communication, prevent delays, and structure the implementation of authentication. This tutorial demonstrates that decoupled apps can thrive: In a WordPress-powered Angular app, we will achieve secure communication using GraphQL and JWT, a popular token-based authentication method.

Efficient Communication in Decoupled Systems: An Angular-WordPress Example

We will build a blog application with a headless WordPress back end and an Angular front end. WordPress, a widely adopted, robust content management system (CMS), is ideal for managing and serving blog content. The choice of Angular is strategic, as it allows for dynamic content updates without requiring full-page reloads, which yields accelerated user interactions. Communication between the two layers will be managed by GraphQL.

Architecture with annotations of a simple, unprotected, decoupled blog app

Initially, the app will be configured to fetch blog post content and display the post titles to users in a list. After it is up and running, you’ll enhance the unprotected blog application by integrating a JWT-based authentication feature. Through this token-based authentication, you ensure that only logged-in users have access. Unauthenticated visitors will see the list of titles but be prompted to sign in or register if they attempt to read a full post.

Architecture with annotations of an enhanced, decoupled blog app

On the front end, the route guard checks user permissions and determines whether a route can be activated, and the HTTP module facilitates HTTP communication. On the back end, GraphQL serves as the app’s communication medium, implemented as an API interface over HTTP.

Note: The complex issue of cybersecurity is a broad topic that falls outside of the scope of this article. This tutorial focuses on the integration of disparate front and back ends through an effective cross-domain solution, leveraging GraphQL to implement authentication in an Angular-WordPress app. This tutorial does not, however, guarantee the restriction of GraphQL access strictly to logged-in users, as achieving that would require configuring GraphQL to recognize access tokens, a task beyond our scope.

Step 1: Set Up the Application’s Environment

This is the launch point for this project:

  1. Use a fresh or existing installation of WordPress on your device.
  2. Log in to WordPress as an administrator and, from the menu, choose Settings/General. In the membership section, select the button beside Anyone can register to enable this option.
  3. Along with WordPress, you’ll use the WPGraphQL plugin. Download the plugin from the WordPress plugin directory and activate it.
  4. To further extend the WPGraphQL plugin’s functionality, we will also use the WPGraphQL JWT Authentication plugin. It is not listed in WordPress’ directory, so add this plugin according to its instructions, making sure to define a secret key, as detailed in the readme.md. The plugin will not work without one.
  5. Add a fresh install of Angular to your local device. Then create a workspace and application with routing and CSS support using the command ng n my-graphql-wp-app --routing --style css.
    • Caveat: This tutorial was written using version 16 of Angular. For subsequent versions of Angular, you may need to adapt the steps and/or modify the file names presented herein.

With your WordPress setup in place, the back end of your simple blog site is ready.

Step 2: Build Out the App’s Front End

You’ll need to have all parts in place before you can establish communication between the application’s two ends. In this step, you will set up the necessary elements: create pages, add and set up routes, and integrate the HTTP module. With these pieces in place, we can fetch and display content.

The WPGraphQL plugin activated during setup will enable WordPress to expose data through the app’s GraphQL API. By default, the GraphQL endpoint is located at YOUR-SITE-URL/graphql where YOUR-SITE-URL is replaced with the URL associated with the WordPress installation. For example, if the site URL is example.com, the app’s GraphQL API endpoint is example.com/graphql.

Create the App’s Pages

This simple app will consist of just two pages initially: posts (listing all post titles) and post (displaying an entire post).

Generate the app’s content pages using Angular’s CLI method. Using your preferred terminal app, access the Angular root directory and type:

ng generate component posts && ng generate component post

But these new pages won’t be visible without a rendering container and routes.

Add Routes

A route allows users to access a page directly via a corresponding URL or navigation link. Although your fresh Angular installation includes routing, the feature is not supported by default.

To add routes to the app, replace the contents of the src/app/app-routing.module.ts file with:

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { PostComponent } from './post/post.component';
import { PostsComponent } from './posts/posts.component';

const routes: Routes = [
  { path: 'post/:id', component: PostComponent },
  { path: 'posts', component: PostsComponent },
];

@NgModule( {
  imports: [ RouterModule.forRoot( routes ) ],
  exports: [ RouterModule ]
} )

export class AppRoutingModule { }

With the preceding code, we’ve added two routes to the app: one route to the posts page, the other to the post page.

Add the Router Outlet Component

To make use of routing support, we need the router-outlet that enables Angular to render the app’s content pages as the user navigates to different routes.

Use your preferred code editor and replace the contents of Angular’s src/app/app.component.html file with:

<router-outlet></router-outlet>

Now the route setup is complete. But before we can fetch content, we have to set up the HTTP module middleware.

Integrate the HTTP Module

To fetch content for visiting users, a page needs to send an HTTP request to the back end. Replace the contents of the src/app/app.module.ts file with:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { HttpClientModule } from '@angular/common/http'; 
import { AppRoutingModule } from './app-routing.module';
import { PostComponent } from './post/post.component';
import { PostsComponent } from './posts/posts.component';
import { AppComponent } from './app.component';

@NgModule( {
  declarations: [
    AppComponent,
    PostComponent,
    PostsComponent,
  ],
  imports: [
    BrowserModule,
    HttpClientModule, 
    AppRoutingModule
  ],
  providers: [],
  bootstrap: [ AppComponent ]
} )

export class AppModule { }

With this code, we have integrated Angular’s native HTTP module, which enables us to send HTTP requests to fetch content.

Set Up to Fetch and Display Content

Let’s now start fetching and displaying content on the blog’s pages.

The Posts Page

Replace the contents of the src/app/posts/posts.component.ts file with:

import { Component } from '@angular/core';
import { HttpClient } from '@angular/common/http';

@Component( {
  selector: 'app-posts',
  templateUrl: './posts.component.html',
  styleUrls: ['./posts.component.css']
} )

export class PostsComponent
{
  posts = [];

  constructor( private http: HttpClient ) { }

  async send_graphql_request( query: string )
  {    
    const response = await this.http.post<any>( HERE_GOES_YOUR_GRAPHQL_API_ENDPOINT, { query: query }, { } ).toPromise()

    return response;
  }

  ngOnInit()
  {
    this.send_graphql_request(
      `query GetPostsQuery {
        posts(where: {orderby: {field: DATE, order: DESC}}) {
          nodes {
            databaseId
            featuredImage {
              node {
                sourceUrl
              }
            }
            title
            excerpt
          }
        }
      }`
    )
    .then( response =>
    {
      if( typeof response.errors == 'undefined' && typeof response.data !== 'undefined' )
      {
        this.posts = response.data.posts.nodes;
      }
      else
      {
        console.log( 'Something went wrong! Please try again.' );
      }
    } )
  }
}

When a user accesses the posts page, this code is triggered and sends an HTTP request to the back end. The request leverages a GraphQL schema to fetch the latest posts from the WordPress database.

Next, to display the fetched posts, replace the contents of src/app/posts/posts.component.html file with:

<div class="content" role="main">
  <h2 class="title">List Of Posts</h2>
  <div id="data">
    <li class="post" *ngFor="let post of posts">
      <img *ngIf="post['featuredImage']" src="{{post['featuredImage']['node']['sourceUrl']}}">
      <img *ngIf="!post['featuredImage']" src="https://picsum.photos/300/200">
      <h3>{{post['title']}}</h3>
        <a routerLink="/post/{{post['databaseId']}}">View Post</a>
    </li>
  </div>
</div>

Add the following CSS to the app/src/posts/posts.component.css file to provide the posts page with a minimalistic look:

.content {
  width: 900px;
  margin: 0 auto;
}
h2.title {
  text-align: center;
}
li.post {
  list-style: none;
  text-align: center;
  flex: 0 0 28.333333%;
  margin-bottom: 15px;
}
img {
  max-width: 100%;
}
div#data {
  display: flex;
  flex-direction: row;
  justify-content: center;
  gap: 5%;
  flex-wrap: wrap;
}

The Post Page

The same procedure readies the post page. Replace the contents of the src/app/post/post.component.ts file with:

import { Component } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { ActivatedRoute } from '@angular/router';

@Component( {
  selector: 'app-post',
  templateUrl: './post.component.html',
  styleUrls: ['./post.component.css']
} )

export class PostComponent
{
  post = {
    title : '',
    content : '',
  };

  constructor( private route: ActivatedRoute, private http: HttpClient ) { }

  async send_graphql_request( query: string )
  { 
    const response = await this.http.post<any>( HERE_GOES_YOUR_GRAPHQL_API_ENDPOINT, { query: query }, {} ).toPromise()

    return response;
  }

  ngOnInit()
  {
    const post_id = this.route.snapshot.paramMap.get( 'id' );
    
    this.send_graphql_request(
      `query GetPostsQuery {
        post(id: "${post_id}", idType: DATABASE_ID) {
          content
          title
        }
      }`
    )
    .then( response =>
    {
      if( typeof response.errors == 'undefined' && typeof response.data !== 'undefined' )
      {
        this.post = response.data.post;
      }
      else
      {
        console.log( 'Something went wrong! Please try again.' );
      }
    } )
  }
}

Now, to display the content fetched from post, replace the contents of src/app/post/post.component.html file with:

<div class="content" role="main">
  <h2 class="title">{{post.title}}</h2>
  <div [innerHTML]="post.content"></div>
</div>

Lastly, add the following CSS to the app/src/post/post.component.css file:

.content {
  width: 900px;
  margin: 0 auto;
}
h2.title {
  text-align: center;
}

These CSS rules will give post the same look and feel as its mate.

Progress Check

You’ve set up the essential elements for the app and established the core infrastructure required for communication between the app’s Angular front end and its headless WordPress back end. In your browser, test the viewability of the app’s sample content.

The posts page.
An Example of a Posts Page
The post page.
An Example of a Post Page

Step 3: Add Authentication

Adding authentication allows for the restriction of the post page to be viewable only by authorized users. To implement this, add a register page and a login page to the app.

The Registration Page

Create the Page

Use the terminal app to reaccess Angular’s root directory and type:

ng generate component register

This creates a new page named register.

To support HTML form input fields as Angular input, import Angular’s FormsModule module into the src/app/app.module.ts file. Replace the existing file contents with:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { HttpClientModule } from '@angular/common/http';
import { AppRoutingModule } from './app-routing.module';
import { PostComponent } from './post/post.component';
import { PostsComponent } from './posts/posts.component';
import { AppComponent } from './app.component';
import { RegisterComponent } from './register/register.component';
import { FormsModule } from '@angular/forms'; //<----- New line added.

@NgModule( {
  declarations: [
    AppComponent,
    PostComponent,
    PostsComponent,
    RegisterComponent,
  ],
  imports: [
    BrowserModule,
    HttpClientModule,
    AppRoutingModule,
    FormsModule //<----- New line added.
  ],
  providers: [],
  bootstrap: [ AppComponent ]
} )

export class AppModule { }

In-line comments are added to pinpoint changes made to the code.

Add a Route

Now, to create the register route, replace the contents of the src/app/app-routing.module.ts file with:

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { PostComponent } from './post/post.component';
import { PostsComponent } from './posts/posts.component';
import { RegisterComponent } from './register/register.component'; //<----- New line added.

const routes: Routes = [
  { path: 'post/:id', component: PostComponent },
  { path: 'posts', component: PostsComponent },
  { path: 'register', component: RegisterComponent }, //<----- New line added.
];

@NgModule( {
  imports: [ RouterModule.forRoot( routes ) ],
  exports: [RouterModule]
} )

export class AppRoutingModule { }

With the route added, it’s time to configure the app to verify the new user’s credentials and finalize their registration. Replace the contents of the src/app/register/register.component.ts file with:

import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { HttpClient } from '@angular/common/http';

@Component( {
  selector: 'app-register',
  templateUrl: './register.component.html',
  styleUrls: ['./register.component.css']
} )

export class RegisterComponent
{
  constructor( public router: Router, private http: HttpClient ) {}
  
  username = '';
  
  email = '';
  
  password = '';
  
  error_message = '';

  async send_graphql_request( query: string )
  {    
    const response = await this.http.post<any>( HERE_GOES_YOUR_GRAPHQL_API_ENDPOINT, { query: query }, { } ).toPromise()

    return response;
  }
  
  register()
  {
    document.getElementsByTagName( 'button' )[0].setAttribute( 'disabled', 'disabled' );
    
    document.getElementsByTagName( 'button' )[0].innerHTML = 'Loading';

    this.send_graphql_request(
      `mutation RegisterMutation {
        registerUser(input: {username: "${this.username}", email: "${this.email}", password: "${this.password}"}) {
          user {
            databaseId
          }
        }
      }`
    )
    .then( response =>
    {        
        if( typeof response.errors == 'undefined' && typeof response.data.registerUser.user.databaseId !== 'undefined' )
        {
          this.router.navigate( ['/login'] );
        }
        else
        {
          this.error_message = this.decodeHTMLEntities( response.errors[0].message );
        }

	  document.getElementsByTagName( 'button' )[0].innerHTML = 'Register';
       document.getElementsByTagName( 'button' )[0].removeAttribute( 'disabled' );
    } )
  }

  decodeHTMLEntities( text : string )
  {
    const entities = [
      ['amp', '&'],
      ['apos', '\''],
      ['#x27', '\''],
      ['#x2F', '/'],
      ['#39', '\''],
      ['#47', '/'],
      ['lt', '<'],
      ['gt', '>'],
      ['nbsp', ' '],
      ['quot', '"']
    ];

    for ( let i = 0, max = entities.length; i < max; ++i )
      text = text.replace( new RegExp( '&' + entities[i][0] + ';', 'g'), entities[i][1] );
    
    return text;
  }
}

The register() method in this code sends the new user’s credentials to the app’s GraphQL API for verification. If registration is successful, the new user is created, and the API returns a JSON response with the newly created user ID. Otherwise, an error message guides the user as necessary.

Add Content

To add a user registration form to the page, replace the contents of the src/app/register/register.component.html file with:

<div class="register-form">
  <h2>Register</h2>
  <div [innerHTML]="error_message"></div>
  <form>
    <input type="text" name="username" [(ngModel)]="username" placeholder="Username" required />
    <input type="text" name="email" [(ngModel)]="email" placeholder="Email" required />
    <input type="password" name="password" [(ngModel)]="password" placeholder="Password" required />
    <button type="submit" class="btn" (click)="register()">Register</button>
  </form>
</div>

Let’s repeat these steps for the login page.

The Login Page

Create the Page

Using the terminal app, reaccess Angular’s root directory and type:

ng generate component login

Create the login route by replacing the contents of the src/app/app-routing.module.ts file with:

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { PostComponent } from './post/post.component';
import { PostsComponent } from './posts/posts.component';
import { RegisterComponent } from './register/register.component';
import { LoginComponent } from './login/login.component'; //<----- New line added.

const routes: Routes = [
  { path: 'post/:id', component: PostComponent },
  { path: 'posts', component: PostsComponent },
  { path: 'register', component: RegisterComponent },
  { path: 'login', component: LoginComponent }, //<----- New line added.
];

@NgModule( {
  imports: [ RouterModule.forRoot( routes ) ],
  exports: [RouterModule]
} )

export class AppRoutingModule { }

To set up the app to verify the user’s credentials, replace the contents of the src/app/login/login.component.ts file with:

import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { HttpClient } from '@angular/common/http';

@Component( {
  selector: 'app-login',
  templateUrl: './login.component.html',
  styleUrls: ['./login.component.css']
} )

export class LoginComponent
{
  constructor( public router: Router, private http: HttpClient ) {}
  
  username = '';
  
  password = '';

  error_message= '';

  async send_graphql_request( query: string )
  {    
    const response = await this.http.post<any>( HERE_GOES_YOUR_GRAPHQL_API_ENDPOINT, { query: query }, { } ).toPromise()

    return response;
  }
  
  login()
  {
    document.getElementsByTagName( 'button' )[0].setAttribute( 'disabled', 'disabled' );
    
    document.getElementsByTagName( 'button' )[0].innerHTML = 'Loading';

    this.send_graphql_request(
      `mutation LoginMutation {
        login(input: {username: "${this.username}", password: "${this.password}"}) {
          authToken
        }
      }`
    )
    .then( response =>
    {        
        if( typeof response.errors == 'undefined' && typeof response.data.login.authToken !== 'undefined' )
        {
          localStorage.setItem( 'auth_token', JSON.stringify( response.data.login.authToken ) );

          this.router.navigate( ['/posts'] );
        }
        else
        {
          this.error_message = this.decodeHTMLEntities( response.errors[0].message );
        }

	   document.getElementsByTagName( 'button' )[0].innerHTML = 'Login';

        document.getElementsByTagName( 'button' )[0].removeAttribute( 'disabled' );
    } )
  }

  decodeHTMLEntities( text : string )
  {
    var entities = [
      ['amp', '&'],
      ['apos', '\''],
      ['#x27', '\''],
      ['#x2F', '/'],
      ['#39', '\''],
      ['#47', '/'],
      ['lt', '<'],
      ['gt', '>'],
      ['nbsp', ' '],
      ['quot', '"']
    ];

    for ( var i = 0, max = entities.length; i < max; ++i )
      text = text.replace( new RegExp( '&' + entities[i][0] + ';', 'g'), entities[i][1] );
    
    return text;
  }
}

Next, replace the contents of src/app/login/login.component.html file with:

<div class="log-form">
  <h2>Login to your account</h2>
  <div [innerHTML]="error_message"></div>
  <form>
    <input type="text" name="username" [(ngModel)]="username" placeholder="Username" required />
    <input type="password" name="password" [(ngModel)]="password" placeholder="Password" required />
    <button type="submit" class="btn" (click)="login()">Login</button>
  </form>
</div>

This snippet adds a login form to the page with inputs for user credentials. Similar to the way the app’s registration page is set up, the code added here sends an existing user’s credentials to the app’s GraphQL API for validation. If the credentials are correct, the API returns a JWT, saving it in the browser’s localStorage for later use. If the user’s credentials are invalid or if the JWT has expired, an error message guides them as necessary.

Progress Check

To test authentication, register as a new user and log in to the app. Then, to log out, remove the token from the browser’s localStorage. Your results should look similar to the screenshots below:

The registration page.
An Example of a Registration Page
The login page.
An Example of a Login Page
The login page with an error message.
An Example of a Login Page With Incorrect Credentials

Step 4: Implement Restrictions

With the authentication feature up and running, the next task is to restrict access to the post route, allowing logged-in users only.

Create and Set Up the Guard and Service

Using the terminal app, reaccess Angular’s root directory and type:

ng generate service auth && ng generate guard auth

You will be prompted with a list of interfaces to implement. Choose CanActivate to establish a guard that confirms a user’s authentication through a service, also created in this step.

Next, set up your guard and service to manage the authentication. Replace the contents of the src/app/auth.service.ts file with:

import { Injectable } from '@angular/core';
import { Router } from '@angular/router';

@Injectable( {
  providedIn: 'root'
} )

export class AuthService
{
  router : any;
  
  constructor( private route: Router )
  {
    this.router = route
  }

  loggedIn()
  {
    if( localStorage.getItem( 'auth_token' ) != null ) return true;

    this.router.navigate( ['/login'] ); return false;
  }
}

With this code, your setup of the service to manage authentication is complete. If a JWT is present, the service sends an affirmative response to the guard. Otherwise, it returns a false response to indicate that the user is not logged in.

To restrict the post route based on information received from the service, replace the contents of the src/app/auth.guard.ts file with:

import { CanActivateFn } from '@angular/router';
import { AuthService } from './auth.service';
import { inject } from '@angular/core';

export const authGuard: CanActivateFn = ( route, state ) =>
{
  // Use dependency injection to get an instance of the AuthService.
  const authService = inject( AuthService );

  // Return whether the user is logged in using the AuthService.
  return authService.loggedIn();
};

Now the post page is restricted, allowing only logged-in users.

Restrict the Post Page’s Route

To extend the post page’s restriction, let’s implement a route-specific restriction. Replace the contents of the src/app/app-routing.module.ts file with:

import { NgModule } from '@angular/core';
import { RouterModule, Routes, CanActivate } from '@angular/router';
import { PostComponent } from './post/post.component';
import { PostsComponent } from './posts/posts.component';
import { LoginComponent } from './login/login.component';
import { RegisterComponent } from './register/register.component';
import { authGuard } from './auth.guard'; //<----- New line added.

const routes: Routes = [
  { path: 'post/:id', component: PostComponent, canActivate: [ authGuard ] }, //<----- New code added.
  { path: 'posts', component: PostsComponent },
  { path: 'register', component: RegisterComponent },
  { path: 'login', component: LoginComponent },
];

@NgModule( {
  imports: [ RouterModule.forRoot( routes ) ],
  exports: [ RouterModule ]
} )

export class AppRoutingModule { }

With the changed code, the post page’s route now uses Angular’s canActivate method to serve the page only to authenticated users.

Verify the JWT

You are now ready to validate the JWT saved in the visiting user’s browser. Specifically, you will check in real time that the JWT is unexpired and valid. Replace the contents of the src/app/post/post.component.ts file with:

import { Component } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { ActivatedRoute } from '@angular/router';

@Component( {
  selector: 'app-post',
  templateUrl: './post.component.html',
  styleUrls: ['./post.component.css']
} )

export class PostComponent
{
  post = {
    title : '',
    content : '',
  };

  constructor( private route: ActivatedRoute, private http: HttpClient ) { }

  async send_graphql_request( query: string )
  {
    
    let headers = {};
    
    // New code begins here.
    const token = localStorage.getItem( 'auth_token' );
    
    if( token !== null )
    {
      const parsedToken = JSON.parse( token );

      if( parsedToken )
      {
        headers = { 'Authorization': 'Bearer ' + parsedToken };
      }
    }
    // New code ends here.
    
    const response = await this.http.post<any>( HERE_GOES_YOUR_GRAPHQL_API_ENDPOINT, { query: query }, { headers } ).toPromise()

    return response;
  }

  ngOnInit()
  {
    const post_id = this.route.snapshot.paramMap.get( 'id' );
    
    this.send_graphql_request(
      `query GetPostsQuery {
        post(id: "${post_id}", idType: DATABASE_ID) {
          content
          title
        }
      }`
    )
    .then( response =>
    {
      if( typeof response.errors == 'undefined' && typeof response.data !== 'undefined' )
      {
        this.post = response.data.post;
      }
      else
      {
        console.log( 'Something went wrong! Please try again.' );
      }
    } )
  }
}

This code injects the saved JWT as a bearer authorization header into each HTTP request made by the user visiting the post page. To emphasize changes from the code’s previous iteration, new code is set off by comments.

Final Output: Achieving Dynamic and Secure UX

To confirm that restrictions are working properly, ensure you are not logged in and access the posts page. Next, attempt to access the post page. You should be redirected to the login page. Log in to view fetched content on the post page. If the app works as expected, you’ve effectively completed this tutorial and developed a decoupled, protected SPA.

In this digital age, providing a dynamic and secure user experience is an expectation, not an enhancement. The concepts and approaches explored in this tutorial can be applied to your next decoupled project to achieve scalability while offering developers flexibility in designing and delivering effective websites.


The editorial team of the Toptal Engineering Blog extends its gratitude to Branko Radulovic for reviewing the code samples and other technical content presented in this article.