How to Work with Angular and MySQL
The MySQL database has been a reliable workhorse for web applications for many years. It is the M in the LAMP stack, and powers a huge number of web servers across the world.
MySQL is also a relational database, with data stored in tables with strict data definitions. Rows in tables correspond to data entries and rows in one table can be referenced from another table through their index. Plenty of literature covers the fundamentals of SQL and best practices in designing relational databases for large applications.
Recently, MariaDB was created by some of the original developers of MySQL as an open-source alternative to MySQL, which is owned by Oracle. To keep all features available to the open-source community, MariaDB was created as a fork of MySQL. In fact, if you are using the default installation of MySQL on a modern Linux distribution, you are likely using MariaDB.
In this tutorial, I will show you how to develop an Angular application using MySQL. Instead of the old established LAMP stack, I will use a variant of the MEAN stack, where the M in MEAN refers to the MySQL database, with the Express.js framework to power the backend, and Angular.js for the client. The application will create a simple timeline in which the user can add and edit events, with a simple CRUD API.
I will assume that you have some familiarity with Node and the npm
package manager and that both of these are installed on your system.
Table of Contents
- Setting up MySQL
- Set up a Simple CRUD Node Express Server
- Run Your Express App and Connect to MySQL
- Authentication With JWT
- The Timeline Angular Client
- Run Your Angular Client Application
- Learn More About MySQL, Angular, and Node
Setting up MySQL
If you don’t have installed MySQL on your system, follow the instructions on the MySQL website to do so. Depending on your operating system, you might be able to use the system’s package manager to install the MySQL server.
Once you have installed MySQL you can create the database that you will use in this tutorial. Log into the database using the MySQL client using the login details of the database administrator.
$ mysql -u root -p
The -p
option indicates that you are required to supply a password when connecting to the MySQL server. You might not need this option, depending on how the database is set up on your system. You should now see the MySQL command line prompt.
mysql>
Create a new database and switch into it by using the following command.
create database timeline;
use timeline;
Next, create a user associated with this database. It is always a good idea to have separate users for each database on the system. The following commands will create the user and grant them all permissions on the timeline
database.
create user 'timeline'@'localhost' identified by 'password';
grant all on timeline.* to 'timeline'@'localhost';
In the first line, replace password
with a more complex password. The rest of this tutorial assumes you used “password”, so if you change it, please make sure and change it in all the code snippets below.
Now you are ready to create the events
table in the new database by specifying the data schema.
create table events (
id INT AUTO_INCREMENT,
owner VARCHAR(255) NOT NULL,
name VARCHAR(255) NOT NULL,
description TEXT,
date DATE,
PRIMARY KEY (id),
INDEX (owner, date)
);
The table contains several columns called id
, owner
, name
, description
, and date
. The id
column acts as the primary key for accessing individual rows. Since you will need to look up entries by owner
and date
, I added a secondary index using these two fields to speed up the lookup. This completes the setup of the database and you can now exit the MySQL client by using the quit
command.
Set up a Simple CRUD Node Express Server
To create the Express server, navigate into a directory of your choice and create a new folder called timeline-server
. Open a terminal in this directory and initialize the Node project using npm
.
npm init
Answer all the questions using the default answer and when asked about the entry point, set it to src/index.js
. Next, install some libraries you will need.
npm install --save-exact express@4.17.1 cors@2.8.5 mysql@2.18.1
Next, use your favorite editor and create a file src/index.js
and paste the following code into it.
const express = require('express');
const cors = require('cors');
const bodyParser = require('body-parser');
const mysql = require('mysql');
const events = require('./events');
const connection = mysql.createConnection({
host : 'localhost',
user : 'timeline',
password : 'password',
database : 'timeline'
});
connection.connect();
const port = process.env.PORT || 8080;
const app = express()
.use(cors())
.use(bodyParser.json())
.use(events(connection));
app.listen(port, () => {
console.log(`Express server listening on port ${port}`);
});
At the top of the file, you can see several imports. express
contains the Express framework for creating REST APIs, cors
is a middleware responsible for allowing cross-origin requests, and body-parser
makes sure that the bodies of the incoming requests are read and attached to the request object.
The mysql
object allows us to connect to your MySQL database and is seen in the code immediately below the require
statements. In the options to createConnection
, you will need to replace password
with the password that you have stored in your MySQL server above. In the bottom part of src/index.js
, the Express server is configured with the middleware and the events
router and then started.
The last require
statement imports the events
router. To create the router, create a new file called src/events.js
and paste the following code.
const express = require('express');
function createRouter(db) {
const router = express.Router();
const owner = '';
// the routes are defined here
return router;
}
module.exports = createRouter;
This piece of code defines a function that takes the database connection as an argument and creates a router. I deliberately left the main part of the function blank. Soon, you will define the individual routes of the REST API in this block. I also defined an empty string called owner
to use as a placeholder for the owner
column in the database. In the next section, I will show you how to obtain the user’s email address and use this to identify the owner.
The first route will insert new events into the database. Paste the following into the body of the createRouter()
function.
router.post('/event', (req, res, next) => {
db.query(
'INSERT INTO events (owner, name, description, date) VALUES (?,?,?,?)',
[owner, req.body.name, req.body.description, new Date(req.body.date)],
(error) => {
if (error) {
console.error(error);
res.status(500).json({status: 'error'});
} else {
res.status(200).json({status: 'ok'});
}
}
);
});
By using router.post
this route will only activate when the server receives an HTTP POST request. The data posted in the request can be obtained through the req.body
object. Using the SQL INSERT
statement, this code adds a new row into the events
table.
To list the events of a single owner, the following route links an HTTP GET request to a MySQL SELECT
statement. You can paste this code into the body of the createRouter()
function right after the previous route.
router.get('/event', function (req, res, next) {
db.query(
'SELECT id, name, description, date FROM events WHERE owner=? ORDER BY date LIMIT 10 OFFSET ?',
[owner, 10*(req.params.page || 0)],
(error, results) => {
if (error) {
console.log(error);
res.status(500).json({status: 'error'});
} else {
res.status(200).json(results);
}
}
);
});
You can also modify existing entries in the database with a PUT route, which links an HTTP PUT request to a MySQL UPDATE
statement.
router.put('/event/:id', function (req, res, next) {
db.query(
'UPDATE events SET name=?, description=?, date=? WHERE id=? AND owner=?',
[req.body.name, req.body.description, new Date(req.body.date), req.params.id, owner],
(error) => {
if (error) {
res.status(500).json({status: 'error'});
} else {
res.status(200).json({status: 'ok'});
}
}
);
});
The code obtains the id
as a route parameter, from the req.params
object.
Finally, you might want to delete an existing event. Do this through an HTTP DELETE request that issues a MySQL DELETE
statement.
router.delete('/event/:id', function (req, res, next) {
db.query(
'DELETE FROM events WHERE id=? AND owner=?',
[req.params.id, owner],
(error) => {
if (error) {
res.status(500).json({status: 'error'});
} else {
res.status(200).json({status: 'ok'});
}
}
);
});
Run Your Express App and Connect to MySQL
The code above implements a fully functioning server. You can run this server by opening the terminal in the project directory and running the following command.
node src/index.js
You might see the following error when you run this command:
Error: ER_NOT_SUPPORTED_AUTH_MODE: Client does not support authentication protocol requested by server; consider upgrading MySQL client
To fix this, log into MySQL and execute the following SQL (where password
is the value you used for your password):
ALTER USER 'timeline'@'localhost' IDENTIFIED WITH mysql_native_password BY 'password';
Authentication With JWT
In this section, I will show you how to use JWT and Okta to authenticate users to your application. User authentication is an important part of any web app, but developers often underestimate the effort required to safely implement and maintain secure authentication. With Okta, you can offload all the complexity and security considerations to an external provider.
Before you begin, you’ll need a free Okta developer account. Install the Okta CLI and run okta register
to sign up for a new account. If you already have an account, run okta login
.
Then, run okta apps create
. Select the default app name, or change it as you see fit.
Choose Single-Page App and press Enter.
Use http://localhost:4200/callback
for the Redirect URI and accept the default Logout Redirect URI of http://localhost:4200
.
What does the Okta CLI do?
The Okta CLI will create an OIDC Single-Page App in your Okta Org. It will add the redirect URIs you specified and grant access to the Everyone group. It will also add a trusted origin for http://localhost:4200
. You will see output like the following when it’s finished:
Okta application configuration:
Issuer: https://dev-133337.okta.com/oauth2/default
Client ID: 0oab8eb55Kb9jdMIr5d6
NOTE: You can also use the Okta Admin Console to create your app. See Create an Angular App for more information.
To use Okta authentication in your server app, you need to install some additional libraries. In the terminal run the following command:
npm install --save-exact express-bearer-token@2.4.0 @okta/jwt-verifier@2.1.0
Next, create a new file src/auth.js
and paste the following code.
const OktaJwtVerifier = require('@okta/jwt-verifier');
const oktaJwtVerifier = new OktaJwtVerifier({
clientId: '{yourClientId}',
issuer: 'https://{yourOktaDomain}/oauth2/default'
});
async function oktaAuth(req, res, next) {
try {
const token = req.token;
if (!token) {
return res.status(401).send('Not Authorized');
}
const jwt = await oktaJwtVerifier.verifyAccessToken(token, ['api://default']);
req.user = {
uid: jwt.claims.uid,
email: jwt.claims.sub
};
next();
}
catch (err) {
console.log('AUTH ERROR: ', err);
return res.status(401).send(err.message);
}
}
module.exports = oktaAuth;
This module defines an Express middleware that reads a token from the request and verifies it using the Okta JWT Verifier. In the code above {yourClientId}
is the client ID you obtained from the Okta application settings and {yourOktaDomain}
is your Okta domain. When the user authenticates successfully, a user
object containing the user ID and email will be added to the request.
Open src/index.js
again and add the following require
statements to the top of the file.
const bearerToken = require('express-bearer-token');
const oktaAuth = require('./auth');
Now modify the Express application configuration to include the bearerToken
and oktaAuth
middlewares.
const app = express()
.use(cors())
.use(bodyParser.json())
.use(bearerToken())
.use(oktaAuth)
.use(events(connection));
Finally, modify the event
route to make use of the user email. In src/events.js
, remove the line const owner = '';
and add const owner = req.user.email;
to the first line in each router block:
router.post('/event', (req, res, next) => {
const owner = req.user.email;
// db.query() code
});
router.get('/event', function (req, res, next) {
const owner = req.user.email;
// db.query() code
});
router.put('/event/:id', function (req, res, next) {
const owner = req.user.email;
// db.query() code
});
router.delete('/event/:id', function (req, res, next) {
const owner = req.user.email;
// db.query() code
});
These changes secure your server application. It also stores events for users separately and each user will only see their own events.
The Timeline Angular Client
Now it’s time to implement the client application, based on Angular 8 with the ngx-bootstrap
library for responsive layout and standard components. You will also use a free icon set called Line Awesome, a variant of the well known Font Awesome that replaces the standard icon designs with some stylish line icons. You will also use the ngx-timeline
library to make it easy to create beautiful vertical timelines.
To start, you will need to install the latest version of the Angular CLI tool. In a terminal, type the following command.
npm install -g @angular/cli@8
Depending on your system, you might have to run this command using sudo
to allow modification of system resources. Next, navigate into a directory of your choice and create a new Angular application.
ng new timeline-client
You will be asked a number of questions about your applications. Make sure that you add the Angular routing module.
? Would you like to add Angular routing? (y/N) Y
When asked which stylesheet format to use, choose the default CSS
option. Now change into the new folder timeline-client
and install some additional packages. Add Bootstrap with the ng add
command.
ng add ngx-bootstrap@5.1.0
To add the timeline library and Okta’s Angular SDK, run the following command:
npm install --save-exact ngx-timeline@5.0.0 @okta/okta-angular@3.0.1
Now start your IDE and open up the file src/index.html
. Here, add some external CSS files inside the <head>
tags to add styles for Bootstrap and Line Awesome:
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://maxcdn.icons8.com/fonts/line-awesome/1.1/css/line-awesome-font-awesome.min.css">
Open src/app/app.component.ts
and replace the contents with the following code.
import { Component, OnInit } from '@angular/core';
import { OktaAuthService } from '@okta/okta-angular';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
title = 'timeline-client';
isAuthenticated: boolean;
constructor(public oktaAuth: OktaAuthService) {
// subscribe to authentication state changes
this.oktaAuth.$authenticationState.subscribe(
(isAuthenticated: boolean) => this.isAuthenticated = isAuthenticated
);
}
async ngOnInit() {
// get authentication state for immediate use
this.isAuthenticated = await this.oktaAuth.isAuthenticated();
}
async login() {
await this.oktaAuth.signInWithRedirect();
}
async logout() {
await this.oktaAuth.signOut();
}
}
The AppComponent
handles the authentication through Okta. OktaAuthService.isAuthenticated()
initializes the isAuthenticated
field of the component. The field is kept up to date by subscribing to the OktaAuthService.$authenticationState
observable. OktaAuthService.signInWithRedirect()
will trigger a browser redirect to the Okta login page and OktaAuthService.signOut()
will log out the user.
Now open the template of the application component src/app/app.component.html
and paste in the following HTML.
<nav class="navbar navbar-expand navbar-light bg-light">
<a class="navbar-brand" [routerLink]="['']">
<i class="fa fa-clock-o"></i>
</a>
<ul class="navbar-nav mr-auto">
<li class="nav-item">
<a class="nav-link" [routerLink]="['']">
Home
</a>
</li>
<li class="nav-item">
<a class="nav-link" [routerLink]="['timeline']">
Timeline
</a>
</li>
</ul>
<span>
<button class="btn btn-primary" *ngIf="!isAuthenticated" (click)="login()"> Login </button>
<button class="btn btn-primary" *ngIf="isAuthenticated" (click)="logout()"> Logout </button>
</span>
</nav>
<router-outlet></router-outlet>
The bootstrap navigation bar contains a menu with links to the home page and a timeline
route. It also contains a login button and a logout button that appear depending on the authentication state.
In the next step, you need to import the component modules into src/app/app.module.ts
. It looks like this.
import { BrowserModule } from '@angular/platform-browser';
import { HttpClientModule } from '@angular/common/http';
import { NgModule } from '@angular/core';
import { BsDatepickerModule } from 'ngx-bootstrap/datepicker';
import { NgxTimelineModule } from 'ngx-timeline';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { ModalModule } from 'ngx-bootstrap/modal';
import { HomeComponent } from './home/home.component';
import { TimelineComponent } from './timeline/timeline.component';
import { OKTA_CONFIG, OktaAuthModule } from '@okta/okta-angular';
const oktaConfig = {
issuer: 'https://{yourOktaDomain}/oauth2/default',
clientId: '{clientId}',
redirectUri: window.location.origin + '/callback'
};
@NgModule({
declarations: [
AppComponent,
HomeComponent,
TimelineComponent
],
imports: [
BrowserModule,
HttpClientModule,
AppRoutingModule,
BrowserAnimationsModule,
FormsModule,
ReactiveFormsModule,
BsDatepickerModule.forRoot(),
NgxTimelineModule,
ModalModule.forRoot(),
OktaAuthModule
],
providers: [{ provide: OKTA_CONFIG, useValue: oktaConfig }],
bootstrap: [AppComponent]
})
export class AppModule { }
Replace{yourClientId}
with your client ID and {yourOktaDomain}
with your Okta domain. If you closed your terminal window, you can run okta apps
to see your apps and okta apps config --app=<clientId>
to get your app’s info.
Next, create two components for the home page and the timeline page, as well as a service to connect to the server.
ng generate component home
ng generate component timeline
ng generate service server
The home page will simply contain a heading and no other functionality. Open src/app/home/home.component.html
and replace the contents with the following.
<div class="container">
<div class="row">
<div class="col-sm">
<h1>Angular MySQL Timeline</h1>
</div>
</div>
</div>
Now open src/app/home/home.component.css
and add the following styles.
h1 {
margin-top: 50px;
text-align: center;
}
Before you implement the timeline component, let’s take a look at the ServerService
first. Open src/app/server.service.ts
and replace the contents with the following code.
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { OktaAuthService } from '@okta/okta-angular';
import { environment } from '../environments/environment';
@Injectable({
providedIn: 'root'
})
export class ServerService {
constructor(private http: HttpClient, public oktaAuth: OktaAuthService) {
}
private async request(method: string, url: string, data?: any) {
const token = await this.oktaAuth.getAccessToken();
const result = this.http.request(method, url, {
body: data,
responseType: 'json',
observe: 'body',
headers: {
Authorization: `Bearer ${token}`
}
});
return new Promise((resolve, reject) => {
result.subscribe(resolve, reject);
});
}
getEvents() {
return this.request('GET', `${environment.serverUrl}/event`);
}
createEvent(event) {
return this.request('POST', `${environment.serverUrl}/event`, event);
}
updateEvent(event) {
return this.request('PUT', `${environment.serverUrl}/event/${event.id}`, event);
}
deleteEvent(event) {
return this.request('DELETE', `${environment.serverUrl}/event/${event.id}`);
}
}
The main logic of this component resides in the request
method, which sends a request to the server. To identify the user to the server, the request header contains a bearer token obtained from OktaAuthService
. The method returns a Promise
that resolves once a response from the server has been obtained.
The remainder of the ServerService
uses the request
method to call the server routes. The server URL is provided through the environment object, which you need to define in src/environments/environment.ts
.
export const environment = {
production: false,
serverUrl: 'http://localhost:8080'
};
The comments in src/environments/environment.ts
provide useful hints about how to use environments in Angular.
Now you are ready to implement the timeline component. Open src/app/timeline/timeline.component.html
and edit the contents to match the code below.
<div class="container page-content">
<div class="row">
<div class="col-sm-12 col-md">
<ngx-timeline [events]="events">
<ng-template let-event let-index="rowIndex" timelineBody>
<div>{{event.body}}</div>
<div class="button-row">
<button type="button" class="btn btn-primary" (click)="editEvent(index, eventmodal)"><i class="fa fa-edit"></i></button>
<button type="button" class="btn btn-danger" (click)="deleteEvent(index)"><i class="fa fa-trash"></i></button>
</div>
</ng-template>
</ngx-timeline>
</div>
<div class="col-md-2">
<button type="button" class="btn btn-primary" (click)="addEvent(eventmodal)"><i class="fa fa-plus"></i> Add</button>
</div>
</div>
</div>
This template uses the ngx-timeline
component to display an array of events in a vertical timeline, with buttons for adding and editing events. Some of these actions will open up a modal window. ngx-bootstrap
allows you to simply create a modal window from an ng-template
component. Paste the following code into the bottom of the file.
<ng-template #eventmodal>
<div class="modal-header">
<h4 class="modal-title pull-left">Event</h4>
<button type="button" class="close pull-right" aria-label="Close" (click)="modalRef.hide()">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<div class="form-group full-width-input">
<label>Name</label>
<input class="form-control" placeholder="Event Name" formControlName="name" required>
</div>
<div class="form-group full-width-input">
<label>Description</label>
<input class="form-control" formControlName="description">
</div>
<div class="form-group full-width-input">
<label>Date</label>
<input class="form-control" formControlName="date" bsDatepicker>
</div>
<div class="button-row">
<button type="button" class="btn btn-primary" (click)="modalCallback()">Submit</button>
<button type="button" class="btn btn-light" (click)="onCancel()">Cancel</button>
</div>
</form>
</div>
</ng-template>
Only a small amount of styling in src/app/timeline/timeline.component.css
rounds off the design.
.page-content {
margin-top: 2rem;
}
.button-row {
display: flex;
justify-content: space-between;
margin-top: 1rem;
}
The src/app/timeline/timeline.component.ts
file contains the bulk of the application logic.
import { Component, OnInit } from '@angular/core';
import { BsModalService, BsModalRef } from 'ngx-bootstrap/modal';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { ServerService } from '../server.service';
@Component({
selector: 'app-timeline',
templateUrl: './timeline.component.html',
styleUrls: ['./timeline.component.css']
})
export class TimelineComponent implements OnInit {
form: FormGroup;
modalRef: BsModalRef;
events: any[] = [];
currentEvent: any = {id: null, name: '', description: '', date: new Date()};
modalCallback: () => void;
constructor(private fb: FormBuilder,
private modalService: BsModalService,
private server: ServerService) { }
ngOnInit() {
this.form = this.fb.group({
name: [this.currentEvent.name, Validators.required],
description: this.currentEvent.description,
date: [this.currentEvent.date, Validators.required],
});
this.getEvents();
}
private updateForm() {
this.form.setValue({
name: this.currentEvent.name,
description: this.currentEvent.description,
date: new Date(this.currentEvent.date)
});
}
private getEvents() {
this.server.getEvents().then((response: any) => {
console.log('Response', response);
this.events = response.map((ev) => {
ev.body = ev.description;
ev.header = ev.name;
ev.icon = 'fa-clock-o';
return ev;
});
});
}
addEvent(template) {
this.currentEvent = {id: null, name: '', description: '', date: new Date()};
this.updateForm();
this.modalCallback = this.createEvent.bind(this);
this.modalRef = this.modalService.show(template);
}
createEvent() {
const newEvent = {
name: this.form.get('name').value,
description: this.form.get('description').value,
date: this.form.get('date').value,
};
this.modalRef.hide();
this.server.createEvent(newEvent).then(() => {
this.getEvents();
});
}
editEvent(index, template) {
this.currentEvent = this.events[index];
this.updateForm();
this.modalCallback = this.updateEvent.bind(this);
this.modalRef = this.modalService.show(template);
}
updateEvent() {
const eventData = {
id: this.currentEvent.id,
name: this.form.get('name').value,
description: this.form.get('description').value,
date: this.form.get('date').value,
};
this.modalRef.hide();
this.server.updateEvent(eventData).then(() => {
this.getEvents();
});
}
deleteEvent(index) {
this.server.deleteEvent(this.events[index]).then(() => {
this.getEvents();
});
}
onCancel() {
this.modalRef.hide();
}
}
I will not go into every detail of this code. The methods addEvent()
, editEvent()
, and deleteEvent()
are the event handlers for the respective HTML buttons. addEvent()
and editEvent()
open the modal window and set either createEvent()
or updateEvent()
as a callback function when the user submits data. These use the ServerService
to complete the request.
Finally, link the components to the routes. Open src/app/app-routing.module.ts
and add the following import statements to the top of the file.
import { HomeComponent } from './home/home.component';
import { TimelineComponent } from './timeline/timeline.component';
import { OktaCallbackComponent, OktaAuthGuard } from '@okta/okta-angular';
Next, replace the routes
array with the following.
const routes: Routes = [
{
path: '',
component: HomeComponent
},
{
path: 'timeline',
component: TimelineComponent,
canActivate: [OktaAuthGuard]
},
{ path: 'callback', component: OktaCallbackComponent }
];
The first entry sets the base route and links it to the HomeComponent
. The second entry links the timeline
route to the TimelineComponent
and prevents unauthorized access to the route with OktaAuthGuard
. When a user authenticates via the Okta service, they will be redirected to the /callback
route.
Run Your Angular Client Application
This completes the implementation of the client. You can start the client from the terminal by running this command.
ng serve
Make sure that you have also started the server as described in the previous section. Open a browser to http://localhost:4200
. Log in, click on Timeline in the top menu, and add events to your heart’s content.
I added some of my favorite historical events. You can see how this looks in the screenshot below.
Learn More About MySQL, Angular, and Node
In this tutorial, I have shown you how to implement a web application with Angular and MySQL using a server based on Express. Using the ngx-timeline library you created beautiful timelines. When written from scratch, authentication requires solid up-to-date knowledge about security issues. Using Okta authentication and helper libraries lets you implement authentication in just a few lines.
You can find the code created in this tutorial on GitHub at okta-angular-mysql-timeline-example.
If you want to learn more about Angular, MySQL, and authentication, check out the links below.
- Use the Okta CLI to Quickly Build Secure Angular Apps
- Build a Basic CRUD App with Angular and Node
- Build Secure Login for Your Angular App
- Build a Node.js API with TypeScript
- Modern Token Authentication in Node with Express
If you’d like to be notified when we publish new posts like this one, please follow @oktadev on Twitter. We also publish videos to our YouTube channel.
Changelog:
- Mar 29, 2021: Updated to use the latest versions of Okta’s JWT Verifier and Angular SDK. You can see the changes in the example app on GitHub. Changes to this article can be viewed in oktadeveloper/okta-blog#634.
Okta Developer Blog Comment Policy
We welcome relevant and respectful comments. Off-topic comments may be removed.