Одностраничное приложение с HttpOnly аутентификацией на основе cookie и управлением сеансом

В течение нескольких дней я искал механизм безопасной проверки подлинности и управления сеансом для моего одностраничного приложения. Судя по многочисленным учебникам и сообщениям в блогах о SPA-процедурах и аутентификации, хранение JWT в localStorage или обычных куках, по-видимому, является наиболее распространенным подходом, но это просто не хорошая идея использовать JWT для сеансов, поэтому это не вариант для этого приложения.

Требования

  • Логины должны быть revokable. Например, если подозрительная активность обнаружена в учетной записи пользователя, она должна немедленно отменить этот доступ пользователя и потребовать новый вход в систему. Аналогично, такие вещи, как сброс пароля, должны аннулировать все существующие логины.
  • Аутентификация должна произойти над Ajax. После этого не должно быть перенаправления браузера ( "жесткое" перенаправление). Вся навигация должна произойти внутри SPA.
  • Все должно работать в кросс-домене. SPA будет на www.domain1.com, а сервер будет на www.domain2.com.
  • JavaScript не должен иметь доступа к конфиденциальным данным, отправляемым сервером, например идентификаторам сеанса. Это необходимо для предотвращения атак XSS, когда вредоносные скрипты могут украсть токены или идентификаторы сеансов из обычных файлов cookie, localStorage или sessionStorage.

Идеальный механизм, по-видимому, является аутентификацией на основе файлов cookie с использованием HttpOnly файлов cookie, содержащих идентификаторы сеанса. Поток будет работать следующим образом:

  • Пользователь приходит на страницу входа и отправляет свое имя пользователя и пароль.
  • Сервер аутентифицирует пользователя и отправляет идентификатор сеанса в качестве файла cookie HttpOnly.
  • Затем SPA включает этот файл cookie в последующие запросы XHR, сделанные на сервере. Возможно, это возможно с помощью опции withCredentials.
  • Когда запрос делается для защищенной конечной точки, сервер ищет файл cookie. Если он найден, он перекрестно проверяет этот идентификатор сеанса на таблицу базы данных, чтобы убедиться, что сеанс по-прежнему действителен.
  • Когда пользователь выходит из системы, сеанс удаляется из базы данных. В следующий раз, когда пользователь приходит на сайт, SPA получает ответ 401/403 (так как срок действия сессии истек), а затем выводит пользователя на экран входа в систему.

Поскольку cookie имеет флаг HttpOnly, JavaScript не сможет прочитать его содержимое, поэтому он будет безопасен от атак, пока он передается через HTTPS.

Проблемы

Вот конкретная проблема, с которой я столкнулся. Мой сервер настроен для обработки запросов CORS. После аутентификации пользователя он правильно отправляет файл cookie в ответ:

HTTP/1.1 200 OK
server: Cowboy
date: Wed, 15 Mar 2017 22:35:46 GMT
content-length: 59
set-cookie: _myapp_key=SFMyNTYBbQAAABBn; path=/; HttpOnly
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
x-request-id: qi2q2rtt7mpi9u9c703tp7idmfg4qs6o
access-control-allow-origin: http://localhost:8080
access-control-expose-headers: 
access-control-allow-credentials: true
vary: Origin

Однако браузер не сохраняет файл cookie (когда я проверяю локальные файлы cookie Chrome, он не существует). Поэтому, когда выполняется следующий код:

context.axios.post(LOGIN_URL, creds).then(response => {
  context.$router.push("/api/account")
}

Создается страница учетной записи:

created() {
  this.axios.get(SERVER_URL + "/api/account/", {withCredentials: true}).then(response => {
    //do stuff
  }
}

Этот вызов не содержит cookie в заголовке. Поэтому сервер отклоняет его.

GET /api/account/ HTTP/1.1
Host: localhost:4000
Connection: keep-alive
Accept: application/json
Origin: http://localhost:8080
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36
Referer: http://localhost:8080/
Accept-Encoding: gzip, deflate, sdch, br
Accept-Language: en-US,en;q=0.8,tr;q=0.6

Нужно ли делать что-то особенное, чтобы браузер сохранял cookie после получения его в ответе на вход? Различные источники, которые я прочитал, сказали, что браузеры сохраняют файлы ответов cookie только тогда, когда пользователь перенаправляется, но я не хочу никаких "жестких" перенаправлений, поскольку это SPA.

Рассматриваемый SPA-код написан на Vue.js, но, я думаю, это относится ко всем SPA. Мне интересно, как люди справляются с этим сценарием.

Другие материалы, которые я прочитал по этой теме:

Ответы

Ответ 1

С полномочиями контролирует настройку и отправку файлов cookie, поэтому не забудьте включить его при публикации для входа, чтобы возвращенный файл cookie был сохранен в браузере.

Ответ 2

Я получил эту работу на Vue.js 2 с credentials = true. Установка учетных данных с сайта клиента только наполовину. Вам также необходимо установить заголовки ответов с сервера:

    header("Access-Control-Allow-Origin: http://localhost:8080");
    header("Access-Control-Allow-Credentials: true");

Вы не можете использовать подстановочный знак для Access-Control-Allow-Origin следующим образом:

header("Access-Control-Allow-Origin: *");

Когда вы указываете заголовок credentials: true, вам необходимо указать orgin.

Как вы можете видеть, это PHP-код, вы можете смоделировать его на NodeJS или на любом языке сценариев на стороне сервера, который вы используете.

В VueJS я устанавливаю учетные данные = true, как это в main.js:

Vue.http.options.credentials = true

В компоненте я успешно выполнил вход с помощью ajax:

<template>
<div id="userprofile">
    <h2>User profile</h2>
    {{init()}}

</div>
</template>

<script>
    export default {  
        name: 'UserProfile',
        methods: {
        init: function() {


                // loggin in
              console.log('Attempting to login');

            this.$http.get('https://localhost/api/login.php')
            .then(resource =>  {
             // logging response - Notice I don't send any user or password for testing purposes  

            });

            // check the user profile

                console.log('Getting user profile');

                this.$http.get('https://localhost/api/userprofile.php')
                .then(resource =>  {
                    console.log(resource.data);
                })

        }
    }
    }

</script>

На стороне сервера все довольно просто: Login.php устанавливает cookie без какой-либо проверки (Примечание: это делается только для целей тестирования. Вам не рекомендуется использовать этот код в процессе производства без проверки)

<?php

header("Access-Control-Allow-Origin: http://localhost:8080");
header("Access-Control-Allow-Credentials: true");

$cookie = setcookie('user', 'student', time()+3600, '/', 'localhost', false , true);

if($cookie){
  echo "Logged in";
}else{
  echo "Can't set a cookie";
}

Наконец, userprofile.php просто проверяет, установлен ли файл cookie =

<?php

  header("Access-Control-Allow-Origin: http://localhost:8080");
    header("Access-Control-Allow-Credentials: true");


if(isset($_COOKIE['user'])){
  echo "Congratulations the user is ". $_COOKIE['user'];

}else{
  echo "Not allowed to access";
}

Успешно вошел в систему

Успешно вошел в систему