express logo
typescript logo

Node.js API Authorization By Example: TypeScript Edition

Updated on January 30, 2023
Photo of Dan Arias
Dan AriasStaff Developer Advocate
Languages
JavaScript
TypeScript

This TypeScript guide will help you learn how to secure an Express.js API using token-based authorization. You'll learn how to integrate Auth0 by Okta with Express.js to implement the following security features:

  • Use Express.js middleware to enforce API security policies.
  • Perform access control in Express.js using a token-based authorization strategy powered by JSON Web Tokens (JWTs).
  • Validate access tokens in JSON Web Token (JWT) format using Express.js middleware.
  • Make authenticated requests to a secure Express.js API server.

This guide uses the express-oauth2-jwt-bearer library, which provides developers with an authentication middleware for Express.js that validates access tokens that follow the JSON Web Token (JWT) format. You can now secure your Express.js APIs following security best practices while writing less code.

Why Use TypeScript with Express.js?

TypeScript is a strongly typed programming language that builds on JavaScript. Using TypeScript with Express.js gives you access to optional static type-checking, robust tooling, and the latest ECMAScript features. TypeScript can help developers build faster and more safely by helping them find bugs early in the development process. Support for TypeScript has grown exponentially in the past years across libraries and frameworks, making this one of the fastest-growing programming languages.

How Does Auth0 Work?

With the help of Auth0 by Okta, you don't need to be an expert on identity protocols, such as OAuth 2.0 or OpenID Connect, to understand how to secure your web application stack.

You first integrate your client applications with Auth0. Your application will then redirect users to an Auth0 customizable login page when they need to log in. Once your users log in successfully, Auth0 redirects them to your client app, returning an access token. The client application can then use that access token to authenticate itself with your Express.js API and access protected resources in your server or database layers.

Get the Starter Project

We have created a starter project to help you learn Express.js security concepts through hands-on practice. You can focus on building Express.js middleware and TypeScript services to secure your application.

Start by cloning the api_express_typescript_hello-world repository on its starter branch:

COMMAND
git clone -b starter git@github.com:auth0-developer-hub/api_express_typescript_hello-world.git

Once you clone the repo, make api_express_typescript_hello-world your current directory:

COMMAND
cd api_express_typescript_hello-world

Install the Express.js project dependencies as follows:

COMMAND
npm install

The starter Express.js project defines the following API endpoints to let client applications access a simple message resource:

ENDPOINTS
# get a public message
GET /api/messages/public
# get a protected message
GET /api/messages/protected
# get an admin message
GET /api/messages/admin

The starter project does not implement access control for these endpoints. After you follow the steps in this guide, the API server will require each request to have a valid access token on their authorization header to access GET /api/messages/protected and GET /api/messages/admin. GET /api/messages/public would be the only public endpoint. Later, you'll integrate this Express.js API with an actual client application using a frontend technology of your choice.

You'll run your Express.js server on port 6060 locally. The compatible demo client application runs on http://localhost:4040 by default. However, this Express.js starter application uses CORS to restrict which origins can request its resources.

You'll manage these configuration elements of your Express.js application using environment variables. As such, create a .env file under the root project directory:

COMMAND
touch .env

Populate your .env file with the following content:

.env
PORT=6060
CLIENT_ORIGIN_URL=http://localhost:4040

The PORT environment variable defines the port in which your Express.js API server will run: http://localhost:6060. The CLIENT_ORIGIN_URL variable defines the origin from which a browser can load resources from your Express.js API — other than your API's origin.

Finally, open another terminal tab and execute this command to run your Express.js application in development mode, which uses ts-node-dev to restart your server anytime that your project source code changes:

COMMAND
npm run dev

You are ready to start implementing authorization in this Express.js project. First, you'll need to configure the Express.js API server to connect successfully to Auth0. Afterward, you'll use the express-oauth2-jwt-bearer middleware to validate bearer tokens from incoming API requests.

Configure Express.js with Auth0

Follow these steps to get started with the Auth0 Identity Platform quickly:

Sign up and create an Auth0 Application

A free account also offers you:

During the sign-up process, you create something called an Auth0 Tenant, representing the product or service to which you are adding authentication.

Once you sign in, Auth0 takes you to the Dashboard. Open the "APIs" section of the Auth0 Dashboard.

Then, click the "Create API" button. A modal opens with a form to provide a name for the API registration and define an API Identifier. Use the following values:

Name
Auth0 Express.js Code Sample
Identifier
https://hello-world.example.com

Click the "Create" button to complete the process. Your Auth0 API registration page loads up.

View 'Register APIs' document for more details.

A page loads up, presenting all the details about your application register with Auth0.

Next, locate the "General Settings" section.

Store the "Identifier" value in the following fields to set up your Express.js API server in the next section:

In the next step, you'll learn how to help Express.js and Auth0 communicate.

What's the relationship between Auth0 Tenants and Auth0 Applications?

Let's say that you have a photo-sharing app called "Expressigram". You then would create an Auth0 tenant called expressigram. From a customer perspective, Expressigram is that customer's product or service.

Now, say that Expressigram is available on three platforms: web as a single-page application and Android and iOS as a native mobile application. If each platform needs authentication, you need to create three Auth0 applications to provide the product with everything it needs to authenticate users through that platform.

Expressigram users belong to the Auth0 Expressigram tenant, which shares them across its Auth0 applications.

Create a communication bridge between Express.js and Auth0

In this guide, you'll implement token-based authorization. That is, your Express.js API server will protect an endpoint by requiring that each request to that endpoint contains a valid access token.

The issuer of those access tokens will be an Auth0 authorization server. Your Express.js API server needs to validate that the access token on a request comes from Auth0.

You'll need the Auth0 Domain and Auth0 Audience values to validate the access tokens.

When setting up APIs in the Auth0 Dashboard, we also refer to the API identifier as the Audience value, which you have already set up in the previous section.

Get the Auth0 domain

Now, follow these steps to get the Auth0 Domain value.

  • Open the Auth0 Domain Settings

  • Locate the bold text in the page description that follows this pattern: tenant-name.region.auth0.com. That's your Auth0 domain!

  • Paste the Auth0 domain value in the following field so that you can use it in the next section to set up your API server:

The region subdomain (au, us, or eu) is optional. Some Auth0 Domains don't have it.

Get an Auth0 access token

You'll also need a test access token to practice making secure calls to your API from a terminal application.

You can get a test access token from the Auth0 Dashboard by following these steps:

  • Head back to the Auth0 registration page of your Express.js API and click the "Test" tab.
If this is the first time that you are setting up a testing application, click on the "Create & Authorize Test Application" button.
  • Locate the section called "Response" and click the copy button in the top-right corner.

  • Paste the access token value in the following field so that you can use it in the next sections to test your API server:

When you enter a value in the input fields present on this page, any code snippet that uses such value updates to reflect it. Using the input fields makes it easy to copy and paste code as you follow along.

For security, these configuration values are stored in memory and only used locally. They are gone as soon as you refresh the page! As an extra precaution, you should use values from an Auth0 test application instead of a production one.

Define the Auth0 Environment Variables

Update the .env file under the project directory as follows to integrate the Auth0 Domain and Auth0 Audience values with your Express.js application:

.env
PORT=6060
CLIENT_ORIGIN_URL=http://localhost:4040
AUTH0_AUDIENCE=AUTH0-AUDIENCE
AUTH0_DOMAIN=AUTH0-DOMAIN

Restart your development server for your Express.js project to become aware of these .env file changes:

COMMAND
npm run dev

Once you reach the "Request Express.js API Resources From a Client App" section of this guide, you'll learn how to use CLIENT_ORIGIN_URL along with an Auth0 Audience value to request protected resources from your Express.js API using a client application that is also protected by Auth0.

Install and Set Up the Authorization Middleware

Auth0 issues access tokens using the JWT format. You'll use the express-oauth2-jwt-bearer authorization middleware for Express.js to validate access tokens.

Install the npm package that bundles the Express.js authorization middleware:

COMMAND
npm install express-oauth2-jwt-bearer

You'll integrate the express-oauth2-jwt-bearer authentication middleware function with your Express API route handlers to limit who can access them. Express will execute this authentication middleware function before it executes the callback function that handles the request.

As you are restricting access to protect resources to requests with a valid access token, you are implementing basic authorization in your Express API endpoints. In this scenario, authorization is a byproduct of authentication. Your users authenticate themselves with Auth0 using your client application to get an access token. An access token is a form of credentials for the client application to authenticate itself with your resource server (your API server).

The access token may contain information that your API server can use to grant access to a resource. However, your API server may require the presence of a valid access token as the only authorization condition for accessing a resource. You can use Auth0 Role-Based Access Control (RBAC) to use permissions to increase those authorization requirements.

You can use two patterns to integrate your endpoints with the authentication middleware function.

Enforce authorization requirements for a group of endpoints

You can separate the Express.js public router handlers from the protected router handlers using the authentication middleware as a boundary between groups. For example, within an Express router, you could do the following:

import { auth } from "express-oauth2-jwt-bearer";
// Public API endpoints
router.get(...);
// Protected API endpoints
router.use(auth());
router.post(...);
router.put(...);
router.delete(...);

As such, client applications can access the GET endpoint without presenting any "proof of authorization" — it is a public endpoint.

However, client requests can only access endpoints that you define after your application mounts auth() into router if auth() can determine that the client making the endpoint request has the authorization requirements to access it. For this API, Auth0 provides the proof of authorization mentioned in the form of a JSON Web Token (JWT) called an access token.

If you needed to protect all the endpoints of the application at once, you could also mount the auth() middleware function into the app directly:

import { auth } from "express-oauth2-jwt-bearer";
app.use(auth());

Enforce authorization requirements per endpoint

You can also "inject" the authentication middleware function in the router handler as follows:

import { auth } from "express-oauth2-jwt-bearer";
router.post(
"/protected",
auth,
async (req, res) => {
// Router handler logic...
}
);

Here, Express calls the auth() middleware function before it calls the (req, res) => { ... } callback function that handles the business logic of the route. The business logic within auth() performs two tasks to protect the Express.js route:

(a) If auth() can determine that the request includes a valid access token, it invokes the next function in the middleware chain, the router handler function, otherwise...

(b) If auth() can't find or validate an access token in the request, it closes the request-response cycle by responding with a 401 (Unauthorized) HTTP error, which prevents your API from executing the route handler.

For this Express.js project, you'll add authorization middleware per router handler to have more granular control of the authorization flow.

Protect Express.js API Endpoints

You'll create an Auth0 module to define middleware functions that can help you carry out the authorization process in your Express.js application.

Create an Auth0 module

You have to instantiate the auth() middleware function in any module where you want to use it. However, what if your project uses different Express.js applications or routers to organize how it handles requests and executes business logic?

The starter Express.js project uses that approach. It uses directories under the src directory to bundle application features. The messages directory hosts all the modules the Express.js application needs to handle requests to its message resources, such as a message service and a message router.

As such, it would be better to create a module to instantiate the auth() middleware function from express-oauth2-jwt-bearer. That way, you only need to instantiate auth() once. Any module that needs its functionality can import it.

Create an auth0.middleware.ts file under the src/middleware to define an Auth0 module that bundles middleware functions related to the authorization process of your Express.js API:

COMMAND
touch src/middleware/auth0.middleware.ts

Populate src/middleware/auth0.middleware.ts as follows:

src/middleware/auth0.middleware.ts
import * as dotenv from "dotenv";
import { auth } from "express-oauth2-jwt-bearer";
dotenv.config();
export const validateAccessToken = auth({
issuerBaseURL: `https://${process.env.AUTH0_DOMAIN}`,
audience: process.env.AUTH0_AUDIENCE,
});

You need two values to create an instance of auth(), issuerBaseURL and audience, which you can get from your .env file environment variables using dotenv.config().

You store the auth() instance into the validateAccessToken() function, which provides the instance a declarative name based on the core functionality it provides.

Implement access token validation in API routes

Next, update the src/messages/messages.router.ts file to import validateAccessToken() and apply it to the /protected and /admin routes:

src/messages/messages.router.ts
import express from "express";
import { validateAccessToken } from "../middleware/auth0.middleware";
import {
getAdminMessage,
getProtectedMessage,
getPublicMessage,
} from "./messages.service";
export const messagesRouter = express.Router();
messagesRouter.get("/public", (req, res) => {
const message = getPublicMessage();
res.status(200).json(message);
});
messagesRouter.get("/protected", validateAccessToken, (req, res) => {
const message = getProtectedMessage();
res.status(200).json(message);
});
messagesRouter.get("/admin", validateAccessToken, (req, res) => {
const message = getAdminMessage();
res.status(200).json(message);
});

That's it! Every request you make using a compatible "Hello World" client application or a terminal application will need to include a valid access token to access the protected Express.js routes.

Handle Authorization Exceptions

In Express.js, the order in which you declare and invoke middleware is essential for the architecture of your application. What should happen when a client makes an API request without an access token to a protected API endpoint? The ideal behavior is to respond to the client with a 401 (Unauthorized) status code.

The started Express.js project already includes middleware functions that handle server errors, 500 (Internal Server Error), and server requests that don't match any server routes, 404 (Not Found). The project defines these error-handling middleware functions in the src/middleware/error.middleware.ts and src/middleware/not-found.middleware.ts files, respectively.

You can extend the functionality of the errorHandler() middleware function defined in the src/middleware/error.middleware.ts to handle any errors related to unauthorized access. Update that file as follows:

src/middleware/error.middleware.ts
import { Request, Response, NextFunction } from "express";
import {
InvalidTokenError,
UnauthorizedError,
} from "express-oauth2-jwt-bearer";
export const errorHandler = (
error: any,
request: Request,
response: Response,
next: NextFunction
) => {
if (error instanceof InvalidTokenError) {
const message = "Bad credentials";
response.status(error.status).json({ message });
return;
}
if (error instanceof UnauthorizedError) {
const message = "Requires authentication";
response.status(error.status).json({ message });
return;
}
const status = 500;
const message = "Internal Server Error";
response.status(status).json({ message });
};

The auth() authentication middleware function from the express-oauth2-jwt-bearer library will yield an error object when it cannot validate an incoming request. The error object has the following properties:

  • status: A property that defines the HTTP status code you can use to reply to the client.
  • message: A property that describes the error that took place.

When the auth() middleware cannot get the access token from the request it will yield an UnauthorizedError object with a 401 status and an Unauthorized message. However, when the access token is invalid the middleware will yield an InvalidTokenError object with a 401 status and a message that describes why the validation failed.

When implementing error handling in APIs, OWASP recommends that developers gracefully handle all possible errors and only respond with error information that is helpful to the user without revealing unnecessary internal details.

The update to the errorHandler middleware function customizes the message response based on the instance of the error object that the auth() middleware throws. You preserve the 401 HTTP status code but you change the response message to Requires authentication when it cannot get the access token from the request and Bad credentials when the access token is invalid.

According to OWASP, improper error handling can introduce various security problems for an application. Sending detailed internal error messages, such as stack traces and error codes, to the user may reveal implementation details that you should never reveal openly to the public. A malicious user could use those details to gain important clues on the potential security flaws of your application.

Test Your Protected API Endpoints

Use an application like Postman or a terminal to test that your API server is working as prescribed.

As an anonymous user:

Make API requests to your Express.js API server without sending any access token:

Request:

COMMAND
curl localhost:6060/api/messages/public

Response:

Status code: 200

{
"text": "This is a public message."
}

Request:

COMMAND
curl localhost:6060/api/messages/protected

Response:

Status code: 401

{
"message": "Requires authentication"
}

Request:

COMMAND
curl localhost:6060/api/messages/admin

Response:

Status code: 401

{
"message": "Requires authentication"
}

Request:

COMMAND
curl localhost:6060/api/messages/invalid

Response:

Status code: 404

{
"message": "Not Found"
}

Any other errors as needed if no other status codes apply:

Response:

Status code: 500

{
"message": "Relevant description message"
}

As an authenticated user or with a valid test access token:

Make an authenticated request to your Express.js API server by including an access token in the authorization header:

You learned on the "Configure Express.js with Auth0" section how to get a test access token. You will use that access token in this section. The access token value should auto-populate if you added it to the corresponding field during the configuration steps.

Request:

COMMAND
curl --request GET \
--url http:/localhost:6060/api/messages/public \
--header 'authorization: Bearer AUTH0-ACCESS-TOKEN'

Response:

Status code: 200

{
"text": "This is a public message."
}

Request:

COMMAND
curl --request GET \
--url http:/localhost:6060/api/messages/protected \
--header 'authorization: Bearer AUTH0-ACCESS-TOKEN'

Response:

Status code: 200

{
"text": "This is a protected message."
}

Request:

COMMAND
curl --request GET \
--url http:/localhost:6060/api/messages/admin \
--header 'authorization: Bearer AUTH0-ACCESS-TOKEN'

Response:

Status code: 200

{
"text": "This is an admin message."
}

Request:

COMMAND
curl --request GET \
--url http:/localhost:6060/api/messages/invalid \
--header 'authorization: Bearer AUTH0-ACCESS-TOKEN'

Response:

Status code: 404

{
"message": "Not Found"
}

Any other errors as needed if no other status codes apply:

Response:

Status code: 500

{
"message": "Relevant message"
}

When using an invalid test access token:

Request:

COMMAND
curl --request GET \
--url http:/localhost:6060/api/messages/protected \
--header 'authorization: Bearer invalidtoken1234567890'

Response:

Status code: 401

{
"message": "Bad credentials"
}

Request Express.js API Resources From a Client App

Let's simulate an essential feature of an API: serving data to client applications.

You can pair this API server with a client application that matches the technology stack that you use at work. Any "Hello World" client application can communicate with this "Hello World" API server sample.

You can simulate a secure full-stack application system in no time. Each client application sample gives you clear instructions to quickly get it up and running.

Once set up, you can test the client-server connection in the http://localhost:4040/protected or http://localhost:4040/admin pages of your client application.

Pick a Single-Page Application (SPA) code sample in your preferred frontend framework and language:

angular
typescript
Angular Standalone Components Code Sample:Basic Authentication
This code sample uses Angular Standalone Components with TypeScript to implement single-page application authentication using the Auth0 Angular SDK.
angular
typescript
Angular Code Sample:Basic Authentication
This code sample uses Angular with TypeScript to implement single-page application authentication using the Auth0 Angular SDK.
react
javascript
React Router 6 Code Sample:Basic Authentication
Code sample showing how to protect a simple React single-page application using React Router 6, Auth0, and JavaScript.
react
typescript
React Router 6/TypeScript Code Sample:Basic Authentication
Code sample showing how to protect a simple React single-page application using React Router 6, Auth0, and TypeScript.
react
javascript
React Code Sample:Basic Authentication
JavaScript code that implements user login, logout and sign-up features to secure a React Single-Page Application (SPA) using Auth0.
react
typescript
React/TypeScript Code Sample:Basic Authentication
Code sample of a simple React single-page application built TypeScript that implements authentication using Auth0.
svelte
javascript
Svelte Code Sample:Basic Authentication
JavaScript code that implements user login, logout and sign-up features to secure a Svelte Single-Page Application (SPA), using routing middleware.
vue
javascript
Vue.js Composition API Code Sample:Basic Authentication
This code sample uses Vue.js 3 with JavaScript and the Composition API to implement single-page application authentication using the Auth0 Vue SDK.
vue
javascript
Vue.js Options API Code Sample:Basic Authentication
This code sample uses Vue.js 3 with JavaScript and the Options API to implement single-page application authentication using the Auth0 Vue SDK.
vue
javascript
Vue.js 2 Code Sample:Basic Authentication
This code sample uses Vue.js 2 with JavaScript to implement single-page application authentication using the Auth0 SPA SDK.

That's all it takes to integrate a client application with an Express.js API server that is also secured by Auth0 and to use an access token to consume protected server resources from the client application.

Next Steps

You have implemented token-based authorization in Express.js to restrict access to server resources.

This guide covered the most common authorization use case for an Express.js API: use middleware to enforce security policies, validate access tokens, and make authenticated requests to your API. However, Auth0 is an extensible and flexible identity platform that can help you achieve even more. If you have a more complex use case, check out the Auth0 Architecture Scenarios to learn more about the typical architecture scenarios we have identified when working with customers on implementing Auth0.

We'll cover advanced authorization strategies in future guides, such as implementing Role-Based Access Control (RBAC) and Fine-Grained Authorization (FGA).