Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Why is there a database hit at every request ? #371

Closed
StitiFatah opened this issue Feb 3, 2021 · 18 comments
Closed

Why is there a database hit at every request ? #371

StitiFatah opened this issue Feb 3, 2021 · 18 comments
Labels

Comments

@StitiFatah
Copy link

StitiFatah commented Feb 3, 2021

Hi and thanks in advance,

I've successfully setup JWT authentication using django-rest-framework-simplejwt and React but I'm still very confused about the advantages and specifically database hits.
I'm using simplejwt with ROTATE_REFRESH_TOKENS': True 'BLACKLIST_AFTER_ROTATION': True, when my access_token expire I ask for a new one through /api/token/refresh and it blacklist old tokens, I'm using axios interceptors to perform that automatically.

But in my understanding the benefits of JWt is that they are stateless, meaning I don't have to hit the user database table everytime I want to make an a request that needs authentication permission.
The problem is even with a simple view like this :

class IsConnecteddAPI(APIView):

    permission_classes = [permissions.IsAuthenticated]

    def get(self, request, *args, **kwargs):
        data = "You seem to be connected"

        return Response(data, status=status.HTTP_200_OK)

using django-silk I see that it still performs 1 query to my user table when my access token is valid , is that normal ? I'm really confused.

Here are django silk outputs
screen1
screen2
screen3
screen4

Isn't get_user from https://github.com/SimpleJWT/django-rest-framework-simplejwt/blob/master/rest_framework_simplejwt/authentication.py hiting the user object everytime ?

I've already asked the question on reddit and SO and they advised me to remove AuthMiddleware, so I removed both 'django.middleware.csrf.CsrfViewMiddleware' and 'django.contrib.auth.middleware.AuthenticationMiddleware' but I still get the same result. Setting permissions_classes to AllowAny doesn't vhange anything either.

That's my axios code if needed :

import axios from "axios";


const baseURL = "http://localhost:5000";

const axiosInstance = axios.create({
  baseURL: baseURL,
  timeout: 5000,
  headers: {
    Authorization: localStorage.getItem("accesstoken")
      ? "JWT " + localStorage.getItem("accesstoken")
      : null,
    "Content-Type": "application/json",
    accept: "application/json",
  },
});

const axioAnonymousInstance = axios.create({
  baseURL: baseURL,
  timeout: 5000,
  headers: {
    "Content-Type": "application/json",
    accept: "application/json",
  },
});

axiosInstance.interceptors.response.use(
  (response) => {
    return response;
  },
  async function (error) {
    const originalRequest = error.config;

    if (typeof error.response === "undefined") {
      alert(
        "A server/network error occurred. " +
          "Looks like CORS might be the problem. " +
          "Sorry about this - we will get it fixed shortly."
      );
      return Promise.reject(error);
    }

    if (
      error.response.status === 401 &&
      originalRequest.url === baseURL + "token/refresh/"
    ) {
      window.location.href = "/login/";
      return Promise.reject(error);
    }

    if (
      error.response.data.code === "token_not_valid" &&
      error.response.status === 401 &&
      error.response.statusText === "Unauthorized"
    ) {
      const refreshToken = localStorage.getItem("refreshtoken");

      if (refreshToken) {
        const tokenParts = JSON.parse(atob(refreshToken.split(".")[1]));

        // exp date in token is expressed in seconds, while now() returns milliseconds:
        const now = Math.ceil(Date.now() / 1000);
        console.log(tokenParts.exp);

        if (tokenParts.exp > now) {
          return axioAnonymousInstance
            .post("/api/token/refresh/", { refresh: refreshToken })
            .then((response) => {
              localStorage.setItem("accesstoken", response.data.access);
              localStorage.setItem("refreshtoken", response.data.refresh);

              axiosInstance.defaults.headers["Authorization"] =
                "JWT " + response.data.access;
              originalRequest.headers["Authorization"] =
                "JWT " + response.data.access;

              return axiosInstance(originalRequest);
            })
            .catch((err) => {
              // redirect ro /login here if wanted
              console.log("axios Safe Instance error");
              console.log(err);
              // window.location.href = "/login/";
            });
        } else {
          console.log("Refresh token is expired", tokenParts.exp, now);
          window.location.href = "/login/";
        }
      } else {
        console.log("Refresh token not available.");
        window.location.href = "/login/";
      }
    }

    // specific error handling done elsewhere
    return Promise.reject(error);
  }
);

export { axiosInstance, axioAnonymousInstance };

( I know I shouldn't use localStorage but whatever )

and I would typically just call this function to make the simple request to the view written above :

 const IsConnected = () => {
    axiosInstance
      .get("/api/is_connected/")
      .then((response) => {
        if (response.status === 200) {
          console.log(response.data);
          console.log("Is connected : CONNECTED ");
        } else {
          console.log("IS connected : not connected");
        }
      })
      .catch((error) => {
        console.log("Is connected : NOT CONNECTED");
        console.log(error);
      });
  };

@Andrew-Chen-Wang
Copy link
Member

Andrew-Chen-Wang commented Feb 3, 2021

Use the experimental JWT authentication class in your settings file instead: JWTTokenUserAuthentication

As for why the database hits, stateless authentication does not mean don't check if the user is still active. Stateless just means we don't keep the session data on the server side. Stateful authorization is when we store the session data in a cache/database.

Even for session/stateful authorization, it's been raised in Django long ago by BertrandBordage as to why we hit the database to make sure the user still exists: https://code.djangoproject.com/ticket/23011

Hope that clears it up!

Edit:

You should not be using React with SimpleJWT in the production side. Please visit https://github.com/Andrew-Chen-Wang/SPA-with-httponly-sessions You're still supposed to use session/stateful authorization for browsers since you need to set the session data as httpOnly in cookies.

If you use React with this repository, then you most likely set a cookie that isn't httpOnly. That 's a credential stealing risk. I recommend your team use SimpleJWT for local development and Django session middleware for production. Read the README in that repository.

@Andrew-Chen-Wang
Copy link
Member

Oh hold on, let me also add the advantages and disadvantages that I missed in that previous comment.

@StitiFatah
Copy link
Author

Ok it's confusing because every article I read which compare classic Tokens vs JWT points out that it doesn't need to hit the databsed to confirm the user is logged in

@Andrew-Chen-Wang
Copy link
Member

@StitiFatah Edited that initial comment.

@Andrew-Chen-Wang
Copy link
Member

Andrew-Chen-Wang commented Feb 3, 2021

Ok it's confusing because every article I read which compare classic Tokens vs JWT points out that it doesn't need to hit the databsed to confirm the user is logged in

I'm assuming you're talking about how 99% of tutorials use this repository when it comes to SPA development. It's an extremely saturated market for tutorials, so I'm not surprised that probably 80% of those tutorials have never actually read the code before. Please only use this repository for local development, and follow that tutorial for production and deployment.

Also:

classic Tokens vs JWT

There is no real difference except for how they are generated. What those tutorials are incorrectly talking about is how (factually true) each repository actually implements these authentication schemas. For example, DRF-JWT (archived) used SlidingToken method to allow you to continuously use one refresh token (dangerous since MITM attacks could happen). The repository uses two tokens and always passes on access/refresh style, like how OpenID Connect does it.

@StitiFatah
Copy link
Author

StitiFatah commented Feb 3, 2021

Ok it's confusing because every article I read which compare classic Tokens vs JWT points out that it doesn't need to hit the databsed to confirm the user is logged in

I'm assuming you're talking about how 99% of tutorials use this repository when it comes to SPA development. It's an extremely saturated market for tutorials, so I'm not surprised that probably 80% of those tutorials have never actually read the code before. Please only use this repository for local development, and follow that tutorial for production and deployment.

I'm referring to general talks about JWT not only ones related to django or to this particular library. But ok I'll read the tutorial, it took me some time to make it works with React and avoid infinite loops and stuff like that but if it's truly not recommended I should start again from scratch ( but why don't you say it on the main README ? )

@Andrew-Chen-Wang
Copy link
Member

Sorry if this text sounds harsh. The topic of JWT with SPAs is something that keeps popping up that I vehemently dislike due to being a huge security vulnerability. If I sound mean/angry, truly sorry about the language!

@StitiFatah
Copy link
Author

StitiFatah commented Feb 3, 2021

Sorry if this text sounds harsh. The topic of JWT with SPAs is something that keeps popping up that I vehemently dislike due to being a huge security vulnerability. If I sound mean/angry, truly sorry about the language!

Don't worry it didn't sound harsh at all, thanks for helping !

@Andrew-Chen-Wang
Copy link
Member

Andrew-Chen-Wang commented Feb 3, 2021

I'm referring to general tals about JWT not only ones related to django or to this particular library.

Hm, that's interesting. JWTs is not an authentication method. It's just a token, a format, something that can just be generated. How you use it is up to a package/library.

but why don't you say it on the main README ?

I used to have all the templates in an issue, but it too was trying to replace session authorization for SimpleJWT authorization (which is what you did). So I just restarted and created that aforementioned repository. I can't merge any PRs without a different maintainer (for security purposes), so that's why you don't see it in the README.

We'll add that repository to the README sooner or later.

I should start again from scratch

No! Use SimpleJWT for local development and keep all your code. For PRODUCTION, don't use SimpleJWT. If you need some help, let me know in that repository (since this method is actually no in the repository).

@Andrew-Chen-Wang
Copy link
Member

@StitiFatah I've update the README in that repository to be a little more clear regarding directions.

@StitiFatah
Copy link
Author

StitiFatah commented Feb 3, 2021

Hm, that's interesting. JWTs is not an authentication method. It's just a token, a format, something that can just be generated. How you use it is up to a package/library.

I should stop reading random articles on the internet and take them too seriously before reading official docs then, i should have learned the lesson while ago

No! Use SimpleJWT for local development and keep all your code. For PRODUCTION, don't use SimpleJWT. If you need some help, let me know in that repository (since this method is actually no in the repository).

OK thanks for your help ! I might take the time to read https://github.com/Andrew-Chen-Wang/SPA-with-httponly-sessions .
If I understand well you advice to use SimpleJWT in development ( to take advantage of hot reloading ) and using the old way ( session ) in production by putting the react app in the static files ?

It's my first React SPA and went with JWT to have a clear separation and don't have to worry about connecting react to django through django-webpack-loader or whatever ( If only I knew it'd be way more of a problem lol ) . When I was using Vue that's what i've been doing ( vue dist in static files ) and I got hot reloading without django webpack loader just using this in my vue config


module.exports = {
  publicPath: "/static/dist",
  outputDir: "../static_in_env/dist",
  indexPath: "../../templates/demo/index.html",
  //   productionSourceMap: false,
  filenameHashing: false,
  // Django will hash the files for us, not webpack
  configureWebpack: {
    output: {
      chunkFilename: "[id]-[chunkhash].js",
      // DO allow webpack to hash chunks.
    },
  },

  devServer: {
    writeToDisk: true,
    // Write files to disk in dev mode, so Django can serve the assets
  },
};

@Andrew-Chen-Wang
Copy link
Member

Yes, you are correct.

django-webpack-loader

Originally, that repository was kinda like a copy of webpack loader, just much smaller. But the new README suggests you don't use web pack at all.

When I was using Vue that's what i've been doing ( vue dist in static files ) and I got hot reloading without django webpack loader just using this in my vue config

Unfortunately, I'm not much of a JS developer haha. But I actually don't quite understand this. But from your description, it sounds like what you did before was mostly correct? Sorry still don't know much about SPA development itself. I think my repository was just providing a way to deploy your static files to GitHub pages as a CDN (and the same action if you deploy the built HTML from your JS repository to your Django repository).

@StitiFatah
Copy link
Author

StitiFatah commented Feb 3, 2021

Yes, you are correct.

django-webpack-loader

Originally, that repository was kinda like a copy of webpack loader, just much smaller. But the new README suggests you don't use web pack at all.

When I was using Vue that's what i've been doing ( vue dist in static files ) and I got hot reloading without django webpack loader just using this in my vue config

Unfortunately, I'm not much of a JS developer haha. But I actually don't quite understand this. But from your description, it sounds like what you did before was mostly correct? Sorry still don't know much about SPA development itself. I think my repository was just providing a way to deploy your static files to GitHub pages as a CDN (and the same action if you deploy the built HTML from your JS repository to your Django repository).

Yes it was fine with Vue. Thanks for your time and clarifications , I'll point out this issue in my topic on r/django and will surely check your repo !

@ExtReMLapin
Copy link

ExtReMLapin commented Oct 18, 2021

Was using dj-rest-auth which support simplejwt.
Each request (even empty ones) took at least 85-125ms because of that query.

Decided to rewrite dj-rest-auth's auth module to use the experimental's one.

Now it's 5 times faster.

However, on views, request.user obviously doesn't return the object but only the token which has id key (user key id)

@Routhinator
Copy link

For PRODUCTION, don't use SimpleJWT

Uhm, can we please elaborate on this statement? I don't see anything in the readme or the docs that says "this module is only for development, never use in prod" - To find this sort of slapped willy-nilly in a comment is jarring at the least.

@Andrew-Chen-Wang
Copy link
Member

For PRODUCTION, don't use SimpleJWT

Uhm, can we please elaborate on this statement? I don't see anything in the readme or the docs that says "this module is only for development, never use in prod" - To find this sort of slapped willy-nilly in a comment is jarring at the least.

It's an opinion of mine that if you're using an SPA with SimpleJWT, my opinion is to switch to using sessions. People have used SimpleJWT successfully with an SPA, but with the influx of tutorials, I've mostly left this out of any docs.

There's a years long PR that is implemented in dj-rest-auth that helps and uses simplejwt, so most have gone there

@Routhinator
Copy link

Interesting, so in your opinion this is better than using just SimpleJWT with the blacklist app and short lived tokens?

@Andrew-Chen-Wang
Copy link
Member

Yes, using sessions is much better. Instagram still does it to this day.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

4 participants