Безопасна ли эта реализация djoser?

Этот вопрос может быть слишком широким для StackOverflow, но я не уверен, куда еще можно обратиться за помощью.

Я написал простую систему аутентификации на Django с помощью Djoser и его реализации JWT, используя jQuery на фронтенде. Я действительно не знаю, что я делаю, и я на 99% уверен, что я сделал это неправильно и это абсолютно небезопасно.

Сначала, когда пользователь отправляет форму входа, я посылаю POST-запрос для получения маркера обновления и маркера доступа. Токен обновления хранится в cookie, а токен доступа - в 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);
        }
    })

У меня есть еще один простой скрипт, который запускается каждый раз, когда загружается страница. В нем я проверяю токен доступа, пытаюсь обновить его, если он недействителен, получаю данные пользователя, используя токен доступа, а затем отправляю эти данные пользователя на бэкэнд для входа в систему. Этот скрипт также выводит пользователя из системы, если он находится на странице выхода:

// 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();

Наконец, на бэкенде я регистрирую пользователя в системе или из нее с помощью встроенных в Django login и 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.")

Больше всего меня беспокоит та часть, где простой POST-запрос может зарегистрировать пользователя, используя только его id, имя пользователя и электронную почту. Хотя я собираюсь держать идентификатор скрытым от общественности, если хакер каким-то образом получит его (или просто угадает), он сможет легко обойти необходимость в пароле или каких-либо токенах.

Мне кажется, что поток авторизации не совсем правильный. На мой взгляд, это должно работать следующим образом: у вас есть конечная точка входа, пользователь вводит свои учетные данные, например, email и пароль. Вы проверяете пользователя в базе данных и, если все в порядке, возвращаете access_token и refresh_token. Мне также кажется более логичным, что сервер должен устанавливать cookies с маркерами, например, через свои JWTTokensMiddleware или просто в конечной точке.

Для безопасности - refresh_token куки будут храниться с установленным флагом httponly=True, чтобы к ним вообще нельзя было получить доступ из js кода; access_token куки будут иметь флаг httponly=False, в свою очередь вы будете читать эти куки и устанавливать их в Authorization заголовок. Некоторые люди хранят access_token в localStore - это тоже вариант, у любого подхода есть свои плюсы и минусы.

Соответственно, если пользователь решит выйти из системы, бэкенд удалит куки с токенами. Чтобы выйти из системы, необходимо предоставить действительный access_token через заголовок, и чтобы действительный refresh_token был в куках.

Мне также кажется, что проверка токенов также должна выполняться сервером, по крайней мере, для проверки access_token вам нужен секретный ключ, и было бы гораздо надежнее хранить его на сервере, а не на клиенте.

Например, для этого можно было бы использовать пользовательские JWTMiddleware, что-то вроде того, как Django использует SessionMiddleware в сочетании с AuthenticationMiddleware.

На клиенте единственное, что вы проверяете, это access_token, а точнее дату истечения срока действия, если срок действия токена истек, вы отправляете запрос на конечную точку refresh_token и, если все в порядке, получаете новый access_token, или новую пару access_token + refresh_token, в зависимости от того, как вы решите это настроить.

Кроме того, у меня, например, refresh_token - это не jwt, а надежная случайная строка достаточной длины. refresh_token, в моем случае, хранится в базе данных, в хешированном виде, без использования соли, и является одноразовой, то есть, когда клиент вызывает конечную точку - /api/token-refresh/, всегда возвращается новая пара - access_token + refresh_token, а старая refresh_token считается недействительной.

p.s. Возможно, вам нужно больше деталей, или я недостаточно раскрыл что-то, дайте мне знать в комментариях, и я постараюсь дать больше подробностей. Также, это не призыв к действию, не думайте, что вы должны делать только так и никак иначе, просто мои мысли и опыт реализации подобного потока аутентификации.

Вернуться на верх