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.