Is this djoser implementation secure?

This question may be too broad for StackOverflow, but I'm not sure where else to go for help.

I wrote a simple authentication system in Django with Djoser and its JWT implementation, using jQuery on the frontend. I really don't know what I'm doing though, and I'm about 99% sure I did it wrong and it's totally insecure.

First, when a user submits the login form, I send a POST request to retrieve a refresh token and an access token. The refresh token is stored as a cookie, and the access token is stored in sessionStorage:

// Post the form
$.post("/auth/jwt/create/", $(this).serialize())
    // Success: store tokens & redirect
    .done(function(data) {
        // Logged in: set redirect path & store tokens
        if (data.refresh !== "undefined" && data.access !== "undefined") {
            Cookies.set("refresh_token", data.refresh, { expires: 30, secure: true, sameSite: "strict" });
            sessionStorage.setItem("access_token", data.access);
        }
    })

I have another simple script that runs every time a page is loaded. There, I verify the access token, attempt a refresh if it's invalid, fetch user data using the access token, then post that user data to the backend to login. This script also logs the user out if on the logout page:

// Log in or out
function auth(data) {
    $.post("/auth/", {
        "user": data,
        "csrfmiddlewaretoken": $("meta[name='csrf-token']").attr("content"),
    });
}

// Remove tokens & log out
function logout() {
    Cookies.remove("refresh_token");
    sessionStorage.removeItem("access_token");
    auth("");
}

// Authorize: get user data & log in
function authorize() {
    $.ajax({
        url: "/auth/users/me/",
        headers: { "Authorization": "JWT "+sessionStorage.getItem("access_token") },
    })
        // Success: log in
        .done(function(data) { auth(JSON.stringify(data)); })
        // Fail: log out
        .fail(function() { logout(); });
}

// Verify access token & authorize
function verify() {
    $.post("/auth/jwt/verify/", { "token": sessionStorage.getItem("access_token") })
        // Success: authorize
        .done(function() { authorize(); })
        // Fail: refresh access token
        .fail(function() {
            $.post("/auth/jwt/refresh/", { "refresh": Cookies.get("refresh_token") })
                // Success: store new access token & authorize
                .done(function(data) {
                    sessionStorage.setItem("access_token", data.access);
                    authorize();
                })
                // Fail: log out
                .fail(function() { logout(); });
        });
}

// Log out page
if (window.location.pathname == "/logout/") {
    logout();
}

// Attempt login
else verify();

Finally, on the backend I log the user in or out with Django's native login and logout:

def auth(request):
    if not request.META.get('HTTP_X_REQUESTED_WITH') == 'XMLHttpRequest' or not request.method=="POST":
        return HttpResponseNotAllowed(["POST"])

    user_post = request.POST.get("user")
    user = None
    
    if user_post != "":
        user_post = json.loads(user_post)
        if "id" in user_post and "username" in user_post and "email" in user_post:
            user = User.objects.filter(id=user_post["id"], username=user_post["username"], email=user_post["email"]).first()
        
    if user == None:
        logout(request)
        return HttpResponse("User logged out.")
    else:
        login(request, user)
        return HttpResponse("User logged in.")

What I'm most worried about is the part where a simple POST request can log a user in with only its id, username and email. Although I'm going to keep the id hidden from the public, if a hacker does obtain it somehow (or just guesses it), they could easily bypass the need for a password or any tokens.

I don't think the authorization flow is quite right. In my opinion, it should work like this: you have a login endpoint, the user enters their credentials, e.g. email and password. You check the user in the database and if everything is ok, you return access_token and refresh_token. It also seems more logical to me that the server should set cookies with tokens, for example through its JWTTokensMiddleware or just at the endpoint.

For security - refresh_token cookie, will be stored with the httponly=True flag set so that it cannot be accessed at all from js code; access_token cookie will have the httponly=False flag, in turn you will read this cookie and set it in the Authorization header. Some people store access_token, in localStore - this is also an option, either approach, has its pros and cons.

Accordingly, if the user decides to log out, the backend will delete cookies with tokens. In order to log out, you need to provide a valid access_token through the header, and for a valid refresh_token to be in the cookie.

It also seems to me that token validation should also be handled by the server, at least for access_token validation, you need a secret key, and it would be much more reliable to store it on the server rather than the client.

For example, there could be custom JWTMiddleware for this, something like how Django, uses SessionMiddleware in conjunction with AuthenticationMiddleware.

On the client the only thing you check is the access_token, more specifically the expiration date, if the token is expired you send a request to the refresh_token endpoint and if everything is ok you get a new access_token, or a new pair of access_token + refresh_token, depending on how you decide to configure it.

Also, I have, for example, refresh_token is not jwt but a reliable random string of sufficient length. The refresh_token, in my case is stored in the database, in a hashed form, without using salt, and it is one-time, that is, when the client calls the endpoint - /api/token-refresh/, a new pair - access_token + refresh_token - is always returned, and the old refresh_token is considered invalid.

p.s. Maybe you need more details, or I haven't disclosed something enough, let me know in the comments and I'll try to give more details. Also, this is not a call to action, don't think that you should do only this way and no other way, just my thoughts and experience in implementing such authentication flow.

Back to Top