Simple jwt token authentication in node js

Alex Kompaniets
7 min readJan 28, 2023

--

Intro

Application authentication is the process of verifying the identity of a user who is trying to access a protected resource. It is used to ensure that only authorized users can access sensitive information and perform certain actions on the website.

Authentication is typically achieved through the use of an email and password (or with a phone number and sms code), which the user enters on a login page. The application then checks the provided credentials against a database of valid users to determine if the user is authorized to access the site.

Authentication is needed for a variety of reasons, including:

  1. Security: Authentication is essential for ensuring the security of sensitive data and protecting against unauthorized access.
  2. Access Control: With authentication, the application can restrict access to certain resources based on the user’s role or level of privilege.
  3. Compliance: Many industries have regulations that require authentication to be in place to protect sensitive data and user’s privacy.
  4. User Experience: Authentication enables the application to provide a personalized experience to the user, such as showing their account information and history.
  5. Business Logic: Authentication is often required for the application’s business logic, for example, to allow a user to purchase items online or access their bank account details.
  6. Auditing: Authentication provides a way to track and log user actions, which is useful for identifying suspicious activity and troubleshooting issues.

JWT token-based authentication provides several benefits over traditional session-based authentication:

  1. Stateless: JWT tokens are self-contained and do not rely on the server to keep track of the client’s state. This makes it easy to scale and distribute the application across multiple servers.
  2. Easy to implement: JWT tokens are simple to implement and can be easily integrated into existing systems.
  3. Increased security: JWT tokens are signed and can be encrypted, which makes them more secure than traditional session-based authentication. Additionally, because the client holds the token, it is less susceptible to server-side attacks such as session hijacking.
  4. Cross-Domain Support: JWT tokens can easily be passed through different domains, which makes it easy to authenticate and authorize users across multiple applications and services.
  5. Mobile friendly: JWT tokens can be stored on the device and don’t need to be stored in a browser cookie, which is more secure, and also support for mobile devices.
  6. Smaller network overhead: JWT tokens are smaller in size than traditional session-based authentication, which reduces network overhead and improves performance.
  7. Token expiration and revoking: JWT tokens can have expiration dates, making it easy to automatically expire tokens and revoke access.

Aim

We will build a server using typescript and koa with 2 routes:

  1. /authenticate — to check user identity (by checking email and password) and to return an access token used to access secure resources
  2. /banking-account — an example of a secured resource that does an access-token validation and returns a sensitive information

We’ll use a curl to imitate client activity and verify the routes. Client app development is out of the scope.

Implementation

Let’s start by adding a /authenticate route first:

// routes.ts
import Router from '@koa/router';
import { AuthenticateInput, User } from './types';
import { generateAccessToken } from './token-service';

export const router = new Router({
prefix: '/api',
});

const users: User[] = [{
id: 'test-user-id',
email: 'test-user@gmail.com',
password: 'test-password',
}];

router.post('/authenticate', async(ctx) => {
const body: AuthenticateInput = ctx.request.body;
const user = users.find((user) => user.email === body.email && user.password === body.password);
if (!user) {
ctx.throw(401, 'Incorrect password or email');
return;
}

ctx.set('Content-Type', 'application/json');
const accessToken = await generateAccessToken(user);
ctx.body = JSON.stringify({ accessToken });
});

For simplicity, we’re using an array of users as storage but normally we would like to store it in the DB.

⚠️ Also we store a password as plain text. You should never do it in the production apps and use proper password encryption. You can read more about password encryption.

So in this handler, we expect the input with email and password, and if they are valid return an access token.

Generally, there are no limits on token types but JWT is a de-facto standard for access tokens due to the number of benefits — they are easy to generate, validate, expire and provide additional data within the token.

Generating token:

// token-service.ts

import * as jwt from 'jsonwebtoken';
import { User } from './types';

const accessTokenSalt = 'very-secret-salt';
const tokenExpiresIn = '1h';

async function generateToken(
payload: Object,
salt: string,
options: jwt.SignOptions
): Promise<string> {
return new Promise((res, rej) => {
jwt.sign(payload, salt, options, (err, token) => {
if (err) {
return rej(err);
}
res(token!);
});
});
}

export function generateAccessToken(user: User): Promise<string> {
return generateToken({ id: user.id, email: user.email }, accessTokenSalt, {
expiresIn: tokenExpiresIn,
});
}

We’ll use the powerful jsonwebtoken node.js library for generating and verifying tokens.

⚠️️ For simplicity we’re using a weak salt and hardcoded it directly in the code. Normally you would like it to be environment-specific (dev, staging, production) and extracted to the config file. Make sure to keep it secure!

Now let’s add a secure route /banking-account and make sure it could be accessed only using a previously obtained access token:

// routes.ts
import { accessMiddleware } from './access-middleware';

...

const bankingAccounts = [{
userId: 'test-user-id',
id: 'test-banking-account-id',
value: 100,
currency: 'USD',
}];

router.get('/banking-account', accessMiddleware, async(ctx) => {
const user: User = ctx.state.user;
const bankingAccount = bankingAccounts.find((bankingAccount) => bankingAccount.userId === user.id);
if (!bankingAccount) {
ctx.throw(404, 'Banking account not found');
return;
}

ctx.set('Content-Type', 'application/json');
ctx.body = JSON.stringify(bankingAccount);
});

So here we get a current user from the state (we’ll discuss how it got there later). The state is a request session in koa. Looking for the current user’s bank account and returning it as a JSON response. Pretty simple, huh? But what about the token validation?

All token validation magic is hidden in the accessMiddleware . Extracting request-flow code into middleware is a popular approach with the web frameworks, specifically in koa and express.

accessMiddleware:

// access-middleware.ts

import { verifyAccessToken } from './token-service';

export async function accessMiddleware(ctx, next) {
const { authorization } = ctx.request.headers;
if (!authorization) {
ctx.throw(401, 'No authorization provided');
}
const token = authorization.split(' ')[1];
if (!token) {
ctx.throw(401, 'No token provided');
}
try {
const decoded = await verifyAccessToken(token);
ctx.state.user = decoded;
return next();
} catch (err) {
ctx.throw(401, 'Invalid token');
}
}

So basically we extract access token from the Authorization header and pass it to the verifyAccessToken function. We expect it to return decoded token information about user that could be set into a request state for later use in handlers. If there is no token or token validation failed we return 401 http status with the error message.

As mentioned above, token could be sent in Authorization header in the following format:

Authorization: Bearer <access_token>

accessMiddleware could be used to check access to all restricted routes.

verifyAccessToken:

// token-service.ts

...

async function verifyToken(token: string, salt: string): Promise<jwt.JwtPayload> {
return new Promise((res, rej) => {
jwt.verify(token, salt, (err, decoded: jwt.JwtPayload) => {
if (err) {
return rej(err);
}
res(decoded);
});
});
}

export function verifyAccessToken(token: string): Promise<jwt.JwtPayload> {
return verifyToken(token, accessTokenSalt);
}

Verify access token is a wrapper around jsonwebtoken verify function. Idea is to verify that the incoming token was signed with our salt and not with any custom salt, confirming its validity. Also, verify checks that token is not expired. If verification succeeded we get decoded info about the user we previously encoded by calling generateAccessToken— user id and user email. We use it to set the request state in the accessMiddleware:

...
const decoded = await verifyAccessToken(token);
ctx.state.user = decoded;
...

Start and check

Now we’ll wrap altogether and start the server:

// server.ts

import Koa from 'koa';
import { koaBody } from 'koa-body';
import { router } from './routes';

const app = new Koa();

app.use(koaBody());
app.use(router.routes());

app.listen(3000, () => {
console.log('Server is running on port 3000');
});

ts-node server.ts

Let’s verify that authentication works properly by running couple curl commands:

Authentication request providing incorrect password:

curl -i -X POST -d '{"email":"test-user@gmail.com","password":"incorrect-password"}' --header 'Content-Type: application/json' http://localhost:3000/api/authenticate
HTTP/1.1 401 Unauthorized
Content-Type: text/plain; charset=utf-8
Content-Length: 27
Date: Sat, 28 Jan 2023 19:20:18 GMT
Connection: keep-alive
Keep-Alive: timeout=5

Incorrect password or email

Authentication request with a correct password:

curl -i -X POST -d '{"email":"test-user@gmail.com","password":"test-password"}' --header 'Content-Type: application/json' http://localhost:3000/api/authenticate
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 213
Date: Sat, 28 Jan 2023 19:21:07 GMT
Connection: keep-alive
Keep-Alive: timeout=5

{"accessToken":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InRlc3QtdXNlci1pZCIsImVtYWlsIjoidGVzdC11c2VyQGdtYWlsLmNvbSIsImlhdCI6MTY3NDkzMzY2NywiZXhwIjoxNjc0OTM3MjY3fQ.cNH962sIE8jIqV_jJOTBSZknSHvh-U1h8YiqORDWxhY"}

Secured route request with incorrect token:

curl -i --header 'authorization: Bearer incorect-token' http://localhost:3000/api/banking-account
HTTP/1.1 401 Unauthorized
Content-Type: text/plain; charset=utf-8
Content-Length: 13
Date: Sat, 28 Jan 2023 19:22:59 GMT
Connection: keep-alive
Keep-Alive: timeout=5

Invalid token

Secured route request using the correct token:

curl -i --header 'authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InRlc3QtdXNlci1pZCIsImVtYWlsIjoidGVzdC11c2VyQGdtYWlsLmNvbSIsImlhdCI6MTY3NDkzMzY2NywiZXhwIjoxNjc0OTM3MjY3fQ.cNH962sIE8jIqV_jJOTBSZknSHvh-U1h8YiqORDWxhY' http://localhost:3000/api/banking-account
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 85
Date: Sat, 28 Jan 2023 19:24:32 GMT
Connection: keep-alive
Keep-Alive: timeout=5

{"userId":"test-user-id","id":"test-banking-account-id","value":100,"currency":"USD"}

Conclusion

Here we added a simple backend authentication process. Purpose of this example is just to share a concept of jwt-based access token authentication.
Before implementing it in the real application make sure to address the following questions:

  • users persistent storage (database)
  • password encryption
  • safe access token salt storage
  • environment-specific access token salt
  • refresh tokens (if needed)

Repository

https://github.com/Oxyaction/react-native-auth-sample

In the following parts, we will extend authentication with 3rd party identity providers (Google) and will use it with React Native app as a client.

Ukraine is a victim of a brutal and unprovoked invasion launched by russia. Terroristic forces committed numerous war crimes — civilian executions, attacking civil infrastructure (power plants) and residential buildings, marauding, and torturing.

If you liked an article and would like to thank — a donation to Ukraine will be the best option. Here you can find several verified funds, let’s stop evil together.
https://u24.gov.ua/
https://war.ukraine.ua/

--

--