Blog JWT Validation Practice

Keywords: Javascript axios JSON Session Database

In the past, the authentication method of blog was session. Recently, I took some time to change it to jwt, and I learned about JWT by the way. The introduction of JWT can be seen Ruan Yifeng's Articles.

jwt implementation process

The figure above is the simplest jwt process. When token expires or fails, it will jump back to the login page. But my ideal state is not to log in for a week, and the expiration time will be delayed for a week after each login. Without modifying the database, the following two methods are proposed:

  1. Set the expiration time of the token to 7 days. Each time when the interface is accessed, the backend regenerates the token according to the current time, and then returns the newly generated token in each interface. After the front end receives the token, it stores it in a cookie and brings the new token in the next request.
  2. After login, a token with an expiration time of 3 hours and a refreshToken with an expiration time of 7 days are generated. When the front-end requests, it carries token information in the header. If the token expires, the front-end carries refreshToken to access the refresh token interface. If the refreshToken does not expire, the back-end returns the new token and refreshToken, otherwise the front-end jumps to the login page. The front end then re-accesses the new interface based on the newly returned token.

The obvious disadvantage of scheme 1 is that each call to the interface generates a token, which increases unnecessary overhead. And each of the back-end interfaces carries token information, and the back-end changes a lot=. = Scheme 2 seems to have no obvious shortcomings, so we choose Scheme 2 to practice.

The above figure shows the refresh token authentication process. You can see that there is one more step to refresh token and refresh token than the simplest process, which is also the key to refresh token.

Implementation code

Backend node

As agreed in the project, 401 indicates token timeout and 402 indicates refreshToken invalid
Initialize token and refreshToken at login time

// jwt.js
const jwt = require('jsonwebtoken')
const { SECRET_KEY, JWT_EXPIRES, REFRESH_JWT_EXPIRES } = require('../config/config') // Introduce SECRET_KEY, token expiration time, refreshToken expiration time from configuration file
const { cacheUser } = require('../cache/user') // Caching User Names with Closures

/**
 * Generating token and initToken
 * @param {string} username 
 */
function initToken (username) {
  return {
    token: jwt.sign({
      username: username
    }, SECRET_KEY, {
      expiresIn: JWT_EXPIRES
    }),
    refresh_token: jwt.sign({
      username
    }, SECRET_KEY, {
      expiresIn: REFRESH_JWT_EXPIRES
    })
  }
}

/**
 * Verify token/refreshToken
 * @param {string} token The format is `Beare ${token}`
 */
function validateToken (token, type) {
  try {
    token = token.replace(/^Beare /, '')
    const { username } = jwt.verify(token, SECRET_KEY)
    if (type !== 'refreshToken') {
      // If it is token and token takes effect, cache token
      cacheUser.setUserName(username)
    }
  } catch (e) {
    throw new Error(e.message)
  }
}

module.exports = {
  initToken,
  getTokenUser,
  validateToken
}

Then call the validateToken method in the middleware

// Check whether to log in to the middleware method
checkLogin (req, res, next) {
    const { headers: { authorization } } = req
    if (!authorization) {
      res.status(402).json({ code: 'ERROR', data: 'No login information detected' })
      return false
    }
    try {
      validateToken(authorization)
    } catch (e) {
      if (e.message === 'jwt expired') {
        // token timeout
        res.status(401).json({ code: 'ERROR', data: 'login timeout' })
        return false
      } else {
        res.status(402).json({ code: 'ERROR', data: 'No login information detected' })
        return false
      }
    }

In some interfaces that require login authentication (such as getting user message interface), middleware is called to verify login first. The token is validated once in the middleware. Here I write a user cache with closures. After the middleware validation is successful, the user is stored in the closure. Then the getUserName method of the closure can be invoked to get the user information in the interface without parsing the token. This can avoid the trouble of the middleware validation token is successful but the validation token expires in the interface.

/**
 * Write a closure to cache user names
 */
const cacheUser = (() => {
  let username = ''
  return {
    setUserName: (user) => {
      if (username === user) {
        return false
      }
      username = user
    },
    getUserName: () => {
      return  username
    },
    clearUserName: () => {
      username = ''
    }
  }
})()

module.exports = {
  cacheUser
}

After successful login, get token and refreshToken and return to the front end.

Client js

js needs to re-encapsulate axios functions

import axios from 'axios'
import qs from 'qs'
......
// Refresh token and refreshToken according to refreshToken
const fetchRefreshToken = () => {
  const token = Cookies.get('refreshToken')
  return axios({
    url: '/api/signin/refreshToken',
    headers: { Authorization: `Beare ${token}` },
    method: 'get'
  })
}

export const post = (url, formData, headers = {}) => {
    const token = Cookies.get('token')
    headers = Object.assign({}, headers, { Authorization: `Beare ${token}` })
    return axios({
        url,
        headers,
        method: 'post',
        data: qs.stringify(formData)
    }).then(res => {
      return res
    }).catch(e => {
      if (e.response.status === 401) {
        // Token timeout, access refresh token interface
        return fetchRefreshToken().then(res => {
          const { data } = res.data
          const { token, refresh_token: refreshToken } = data
          Cookies.set('token', token)
          Cookies.set('refreshToken', refreshToken)
          return post(url, formData, headers) // Re-invoke this interface
        })
      }
    })
}
...

Modify axios interception function

// The axios interceptor jumps to the login page if it is not logged in
axios.interceptors.response.use(
    res => {
        if (res.data.code === 'OK') {
            return res
        } else {
            // Tips for error reporting
            message.error(res.data.data, 10)
            return Promise.reject(res)
        }
    },
    error => {
        if (error.response) {
            switch (error.response.status) {
                case 402:
                    // Login timeout, jump to login page, self-realization
                   // store.dispatch(actionCreators.logoutSuccess())
                    break
                // Jump to the login page
                default:
                    break
            }
        }
        return Promise.reject(error)
    }
)

Optimization point

So far, token automatic refresh has been implemented. But this method is still flawed. For example, when multiple interfaces are invoked at the same time, if the token expires, because the request is asynchronous, the refreshtoken interface will be invoked many times. This optimal solution should be the first interface to refresh token, and the remaining interface should wait until token refreshes before it starts calling.
sf has An article This method is explained in more detail.

Posted by deansp2001 on Fri, 20 Sep 2019 00:11:14 -0700