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, but I'm not sure if I did it right or if it's secure.
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 in a cookie or a session cookie (depending on a checkbox being ticked), and the access token is stored in a session cookie:
// 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") {
if (remember) Cookies.set("refresh_token", data.refresh, { expires: 30, secure: true, sameSite: "strict" });
else Cookies.set("refresh_token", data.refresh, { secure: true, sameSite: "strict" });
Cookies.set("access_token", data.access, { secure: true, sameSite: "strict" });
}
})
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 and the access token to the backend to login. This script also logs the user out if on the logout page:
$("meta[name='csrf-token']").ready(function() {
// Log in or out
function auth(data, access_token) {
$.post("/auth/", {
"user": data,
"access_token": access_token,
"csrfmiddlewaretoken": $("meta[name='csrf-token']").attr("content"),
});
}
// Remove tokens & log out
function logout(reload=true) {
Cookies.remove("refresh_token");
Cookies.remove("access_token");
auth("", "");
}
// Authorize: get user data & log in
function authorize() {
let access_token = Cookies.get("access_token");
$.ajax({
url: "/auth/users/me/",
headers: { "Authorization": "JWT " + access_token },
})
// Success: log in
.done(function(data) { auth(JSON.stringify(data), access_token); })
// Fail: log out
.fail(function() { logout(); });
}
// Refresh access token
function refresh() {
if ("refresh_token" in Cookies.get()) {
$.post("/auth/jwt/refresh/", { "refresh": Cookies.get("refresh_token") })
// Success: store new access token & authorize
.done(function(data) {
Cookies.set("access_token", data.access, { secure: true, sameSite: "strict" });
authorize();
})
// Fail: log out
.fail(function() { logout(); });
}
// No refresh token: log out
else logout();
}
// Verify access token & authorize or refresh
function verify() {
if ("access_token" in Cookies.get()) {
$.post("/auth/jwt/verify/", { "token": Cookies.get("access_token") })
// Success: authorize
.done(function() { authorize(); })
// Fail: refresh access token
.fail(function() { refresh(); });
// No access token: refresh
} else refresh();
}
// Log out page
if (window.location.pathname == "/logout/") {
// Log out & redirect
logout(false);
}
// Attempt login
else verify();
});
Finally, on the backend I log the user in or out with Django's native login
and logout
, checking the POSTed access token against the session cookie:
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 != "" and request.POST.get("access_token") == request.COOKIES.get('access_token'):
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 and request.user != None and request.user.is_authenticated:
logout(request)
return HttpResponse("User logged out.")
elif user != None and user != request.user:
login(request, user)
return HttpResponse("User logged in.")
else:
return HttpResponse("No change.")
What I'm most worried about is the part where a user can be logged in through POST without a password (but with an access token).
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.
UPDATED
Django by default uses session based authentication. Now if you look at your auth
function that you provided, you verify the tokens and then call Django's functions: login or logout. The login
function will in turn write the user's ID to the session, so that on subsequent user requests, Django can authenticate that user via the session cookie. And the logout
function will simply delete the session, which will cause the session cookies to be deleted. The problem now is that you kind of have two approaches: token and session. Usually one or the other is chosen.
All djozer
does is, it provides some ready-made endpoints to handle jwt tokens. Under the hood, djozer
, delegates all work with JWT tokens to the simple-jwt library. Which in turn uses the django-rest-framework. I think that if you are still new to Django, and you don't plan to use django-rest-framework
yet, try using the classic Django authentication system using sessions. It's been tested for years, it's easy to extend, and it will be much more secure than your current implementation. Or you can use simple-jwt
directly without using djozer
, in which case you will have much more control.