DEV Community

Cover image for JWT Authentication using Prisma and Express
Mihai-Adrian Andrei
Mihai-Adrian Andrei

Posted on • Edited on • Originally published at mihai-andrei.com

JWT Authentication using Prisma and Express

All articles are first published on my site.

I wrote a blog post two years ago. It started like this:

After long research, I finally got an implementation of an authentication workflow that I like. Please note that I don't think this implementation is perfect, but, for my use cases, it will work perfectly. Also, this article does not handle any kind of validation or email verification. It explains only the JWT Token implementation.

Two years later, I have different opinions. Back then, I used a refresh token as jwt. Why? Because that's how I learned it. Now, I propose this one can be a random string. You can think of a refresh token as a session id. You check the session in the database. The Access Token ( JWT ) just acts as a "caching mechanism" to not check the db for every request.

While I advocate for using session authentication with http only cookies when possible, there are cases where this approach might be harder to use, such as in a microservices architecture. If you find yourself needing to implement JWT authentication, let me guide you through an improved method.

Part 1: Workflow

We are going to implement the following endpoints:

Endpoints

For /auth/login and /auth/register, the client needs to provide a user and a password in exchange for a pair of tokens ( access token and refresh token ).
With the access token, a request can be made to /users/profile. Here, the following workflow will be applied:

Access Token

Note: We only check if the token is valid. This way, we keep our workflow stateless. Because of that, the access token should expire fast ( 5/10 minutes ).

In order to keep the user logged in, the client needs to make a request to /auth/refreshToken containing the refreshToken received on register/login.
Based on that token, on the server we will make some checks and provide a new pair of tokens. The process is explained in the following diagram.

Refresh Token

Now, let's move to the coding part.

Part 2 Code

The code for this implementation can be found here.

Note My suggestion is to store both of the tokens in HTTP Only Cookies. If this is not possible because your frontend doesn't have a BFF (Backend For Frontend) you could store the access token in memory and the refresh token in localStorage. Just make sure your frontend logic is safe against XSS attacks.

Step 1: Create the app

When I write javascript code on the backend, I prefer to use a boilerplate made by Coding Garden.
In order to use CJ's boilerplate, we can run the following code from the terminal.

npx create-express-api -d auth-server
cd auth-server
npm install
npm run dev
Enter fullscreen mode Exit fullscreen mode

Now, you can test your server with a GET request on http://localhost:5000/api/v1/.

Step 2: Install dependencies and setup env variables

npm install -dev prisma
npm install @prisma/client bcrypt jsonwebtoken
npx prisma init --datasource-provider sqlite
Enter fullscreen mode Exit fullscreen mode

Add the following inside .env.

JWT_ACCESS_SECRET=SECRET123
Enter fullscreen mode Exit fullscreen mode

Step 3: Prisma setup

Inside prisma/schema.prisma, we will define our database model. Paste the following.

model User {
  id            String         @id @unique @default(uuid())
  email         String         @unique
  password      String
  refreshTokens RefreshToken[]
  createdAt     DateTime       @default(now())
  updatedAt     DateTime       @updatedAt
}

model RefreshToken {
  id          String   @id @unique @default(uuid())
  hashedToken String   @unique
  userId      String
  User        User     @relation(fields: [userId], references: [id], onDelete: Cascade)
  revoked     Boolean  @default(false)
  createdAt   DateTime @default(now())
  updatedAt   DateTime @updatedAt
  expireAt    DateTime
}
Enter fullscreen mode Exit fullscreen mode

And run npx prisma migrate dev in the console.

Now, we have everything ready for writing our authentication logic.

We are going to use 2 tables. The user table is self-explanatory. The refresh token table is going to be used as a Whitelist for the tokens that we generate as explained in part 1.

Step 4: Add utility functions.

Create a folder called utils inside src. Here, we will add the following files:

  • db.js - used for database interaction with prisma.
const { PrismaClient } = require('@prisma/client');

const db = new PrismaClient();

module.exports = { db };
Enter fullscreen mode Exit fullscreen mode
  • jwt.js - used for token generation.
const jwt = require('jsonwebtoken');
const crypto = require('crypto');

// Usually I keep the token between 5 minutes - 15 minutes
function generateAccessToken(user) {
  return jwt.sign({ userId: user.id }, process.env.JWT_ACCESS_SECRET, {
    expiresIn: '5m',
  });
}

// Generate a random string as refreshToken
function generateRefreshToken() {
  const token = crypto.randomBytes(16).toString('base64url');
  return token;
}

function generateTokens(user) {
  const accessToken = generateAccessToken(user);
  const refreshToken = generateRefreshToken();
  return { accessToken, refreshToken };
}

module.exports = {
  generateAccessToken,
  generateRefreshToken,
  generateTokens,
};

Enter fullscreen mode Exit fullscreen mode
  • hashToken.js - used to hash the token before saving it to the database.
const crypto = require('crypto');

function hashToken(token) {
  return crypto.createHash('sha512').update(token).digest('hex');
}

module.exports = { hashToken };

Enter fullscreen mode Exit fullscreen mode

Step 5: Project structure

Delete the emojis.js from src/api and cleanup api/index.js by removing emojis route.

Create 2 folders: auth and users inside src/api. In each folder, we will create 2 files for routes and services.

 ┣ 📂src
 ┃ ┣ 📂api
 ┃ ┃ ┣ 📂auth
 ┃ ┃ ┃ ┣ 📜auth.routes.js
 ┃ ┃ ┃ ┗ 📜auth.services.js
 ┃ ┃ ┣ 📂users
 ┃ ┃ ┃ ┣ 📜users.routes.js
 ┃ ┃ ┃ ┗ 📜users.services.js
 ┃ ┃ ┗ 📜index.js
 ┃ ┣ 📂utils
 ┃ ┃ ┣ 📜db.js
 ┃ ┃ ┣ 📜hashToken.js
 ┃ ┃ ┣ 📜jwt.js
 ┃ ┃ ┗ 📜sendRefreshToken.js
Enter fullscreen mode Exit fullscreen mode

Step 6: Services

Now, inside user.services.js paste the following code:

const bcrypt = require('bcrypt');
const { db } = require('../../utils/db');

function findUserByEmail(email) {
  return db.user.findUnique({
    where: {
      email,
    },
  });
}

function createUserByEmailAndPassword(user) {
  user.password = bcrypt.hashSync(user.password, 12);
  return db.user.create({
    data: user,
  });
}

function findUserById(id) {
  return db.user.findUnique({
    where: {
      id,
    },
  });
}

module.exports = {
  findUserByEmail,
  findUserById,
  createUserByEmailAndPassword,
};

Enter fullscreen mode Exit fullscreen mode

Most of the code is self-explanatory, but as a summary, we define some helpers specific to the User table that we are going to use in the project.

Now, the code for auth.services.js.

const { db } = require('../../utils/db');
const { hashToken } = require('../../utils/hash');

// used when we create a refresh token.
// a refresh token is valid for 30 days
// that means that if a user is inactive for more than 30 days, he will be required to log in again
function addRefreshTokenToWhitelist({ refreshToken, userId }) {
  return db.refreshToken.create({
    data: {
      hashedToken: hashToken(refreshToken),
      userId,
      expireAt: new Date(Date.now() + 1000 * 60 * 60 * 24 * 30), // 30 days
    },
  });
}

// used to check if the token sent by the client is in the database.
function findRefreshToken(token) {
  return db.refreshToken.findUnique({
    where: {
      hashedToken: hashToken(token),
    },
  });
}

// soft delete tokens after usage.
function deleteRefreshTokenById(id) {
  return db.refreshToken.update({
    where: {
      id,
    },
    data: {
      revoked: true,
    },
  });
}

function revokeTokens(userId) {
  return db.refreshToken.updateMany({
    where: {
      userId,
    },
    data: {
      revoked: true,
    },
  });
}

module.exports = {
  addRefreshTokenToWhitelist,
  findRefreshToken,
  deleteRefreshTokenById,
  revokeTokens,
};

Enter fullscreen mode Exit fullscreen mode

Now, we have everything in place to write our routes.

Step 7: Auth Routes.

Let's make the /register endpoint. Inside auth.routes.js put the following code:

const express = require('express');
const bcrypt = require('bcrypt');
const { generateTokens } = require('../../utils/jwt');
const {
  addRefreshTokenToWhitelist,
  findRefreshToken,
  deleteRefreshTokenById,
  revokeTokens,
} = require('./auth.services');

const router = express.Router();
const {
  findUserByEmail,
  createUserByEmailAndPassword,
  findUserById,
} = require('../user/user.services');

router.post('/register', async (req, res, next) => {
  try {
    const { email, password } = req.body;
    if (!email || !password) {
      res.status(400);
      throw new Error('You must provide an email and a password.');
    }

    const existingUser = await findUserByEmail(email);

    if (existingUser) {
      res.status(400);
      throw new Error('Email already in use.');
    }

    const user = await createUserByEmailAndPassword({ email, password });
    const { accessToken, refreshToken } = generateTokens(user);
    await addRefreshTokenToWhitelist({ refreshToken, userId: user.id });

    res.json({
      accessToken,
      refreshToken,
    });
  } catch (err) {
    next(err);
  }
});

module.exports = router;
Enter fullscreen mode Exit fullscreen mode

Here, we take the email/password from the user. We make some basic validation ( you will need to add some validation steps here ( for example, use yup or joi ). We create the user, the tokens, and we add the refresh token to the whitelist ( check diagram 1 for the flow).
In order for the route to be recognised by our app, we need to add some code inside src/api/index.js:

const auth = require('./auth/auth.routes');
router.use('/auth', auth);
Enter fullscreen mode Exit fullscreen mode

You can now test the endpoint by making a post request to http://localhost:5000/api/v1/auth/register. The response will be:

{
    "accessToken": "generatedAccessToken...",
    "refreshToken": "generatedRefreshToken..."
}
Enter fullscreen mode Exit fullscreen mode

Let's move to the login endpoint. This one is very similar to the register one.

router.post('/login', async (req, res, next) => {
  try {
    const { email, password } = req.body;
    if (!email || !password) {
      res.status(400);
      throw new Error('You must provide an email and a password.');
    }

    const existingUser = await findUserByEmail(email);

    if (!existingUser) {
      res.status(403);
      throw new Error('Invalid login credentials.');
    }

    const validPassword = await bcrypt.compare(password, existingUser.password);
    if (!validPassword) {
      res.status(403);
      throw new Error('Invalid login credentials.');
    }

    const { accessToken, refreshToken } = generateTokens(existingUser);
    await addRefreshTokenToWhitelist({ refreshToken, userId: existingUser.id });

    res.json({
      accessToken,
      refreshToken,
    });
  } catch (err) {
    next(err);
  }
});
Enter fullscreen mode Exit fullscreen mode

Now you can test the login endpoint by providing an existing user/password combination via a POST request to http://localhost:5000/api/v1/auth/login. If it is successful, you will get a response containing an access token and a refresh token.

Next, we will add the refresh_token endpoint and a test endpoint for revoking all tokens. Here is all the code for auth.routes.ts:

const express = require('express');
const bcrypt = require('bcrypt');
const { generateTokens } = require('../../utils/jwt');
const {
  addRefreshTokenToWhitelist,
  findRefreshToken,
  deleteRefreshTokenById,
  revokeTokens,
} = require('./auth.services');

const router = express.Router();
const {
  findUserByEmail,
  createUserByEmailAndPassword,
  findUserById,
} = require('../user/user.services');

router.post('/register', async (req, res, next) => {
  try {
    const { email, password } = req.body;
    if (!email || !password) {
      res.status(400);
      throw new Error('You must provide an email and a password.');
    }

    const existingUser = await findUserByEmail(email);

    if (existingUser) {
      res.status(400);
      throw new Error('Email already in use.');
    }

    const user = await createUserByEmailAndPassword({ email, password });
    const { accessToken, refreshToken } = generateTokens(user);
    await addRefreshTokenToWhitelist({ refreshToken, userId: user.id });

    res.json({
      accessToken,
      refreshToken,
    });
  } catch (err) {
    next(err);
  }
});

router.post('/login', async (req, res, next) => {
  try {
    const { email, password } = req.body;
    if (!email || !password) {
      res.status(400);
      throw new Error('You must provide an email and a password.');
    }

    const existingUser = await findUserByEmail(email);

    if (!existingUser) {
      res.status(403);
      throw new Error('Invalid login credentials.');
    }

    const validPassword = await bcrypt.compare(password, existingUser.password);
    if (!validPassword) {
      res.status(403);
      throw new Error('Invalid login credentials.');
    }

    const { accessToken, refreshToken } = generateTokens(existingUser);
    await addRefreshTokenToWhitelist({ refreshToken, userId: existingUser.id });

    res.json({
      accessToken,
      refreshToken,
    });
  } catch (err) {
    next(err);
  }
});

router.post('/refreshToken', async (req, res, next) => {
  try {
    const { refreshToken } = req.body;
    if (!refreshToken) {
      res.status(400);
      throw new Error('Missing refresh token.');
    }
    const savedRefreshToken = await findRefreshToken(refreshToken);

    if (
      !savedRefreshToken
      || savedRefreshToken.revoked === true
      || Date.now() >= savedRefreshToken.expireAt.getTime()
    ) {
      res.status(401);
      throw new Error('Unauthorized');
    }

    const user = await findUserById(savedRefreshToken.userId);
    if (!user) {
      res.status(401);
      throw new Error('Unauthorized');
    }

    await deleteRefreshTokenById(savedRefreshToken.id);
    const { accessToken, refreshToken: newRefreshToken } = generateTokens(user);
    await addRefreshTokenToWhitelist({ refreshToken: newRefreshToken, userId: user.id });

    res.json({
      accessToken,
      refreshToken: newRefreshToken,
    });
  } catch (err) {
    next(err);
  }
});

// This endpoint is only for demo purpose.
// Move this logic where you need to revoke the tokens( for ex, on password reset)
router.post('/revokeRefreshTokens', async (req, res, next) => {
  try {
    const { userId } = req.body;
    await revokeTokens(userId);
    res.json({ message: `Tokens revoked for user with id #${userId}` });
  } catch (err) {
    next(err);
  }
});

module.exports = router;
Enter fullscreen mode Exit fullscreen mode

The /revokeRefreshTokens should not be exposed in the api. You should call the revokeTokens method only in a specific case where you would like to invalidate all the tokens ( ex: password reset ).
As for the refresh_token endpoint, it is used to get another pair of tokens, in order to keep the user logged. We check if the sent refresh token is valid and if it is in our database and it is not revoked. If those conditions are met, we invalidate the previous refresh token and generate a new pair of tokens.

Step 8: Protected Routes.

First, in order to protect our routes, we need to define a middleware. Go to src/middlewares.js and add the following code:

function isAuthenticated(req, res, next) {
  const { authorization } = req.headers;

  if (!authorization) {
    res.status(401);
    throw new Error('🚫 Un-Authorized 🚫');
  }

  try {
    const token = authorization.split(' ')[1];
    const payload = jwt.verify(token, process.env.JWT_ACCESS_SECRET);
    req.payload = payload;
  } catch (err) {
    res.status(401);
    if (err.name === 'TokenExpiredError') {
      throw new Error(err.name);
    }
    throw new Error('🚫 Un-Authorized 🚫');
  }

  return next();
}

module.exports = {
    // ... other modules
    isAuthenticated
}
Enter fullscreen mode Exit fullscreen mode

We check if the client sends an Authorization header. The format should be: Bearer token. If the token is present, we verify it with our secret and add it to the request so it can be accessed in the routes.

NOTE: We only check if the token is valid. This way, we keep our workflow stateless.
Question: What happens if the user is deleted or the refresh tokens are invalidated?
Answer: The user will still have access until the access token expire. This will be for a maximum of 5 minutes ( that's why our access tokens expire fast). In my opinion, this will suit most of the apps ( Of course, if you develop a banking app, it might not work. But for most of the apps, it will be alright. For example, imagine you build an app that has some free and premium content. The user pays you for 30 days of premium content. After 30 days, you will decrease his access, but if he already had a token, he will still be able to access the content for 5 more minutes. Based on this logic, you can set your expire time for access token accordingly.

Now, let's write the protected route. Go to src/api/users/users.routes.js and add the following code:

const express = require('express');
const { isAuthenticated } = require('../../middlewares');
const { findUserById } = require('./user.services');

const router = express.Router();

router.get('/profile', isAuthenticated, async (req, res, next) => {
  try {
    const { userId } = req.payload;
    const user = await findUserById(userId);
    delete user.password;
    res.json(user);
  } catch (err) {
    next(err);
  }
});

module.exports = router;

Enter fullscreen mode Exit fullscreen mode

And inside src/api/index.js:

const express = require('express');
const auth = require('./auth/auth.routes');
const users = require('./user/user.routes');

const router = express.Router();

router.get('/', (req, res) => {
  res.json({
    message: 'API - 👋🌎🌍🌏',
  });
});

router.use('/auth', auth);
router.use('/users', users);

module.exports = router;

Enter fullscreen mode Exit fullscreen mode

Now, you can make a GET request to http://localhost:5000/api/v1/users/profile. You will need to add an Authorization header with the access token that you got from the /login endpoint.

And that's it.🎉🎉🎉

Top comments (38)

Collapse
 
yousihy profile image
yousihy

It might be a silly question, but why do we need a refresh token? Since the client can easily have access to the refresh token and use it to get an access token, why don't we just extend the access token to 8 hours or so and be done with it. I am new to this. So maybe I am missing something

Collapse
 
mihaiandrei97 profile image
Mihai-Adrian Andrei

Not a stupid question at all. You could use only an access token, But the problem is that in the middleware, you verify only if the access token is valid. You do not verify the user. So, in case a user might be deleted, if he keeps his access token, he will still get access to the app for a maximum of 8h ( in case you deleted him right after a new token was given ). If you keep the access token lifetime short (15 minutes), when a request to refresh token will be made, it will check again the user in the db, and based on that it will provide a new token or not. So, for ex, if you deleted the user, he will still have access for a maximum of 15 minutes. As a tldr, this depends on the purpose of your app. If you don’t mind a user getting access to your app for a longer time after you want to restrict it, you can just provide an access token. If there are still other questions, just ask and I will try to clarify😬

Collapse
 
yousihy profile image
yousihy

Aha yes, it makes sense now. :)
Ok, one more question for my understanding. Now, the access token has been sent by the client (a React app for example) and it was found to be expired. Who will initiate the request for a new token using the refresh token? will the user on the React app be requested to login again? This part confuses me. If the user will not be asked to login again (which I assume this is the case), how to make the experience smooth for the user so he will not know what's happening in the backend and keeps using the app while the refresh token is still alive?

Thread Thread
 
mihaiandrei97 profile image
Mihai-Adrian Andrei • Edited

You are right. He will not be asked to login again. There are multiple aproaches, but the one I use is usually with axios interceptors. The general flow is the following: You just make normal requests, and if the backend sends you an error that the access token is expired, you intercept that error and make a call to refreshToken in order to receive a new access + refresh token. If that call responds with 200, then you just continue making calls with the new token. Otherwise, if the refresh token request failed, it probably means also the refresh token is invalid, so you redirect the user to /login again.

I will try to make a blog post on the frontend part for this blog, but probably next week. Do you prefer any framework?

Thread Thread
 
yousihy profile image
yousihy

You are amazing man :)
Well, my app is using Node, Express and Prisma for backend .. and React for the frontend. Axios is something I use also for sending API requests from my React app. Honestly, I have never used or knew about "Interceptors". Would be great if you have a blog post which adds to this one so we can learn the follow end-to-end 😬

Thread Thread
 
mihaiandrei97 profile image
Mihai-Adrian Andrei

Done. You can check it now :D

Thread Thread
 
sipkoepp profile image
sipkoepp

I was wondering what would be the best way to verify the user in the middleware. If a user is deleted but still has access to the app (even for max. 15 mins) each request might produce some chaos (Except you verify the userId on each request). How would you solve this without producing a too big impact on the Peformance?

Is it enough to check if an user exists based on the userId provided by the signed jwt? This should be safe, right? (Because you can only generate a valid signed jwt using the secret that the backend holds) Maybe the middleware could cache users like this:

import NodeCache from "node-cache";

const cache = new NodeCache({
  stdTTL: 60, // set TTL to 60 seconds
  checkperiod: 120, // set check period to 120 seconds
  maxKeys: 100, // set maximum number of keys to 100
});

const isAuthenticated = async (.....) = {
    ....

    const payload = jwt.verify(token, jwtAccessSecret) as {
      jti: string;
      userId: string;
    };
    console.log(payload);

    // check if user exists in cache
    const cachedUser = cache.get(payload.userId);
    if (cachedUser) {
      req.userId = payload.userId;
      return next();
    }

    // if user not in cache, query the database
    const user = await User.findById(payload.userId);
    if (!user) {
      throw new HttpException(401, "🚫 Un-Authorized 🚫");
    }

    // cache the user object for future requests
    cache.set(payload.userId, user);

    req.userId = payload.userId;
} 
Enter fullscreen mode Exit fullscreen mode

Would love to get your Feedback about this!

Thread Thread
 
mihaiandrei97 profile image
Mihai-Adrian Andrei

I think the JWT auth should be stateless ( the access token part ), so I would make the checks that you said on the refresh token endpoint. Regarding the “delete” problem, i would make the lifetime for the access token probably 1 minute. And it will achive what you are trying to do basically. But instead of checking the user every request (either in cache or db ), we check it only on refresh token ( every minute, if we set the expiry for access token to 1min )

Thread Thread
 
mihaiandrei97 profile image
Mihai-Adrian Andrei • Edited

There is a tradeoff (how many times to query the db agains having it valid more time and maybe stale), and the expire time should be based on your app logic. Regarding the “chaos” part, probably if the actions require him to write data, in the db you will have a foreign key to that user_id on the table he is writing to. And Because the user will no longer exist, there will be a db error.

Collapse
 
pedrobadm7 profile image
Pedro Barros

Just perfect, I loved this implementation!! Applied it to my project

Collapse
 
mihaiandrei97 profile image
Mihai-Adrian Andrei

Awesome! What are you building? 💙

Collapse
 
pedrobadm7 profile image
Pedro Barros

A simple CRUD just to study

Collapse
 
ahmedriad1 profile image
Ahmed Riad

Great article, well done 👏👏

Collapse
 
mengtongun profile image
SaTy

Great articles, thank you for sharing!

Collapse
 
ihsansonz profile image
IhsanSonz

Absolutely lovin' it

Collapse
 
mihaiandrei97 profile image
Mihai-Adrian Andrei

Glad you liked it😬

Collapse
 
reanzi profile image
Raymond Michael Nziku

Thanks for this, Totally inspired .
But I'm having a problem with the typescript version (from your repo), the server is not running I'm getting "Property 'user' does not exist on type 'Request>"
from the middleware.ts file on the requireUser function, looks like user is not present on the request.
I've tried to created .d.ts
`import { z } from "zod";

export { };

declare global {
namespace Express {
interface Request {
user: z.AnyZodObject; // 👈️ turn off type checking
}
}
}`

but still, no luck, app crashed

How would do to get passed this, as I real like your approach. Thanks

Collapse
 
mihaiandrei97 profile image
Mihai-Adrian Andrei
Collapse
 
reanzi profile image
Raymond Michael Nziku

Thanks for the reply (a quick one) :),
But I do still have trouble,

line 67 & 77 in middlewares.ts
req.user = payload
{ const user = req.user }

the error is "

TSError: ⨯ Unable to compile TypeScript:
src/middlewares.ts:67:9 - error TS2339: Property 'user' does not exist on type 'Request>'.

67 req.user = payload
~~~~
src/middlewares.ts:77:22 - error TS2339: Property 'user' does not exist on type 'Request>'.

Thread Thread
 
mihaiandrei97 profile image
Mihai-Adrian Andrei

github.com/mihaiandrei97/authentic...

You also need to have this in tsconfig:

"typeRoots": ["./typings"],

Thread Thread
 
reanzi profile image
Raymond Michael Nziku

My tscofig.json
"baseUrl": "src",
"paths": {
"@/config/*": ["config/*"],
"@/utils/*": ["utils/*"],
"@/assets/*": ["assets/*"]
},
// "typeRoots": ["./typings", "./node_modules/@types"],
"typeRoots": ["./typings"],
"types": ["express", "@types/jest"],
"resolveJsonModule": true,

I even changed the file structure to honor the tsconfig, restarted the TS server (on VSCode) but n luck,. Looks like req.user is of type Any not of ParsedToken! Every where the req.user is of type Any, that's why I can't even assign req.user to payload.

I don't know what I'm missing or doing wrong

Collapse
 
talinthedev profile image
Talin Sharma

I'm not using the exact same process but I took heavy inspiration from this article and my method is the same. I was just wondering, from a security standpoint, is there any problem with the server auto-generating a new access and refresh token when needed and only sending a 401 when there is a problem generating tokens or the refresh token has expired? IMO this would be better since the client would only need one request instead of two (the second request is to /refreshToken) but I don't know much about auth so would love some feedback for this!

Collapse
 
mihaiandrei97 profile image
Mihai-Adrian Andrei

I'm not really sure about auto-generating them, however, if you want to not make 2 requests, what I implemented was on frontend, you can also decode the jwt token, and check the expiration every X seconds, and if it will expire in the next 30s, you can call /refreshToken. This way, you automatically refresh the tokens without the backend explicitly telling you it is expired.

Collapse
 
vannakvy profile image
vannakvy

Do you have a plan to move this into Typescript?

Collapse
 
mihaiandrei97 profile image
Mihai-Adrian Andrei

You have an example on how to do it in TypeScript here:

github.com/mihaiandrei97/authentic...

Collapse
 
vannakvy profile image
vannakvy

thanks

Thread Thread
 
vannakvy profile image
vannakvy

how do we handle logout logic

Thread Thread
 
mihaiandrei97 profile image
Mihai-Adrian Andrei

You just delete the tokens from the frontend (ex: from localstorage )

Thread Thread
 
vannakvy profile image
vannakvy

thanks very much. I have one more question. Whenever I throw new Error() , my server crash. How do we prevent it?

Thread Thread
 
mihaiandrei97 profile image
Mihai-Adrian Andrei

github.com/mihaiandrei97/express-p...

You need to have the errorHandler defined and imported in the main file as middleware

Collapse
 
ricardorien profile image
Ricardo Aguilar

Can you explain the delete user.password; in '/profile'?
Also, it would be nice look this code in TypeScript.

Collapse
 
mihaiandrei97 profile image
Mihai-Adrian Andrei

Hello! That is in order to not return the password in the response. Here you can find the typescript equivalent: github.com/mihaiandrei97/authentic...

Collapse
 
ricardorien profile image
Ricardo Aguilar

Oh thanks a lot! I'm gonna check your repo for future references. Keep going!

Collapse
 
danilabesk profile image
what?

what about xss attacks? if the refresh token is stored on the client. Wouldn't it be better to store it on the server, protecting the user? For example, store it for 7 days and if the client does not have a refresh token, then if the response is Unauthorized, it will be possible to request a new pair of access and refresh tokens. Next, when the client receives a response in the form of a new access token, repeat the request that was originally made. It turns out 3 requests, but it’s safer, refresh is stored on the server. What do you think?