IndominusByte / fastapi-jwt-auth

FastAPI extension that provides JWT Auth support (secure, easy to use, and lightweight)
http://indominusbyte.github.io/fastapi-jwt-auth/
MIT License
644 stars 150 forks source link

Sliding sessions #38

Closed hestal closed 3 years ago

hestal commented 3 years ago

I really appreciate your work on this project.

It could be a nice feature to have sliding sessions available, i.e. sending fresh access tokens on every request, if a certain time of inactivity is not exceeded.

Some information about this approach can be found here https://auth0.com/blog/refresh-tokens-what-are-they-and-when-to-use-them/.

Any suggestions how this could be implemented? Maybe as a middleware?

Best regards

IndominusByte commented 3 years ago

it would be nice if you implemented sliding sessions from the frontend app because if the access token is stolen by an attacker, then attacker can use the access token to access your all endpoint cause you always generate fresh access token on every request without using refresh token from the user and you will end up generating lots of tokens which will expire by themselves.

paulussimanjuntak commented 3 years ago

You can handle this problem from Axios interceptor and if the current request is token expired then use the refresh token from the user and use them to generate a new access token. In your function, you can catch the error message, if the message is token expired, you can run that function again.

here an example of my Axios interceptors, implemented in react app

import axios from "axios";
import * as actions from "store/actions";
import { parseCookies } from "nookies";

// For refresh
export const signature_exp = "Signature has expired";

// Need logout
const not_enough_seg = "Not enough segments";
const token_rvd = "Token has been revoked";
const signature_failed ="Signature verification failed";
const csrf_not_match = "CSRF double submit tokens do not match";
const invalid_alg = "The specified alg value is not allowed";
const invalid_header_str = "Invalid header string: 'utf-8' codec can't decode byte 0xab in position 22: invalid start byte";

const instance = axios.create({
  baseURL: process.env.NEXT_PUBLIC_API_URL,
  withCredentials: true,
});

instance.interceptors.request.use((config) => {
    // Do something before request is sent
    return config;
  },
  function (error) {
    // Do something with request error
    return Promise.reject(error);
  }
);

export const installInterceptors = (store) => {
  instance.interceptors.response.use((response) => {
    return response;
  }, async error => {
    const cookies = parseCookies();
    const { csrf_refresh_token } = cookies;

    const { status, data, config } = error.response;

    /*
     * This section will run if the refresh token has been expired
     * and delete all cookies
     * TODO:
     * - Check for more condition
    */
    if(status == 422 && data.detail == signature_exp && csrf_refresh_token && config.url === "/users/refresh-token"){
      instance.delete("/users/delete-cookies")
      store.dispatch(actions.logout());
      return Promise.reject(error);
    }

    if(status == 404){
      return error.response;
    }

    /*
     * Section when token is invalid and must be logout
     * TODO:
     * - Check for more condition
     */
    if(status == 401 && ((data.detail == token_rvd) || (data.detail == csrf_not_match) || (data.detail == invalid_alg))){
      instance.delete("/users/delete-cookies")
      store.dispatch(actions.logout());
    }
    if(status == 422 && ((data.detail == signature_failed) || (data.detail == not_enough_seg) || (data.detail == invalid_header_str))){
      instance.delete("/users/delete-cookies")
      store.dispatch(actions.logout());
    }

    /*
     * Section when token is expired and must be refreshed
     * DO:
     * - resolve request after token expired
     * - passed in update password
     *
     * TODO:
     * - Check for more condition
     */
    if(status == 422 && data.detail == signature_exp && csrf_refresh_token){
      await instance.post("/users/refresh-token", null, refreshHeader())
        .then(() => {
          const { csrf_access_token } = parseCookies();
          const needResolve = {
            ...error.config,
            headers: {
              ...error.config.headers,
              "X-CSRF-TOKEN": csrf_access_token,
            },
          }

          return instance.request(needResolve);
        })
    }

    return Promise.reject(error);
  });
}

export default instance;
hestal commented 3 years ago

Thank you both. The comments clarified the flow for me and the code sample is really helpful.