How to Set Up an API with JWT, req.User and Route Security

Tomas Nilsson
JavaScript in Plain English
6 min readSep 27, 2021

--

Photo by Anthony Riera on Unsplash

After reading this article, you’ll understand how to create an API in Node.js/TypeScript with functions such as:

  • On calling /login, generate and return a Bearer/JWT to the user
  • Use middleware to decode the Authentication header (Bearer/JWT)
  • Modify the Express namespace to add support for req.User
  • Add security on routes (ie, “Only Admin may access this Route”)

PS: There is a link to a working git at the very end.

Basic Express setup

There are very few exciting things about something that has been documented a thousand times, but we need to get through this part:

import Express from 'express';
import http from 'http';
import cors from 'cors';
const PORT = process.env.PORT !== undefined ? parseInt(process.env.PORT) : 3001;let app: Express.Application | undefined = undefined;app = Express();app.use(cors({ exposedHeaders: 'Authorization' }));
app.use(Express.urlencoded({ extended: true }));
app.use(Express.json());
// -----> MORE CODE WILL BE ADDED HERE! <--------http.createServer(app).listen(PORT, () => console.log(`Webserver running at http://localhost:${PORT}/`));

Add all routes at the line shown above in the order they’re presented. Add the rest of the code somewhere else. A few items should be in their own file(s).

Decoding the JWT

Really early in the routes, the function for decoding the header should be found. If there are truly anonymous routes present (like /robots.txt , then this function should probably come after that).

app.use(function JWTDecoderMiddleware(req: Express.Request, res: Express.Response, next: Express.NextFunction) {  const authorization = req.headers.authorization;
if (authorization && authorization.startsWith("Bearer ")) {
const u = DecodeJWT(authorization.substring(7));
if (u)
req.User = u;
else
console.log("jwt found, but it was not valid.");
}
next();
});

This function is dependant on two things; the global namespace being modified making req.User available and the DecodeJWT -function (see below)

The `User`, `Bearer` and `AccessLevel` types

Before continuing, there are a couple of types needed. At the API side, we will have some kind of user logged in, and the type/interface for that needs to be handled.

Next, the Bearer. It will be sent to and stored by the client (in the encrypted JWT format). Basically, anything non-sensitive can be stored in the JWT, just be aware that the user can easily decrypt the JWT (but they cannot modify it). Examine JWT at https://jwt.io/.

types.ts

export type User = {
ID: string,
Name: string,
AccessLevel: AccessLevel,
/** Never ever store an clear-text pw like this! */
Password: string
}

export type Bearer = {
Name: string,
ID: string,
AccessLevel: AccessLevel
}
export type AccessLevel = "Admin" | "User" | "Anonymous";

Store the user's credentials in a safe way, don’t ever use clear text passwords as in this demo.

global.d.ts

Note the global.d.ts naming. Read more about global variables in https://javascript.plainenglish.io/typescript-and-global-variables-in-node-js-59c4bf40cb31

At the same time as Express.User is added, we add the types for the environment variables that we intend to use:

import { User } from './types';declare global {    namespace Express {
interface Request {
User: User
}
}
namespace NodeJS {
interface ProcessEnv {
JWTENCRYPTIONKEY: string;
NODE_ENV: 'development' | 'production';
PORT: string; // All environment variables are strings
}
}
}
export { };

❕ Make sure you either use .env or set the environment variables.

The DecodeJWT function

In order to verify the JWT hasn’t been tampered with, the API must define a secret key. Basically decode it as:

import * as jsonwebtoken from "jsonwebtoken";export function Decode<T extends object>(iJWT: string): T | undefined {
try {
return jsonwebtoken.verify(iJWT,
process.env.JWTENCRYPTIONKEY) as T;
} catch (e) {
return undefined;
}
}
export function DecodeJWT(jwt: string): User | undefined { const decoded = Decode<Bearer>(jwt);
if (decoded)
return Users.find(u => u.ID === decoded.ID);
}

Users?

We need some users.

const Users: User[] = [
{ ID: "root", Name: "root", AccessLevel: "Admin", Password: "Banana1" },
{ ID: "foo", Name: "Foo", AccessLevel: "User", Password: "Bar" }
];

Store the user's credentials in a safe way, don’t ever use clear text passwords as in this demo.

Route /

Let’s add some routes, beginning with / .

app.get('/', function (req: Express.Request, res: Express.Response){
res.status(200).json({
"Foo": "Bar",
"Time": new Date().toISOString()
});
});

Route /login

This route will take the { "username": "Foo", "password" : "Bar" } and try to find a user matching the credentials. If found, a JWT is generated and sent to the callee.

app.post('/login', function (req: Express.Request, res: Express.Response) {    console.log(`U=${req.body.username}, Pwd=<hidden>`);    const jwt = Login(req.body);
if (jwt) {
res.append('Authorization', jwt);
res.status(200).json({ Success: true, JWT: jwt });
}
else
res.status(200).json({ Success: false });
});

Login

export function Login(arg: { username: string, password: string }): string | undefined {    const user = Users.find(u => u.Name === arg.username 
&& u.Password === arg.password);
if (user) {
const result: Bearer = {
ID: user.ID,
Name: user.Name,
AccessLevel: user.AccessLevel
};
return Encode(result); }
}

Encode function

export function Encode<T extends object>(iPayLoad: T, iTimeoutSeconds = 3600 * 10): string {
return jsonwebtoken.sign(
iPayLoad,
process.env.JWTENCRYPTIONKEY,
{ expiresIn: iTimeoutSeconds }
);
}

So far, so good.

At this point, we have achieved a running web server that exposes / , /login and that can decode any valid JWT and through that can return a User.

Now it gets a little bit more complicated. :)

HasAccessLevel — implemented as a function

Checking accesslevel can be done with a function or as Middleware.

/me — with a function to check accesslevel

app.get('/me', function (req: Express.Request, res: Express.Response) {   if (!HasAccessLevel(req, res, ["User", "Admin"])) 
return; // Response has already been sent
res.status(200).json({ "UserName": req.User.Name });});

HasAccessLevel function

AccessLevel is a type, defined as type AccessLevel = "Admin" | "User" | "Anonymous .

function HasAccessLevel(req: Express.Request, res: Express.Response, iReqLevel: AccessLevel[] = []): boolean {    let result = false;
if (req.User) // Has the middleware decoded the jwt properly?
// Yes, does the user match?
result = iReqLevel.includes(req.User.AccessLevel);
else
// No, but maybe "Anonymous" can access this function?
result = iReqLevel.includes("Anonymous");

// Not allowed to access? Send 401!
if (!result) {
res.status(401).json(
{ Success: false, Error: "Access denied" });
res.end();
}
return result;
}

HasAccessLevelMiddleware — implemented as Middleware

/me — using middleware

Middlewares has to have three parameters, but that can easily be avoided with the following syntax:

app.get('/me2',
(rq, rs, n) => HasAccessLevelMiddleware(rq, rs, n, ["User", "Admin"]),
(req: Express.Request, res: Express.Response) => {
// Will only get here if HasAccessLevelMiddleware allows it
res.status(200).json({ "UserName": req.User.Name });
});

HasAccessLevelMiddleware

The middleware decides if next() is called, or not.

export function HasAccessLevelMiddleware(req: Express.Request, res: Express.Response, next: Express.NextFunction, iReqLevel: AccessLevel[] = []): void {    let hasAccess = false;
if (req.User) // Has the middleware decoded the jwt properly?
hasAccess = iReqLevel.includes(req.User.AccessLevel);
else
hasAccess = iReqLevel.includes("Anonymous");
if (hasAccess)
next(); // Continue executing

else {
res.status(401).json({ Success: false, Error: "Denied" });
res.end();
}
}

Error handler

At last, add the error handler. The error handler takes four parameters, where the first one is err . To get TypeScript to shine, it’s a good thing to define err as Error & { status: number, message: string } . Error is the built-in Error object and Express adds the status and message .

app.use(function (err: Error & { status: number, message: string }, req: Express.Request, res: Express.Response, next: Express.NextFunction) {console.error(err.status);
console.error(err.message);
console.error(err.stack);
res.status(500).json({ Error: "Internal error" });
res.end();
});

curl

These commands were written on Windows, make necessary adjustments to run on other platforms:

# Login as user "Foo"curl -X POST -H "Content-Type: application/json" -d "{\"username\":\"Foo\",\"password\":\"Bar\"}" http://localhost:3001/login# Grab token from body or headers and do:set TOKEN=eyJhbGciOiJIUzI1NiIsIn.....# Use the tokencurl -H "Authorization: Bearer %TOKEN%" http://localhost:3001/me

Git repo

The repo is prepared to just run by pressing F5 in VSCode (or nodemon , or by running node ). Read more on https://github.com/tomnil/expressplate

Enjoy :)

More content at plainenglish.io

--

--