Node.js API Authorization By Example: TypeScript Edition
Updated on January 30, 2023This 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:
Once you clone the repo, make api_express_typescript_hello-world
your current directory:
cd api_express_typescript_hello-world
Install the Express.js project dependencies as follows:
npm install
The starter Express.js project defines the following API endpoints to let client applications access a simple message
resource:
# get a public messageGET /api/messages/public# get a protected messageGET /api/messages/protected# get an admin messageGET /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:
touch .env
Populate your .env
file with the following content:
PORT=6060CLIENT_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:
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:
- Auth0 Universal Login for Web, iOS & Android.
- Up to 2 social identity providers like Google, GitHub, and Twitter.
- Up to 3 Actions, Rules, & Hooks to customize and extend Auth0's capabilities.
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:
Auth0 Express.js Code Sample
https://hello-world.example.com
Click the "Create" button to complete the process. Your Auth0 API registration page loads up.
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:
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.
-
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.
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:
PORT=6060CLIENT_ORIGIN_URL=http://localhost:4040AUTH0_AUDIENCE=AUTH0-AUDIENCEAUTH0_DOMAIN=AUTH0-DOMAIN
Restart your development server for your Express.js project to become aware of these .env
file changes:
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:
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 endpointsrouter.get(...);// Protected API endpointsrouter.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:
touch src/middleware/auth0.middleware.ts
Populate src/middleware/auth0.middleware.ts
as follows:
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:
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:
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.
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:
curl localhost:6060/api/messages/public
Response:
Status code: 200
{"text": "This is a public message."}
Request:
curl localhost:6060/api/messages/protected
Response:
Status code: 401
{"message": "Requires authentication"}
Request:
curl localhost:6060/api/messages/admin
Response:
Status code: 401
{"message": "Requires authentication"}
Request:
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:
Request:
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:
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:
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:
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:
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:
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).