thắc mắc [ReactJS + NodeJS] Refreshtoken bị lỗi khi 1 user gửi 2 yêu cầu cùng lúc

Hello.World

Senior Member
Hi các bác, em đang gặp 1 vấn đề như sau ạ.
Để làm chức năng duy trì đăng nhập ở api e dùng jwt lưu refreshtoken (RT) vào database và đồng thời gửi RT cho client dưới dạng cookie, mỗi khi accesstoken (AT) hết hạn, nếu trong cookie có RT thì client sẽ tự động gửi RT về server để lấy AT và RT mới. Trang client của em có 1 vài trang cùng 1 thời điểm sẽ có nhiều component dispatch action có yêu cầu xác thực trong đó nên sẽ gửi yêu RT khi AT hết hạn, ví dụ như component Cart và Profile, khi AT hết hạn mà mình refresh trang thì đồng thời Cart và Profile sẽ dispatch action để lấy dữ liệu về, khi đó server sẽ response đầu tiên là ""user has been hacked" sau đó nó rơi vào vòng lặp response liên tục "Not found the key", RT và AT sẽ bị xoá.
Theo em hiểu thì khi yêu cầu refreshtoken của th Cart đang được xử lí khi đó RT hiện tại đã bị xoá khỏi database nhưng chưa được cấp RT mới thì đồng thời yêu cầu refreshtoken của th Profile cũng nhảy vào dẫn đến server xác định RT của user đó bị đánh cắp dẫn đến lỗi trên. Em đang tạm giải quyết ở phần client là cho bọn nó xếp hàng lần lượt, khi nào dispatch nhận response từ server thì mới cho dispatch tiếp theo gửi yêu cầu tới server, ví dụ: khi th Cart dispatch action và nhận response từ server thì th Profile mới đc dispatch action thì không bị lỗi trên nữa. Hiện tại em đang thắc mắc có phải logic của em bị sai ở đâu không, có cách giải quyết khác nào không ạ? Văn em lủng củng, mong các bác bỏ qua và giúp đỡ ạ :burn_joss_stick:

Code phần axios ở client:
JavaScript:
const axiosPrivate = axios.create({
  baseURL: BASE_URL,
  headers: {
    'Content-Type': 'application/json'
  },
  withCredentials: true
});

axiosPrivate.interceptors.request.use(
  config => {
    if (!config.headers['accesstoken']) {
      config.headers['accesstoken'] = `Bearer ${getLocalAccessToken()}`;
    }

    return config;
  }, (error) => Promise.reject(error)
)

axiosPrivate.interceptors.response.use(
  response => response,
  async (error) => {
    const prevRequestConfig = error?.config;
    if (error?.response?.status === 403 && error?.response?.data === "logout") {
      clearLocalStorage();
      return window.location.reload();
    }

    if (error?.response?.status === 401 && !prevRequestConfig.sent) {
      const newAccessToken = await refreshToken();

      return axiosPrivate({
        ...prevRequestConfig,
        headers: {
          ...prevRequestConfig.headers,
          accesstoken: `Bearer ${newAccessToken}`,
          sent: true
        }
      });
    }
    return Promise.reject(error);
  }
);

Code request refreshToken của client:
JavaScript:
import axiosPrivate from "../shared/axios/requestMethod";
import { updateLocalAccessToken } from "./localStorage";

export const refreshToken = async () => {
  const res = await axiosPrivate.get('/refreshToken/client');
  updateLocalAccessToken(res.data.accessToken);
  return res.data.accessToken;
}

Code xử lí refreshtoken ở api:
JavaScript:
export const refreshTokenClient = async (req, res) => {
  const cookies = req.cookies;
  if (!cookies?.jwtClient) return res.status(401).json("Not found the key");

  const refreshToken = cookies.jwtClient;

  res.clearCookie('jwtClient', { httpOnly: true, sameSite: 'None', secure: true });

  const user = await Customer.findOne({ refreshToken: refreshToken }).exec();

  if (!user) {
    jwt.verify(refreshToken, process.env.REFRESH_TOKEN_KEY_CLIENT, async (err, decoded) => {
      if (err) return res.status(401).json(err);

      const hackedUser = await Customer.findOne({ _id: decoded._id }).exec();
      hackedUser.refreshToken = [];
      await hackedUser.save();
    })
    return res.status(401).json("user has been hacked");
  }

  const newRTArr = user.refreshToken.filter(item => item !== refreshToken);

  jwt.verify(refreshToken, process.env.REFRESH_TOKEN_KEY_CLIENT, async (err, decoded) => {
    if (err) {
      user.refreshToken = [...newRTArr];
      await user.save();
      return res.status(401).json(err);
    }

    if (user._id != decoded._id) return res.status(401).json("????");

    const accessToken = jwt.sign(
      {
        _id: user._id,
        isActive: user.isActive,
        role: user.role
      },
      process.env.JWT_KEY_CLIENT,
      { expiresIn: '15m' }
    );

    const newRefreshToken = jwt.sign(
      {
        _id: user._id,
        role: user.role,
        isActive: user.isActive
      },
      process.env.REFRESH_TOKEN_KEY_CLIENT
    )

    user.refreshToken = [...newRTArr, newRefreshToken];
    await user.save();

    res.cookie('jwtClient', newRefreshToken, { httpOnly: true, sameSite: 'None', secure: true, maxAge: 24 * 60 * 60 * 1000 });
    res.status(201).json({ accessToken });
  })
};
 
Vụ này xưa làm rồi thì phải. Giả sử việc refresh token sẽ trả về refresh token mới & access token mới & invalidate refesh token cũ. Tình huống là như này:

1. Client gửi request A & B đồng thời, cùng sử dụng một access token (AT1).
2. Request A response lại với lỗi expired RT. Interceptor bắt đc và lấy refresh token (RT1) trong storage ra để lấy AT mới, gọi là AT2. Sau đó retry lại request A với AT2.
3. Trong khi step 2 đang chờ lấy AT mới, request B gặp lỗi tương tự và lại refresh access token sử dụng RT1. Lúc này RT1 đã bị consume bởi step 2 => Gặp lỗi không thể refresh & force logout.

Case này của mình còn 1 down side nữa là gọi refresh chỉ cần 1 lần là đủ. Làm như trên thì N request gọi đồng thời sẽ phải refresh N lần.
 
Vụ này xưa làm rồi thì phải. Giả sử việc refresh token sẽ trả về refresh token mới & access token mới & invalidate refesh token cũ. Tình huống là như này:

1. Client gửi request A & B đồng thời, cùng sử dụng một access token (AT1).
2. Request A response lại với lỗi expired RT. Interceptor bắt đc và lấy refresh token (RT1) trong storage ra để lấy AT mới, gọi là AT2. Sau đó retry lại request A với AT2.
3. Trong khi step 2 đang chờ lấy AT mới, request B gặp lỗi tương tự và lại refresh access token sử dụng RT1. Lúc này RT1 đã bị consume bởi step 2 => Gặp lỗi không thể refresh & force logout.

Case này của mình còn 1 down side nữa là gọi refresh chỉ cần 1 lần là đủ. Làm như trên thì N request gọi đồng thời sẽ phải refresh N lần.
đúng r bác, cách giải thích của bác e thấy hiểu hơn về cái lỗi củ cẹc trên ạ, bác có keyword gì gợi mở hơn để e tìm hiểu thêm được k ạ? cách gọi RT 1 lần k lẽ mình tính time ở phía client nếu AT exprired thì gửi yêu cầu RT tới server ạ?

// Em tìm thấy keyword r ạ, cám ơn các bác nhiều :sweet_kiss:
 
Last edited:
đúng r bác, cách giải thích của bác e thấy hiểu hơn về cái lỗi củ cẹc trên ạ, bác có keyword gì gợi mở hơn để e tìm hiểu thêm được k ạ? cách gọi RT 1 lần k lẽ mình tính time ở phía client nếu AT exprired thì gửi yêu cầu RT tới server ạ?

// Em tìm thấy keyword r ạ, cám ơn các bác nhiều :sweet_kiss:
Share đi bác. Em biết vấn đề thôi chứ cách e cũng chưa hẳn tối ưu. Với việc 1 refresh token refresh đc nhiều lần thì đơn giản chỉ là waste vài request thôi. Nhưng với việc refresh token bị renew như em nói trên thì phải làm cách khác
 
Share đi bác. Em biết vấn đề thôi chứ cách e cũng chưa hẳn tối ưu. Với việc 1 refresh token refresh đc nhiều lần thì đơn giản chỉ là waste vài request thôi. Nhưng với việc refresh token bị renew như em nói trên thì phải làm cách khác
e search lọ mọ thì ra hướng giải quyết với keyword kiểu như: Axios interceptor refresh token for multiple requests. Có vẻ như cách ng ta hay làm là đặt 1 biến ở axios để nói cho các request sau biết được th trước có đang yêu cầu refreshtoken k? nếu đang yêu cầu thì bọn sau chờ newtoken r hãy request. E để ý thì hầu như giải quyết từ phía client, phía server bác trên có báo dùng redlock nhưng e chưa thử được.

https://stackoverflow.com/questions...-requests-when-making-simultaneous-api-reques
https://stackoverflow.com/questions...token-for-multiple-requests?noredirect=1&lq=1
 
e search lọ mọ thì ra hướng giải quyết với keyword kiểu như: Axios interceptor refresh token for multiple requests. Có vẻ như cách ng ta hay làm là đặt 1 biến ở axios để nói cho các request sau biết được th trước có đang yêu cầu refreshtoken k? nếu đang yêu cầu thì bọn sau chờ newtoken r hãy request. E để ý thì hầu như giải quyết từ phía client, phía server bác trên có báo dùng redlock nhưng e chưa thử được.

https://stackoverflow.com/questions...-requests-when-making-simultaneous-api-reques
https://stackoverflow.com/questions...token-for-multiple-requests?noredirect=1&lq=1
Thế cũng hơi giống mình. Các requests share nhau 1 refresh promise. Reject cùng nhau & fulfill cùng nhau
 
Trước khi request, bạn kiểm tra xem at đã hết hạn hay chưa, nếu hết hạn thì chủ động gọi api refresh token. Viết lại hàm refreshToken, dùng 1 promise để lock giống cách trên mạng.

via theNEXTvoz for iPhone
 
Last edited:
Trước khi request, bạn kiểm tra xem at đã hết hạn hay chưa, nếu hết hạn thì chủ động gọi api refresh token. Viết lại hàm refreshToken, dùng 1 promise để lock giống cách trên mạng.

via theNEXTvoz for iPhone
như vậy là trước mỗi lần request tới server thì mình verify AT ở ngay trên client để chủ động gọi api refresh token à bác?
 
Nhưng tại sao lại lưu access token và refresh token trong cookie ? Cái này cực kì insecure.

Thím nên tìm hiểu lại tác dụng của cookie và nên dùng nó trong trường hợp nào. Nói chung nếu thím đã send token ở header rồi thì k đặt trong cookie nữa. Ở client thì lưu trong localstorage.
 
Nhưng tại sao lại lưu access token và refresh token trong cookie ? Cái này cực kì insecure.

Thím nên tìm hiểu lại tác dụng của cookie và nên dùng nó trong trường hợp nào. Nói chung nếu thím đã send token ở header rồi thì k đặt trong cookie nữa. Ở client thì lưu trong localstorage.
Dựa vào đâu nói insecure thế bro? Mình vẫn hay lưu vào cookie, vì nó hỗ trợ set expiry và SSR.
 
Dựa vào đâu nói insecure thế bro? Mình vẫn hay lưu vào cookie, vì nó hỗ trợ set expiry và SSR.
Vì cookie tự động được gửi lên server theo mỗi request, nên khá vulnerable với các dạng client side attack. Ví dụ như cái này là common nhất: https://owasp.org/www-community/attacks/csrf

Lưu trữ token trong cookie chỉ có lợi thế là nó sẽ được tự động được gửi lên server. Khi đã dùng OAuth2 và gửi access token trên header thì việc này trở nên vô nghĩa, chỉ làm giảm tính bảo mật.
 
Vì cookie tự động được gửi lên server theo mỗi request, nên khá vulnerable với các dạng client side attack. Ví dụ như cái này là common nhất: https://owasp.org/www-community/attacks/csrf

Lưu trữ token trong cookie chỉ có lợi thế là nó sẽ được tự động được gửi lên server. Khi đã dùng OAuth2 và gửi access token trên header thì việc này trở nên vô nghĩa, chỉ làm giảm tính bảo mật.
Cái đó thì đúng. Nhưng biết tránh là được. Làm SPA thì CSRF ko phải vấn đề lớn. Vì API hầu hết stateless, authenticate thường là dùng Bearer token + JWT. Với bài toán server side rendering thì buộc phải sử dụng Cookie, encrypted hoặc ko, nhưng ko consume thẳng bởi API mà bởi SSR request.
 
về phía client e thấy gần như giống nhau, nếu khác nhau e thấy giải quyết ở phía client hay server :pudency:
Server thì đơn giản hơn chứ. Ko rõ bro gặp vấn đề gì.
Solution như trên ở client sẽ gặp tình huống khi mà request body ở dạng consumable (stream). Ngoài ra có khả năng loop inf nếu như luồng refresh token lại trigger một luồng refresh token khác.
 
Cái đó thì đúng. Nhưng biết tránh là được. Làm SPA thì CSRF ko phải vấn đề lớn. Vì API hầu hết stateless, authenticate thường là dùng Bearer token + JWT. Với bài toán server side rendering thì buộc phải sử dụng Cookie, encrypted hoặc ko, nhưng ko consume thẳng bởi API mà bởi SSR request.
Thì đúng nhưng đó là khi làm server side rendering. Còn mình chỉ bàn trong case cụ thể này thì việc lưu token vào cookie nó vô nghĩa và insecure thôi. Vì token đã được gửi qua header rồi.
 
Back
Top