Авторизация действий пользователя #

Васильев Андрей Михайлович, 2024

Версии презентации

Конфиденциальность данных #

Операции над данными должны осуществляться только уполномоченными лицами

Выделенные группы пользователей #

Рассмотрим возможности, доступные на информационном портале

  • Большинство пользователей может только просматривать опубликованные статьи
  • Зарегистрированные пользователи могут добавлять комментарии
  • Модераторы могут удалять комментарии
  • Авторы статей могут добавлять новые элементы на ресурс
  • Администрации ресурса доступны все функции

Системы разделения полномочий #

  • Каждое действие внутри системы рассматривается отдельно: просмотр, редактирование, добавление, удаление
  • Пользователю предоставляется доступ к конкретным действиям

Аутентификация с сохранением сессии #

HTTP cookie — это небольшой фрагмент данных, отправляемый сервером на браузер пользователя, который может сохранить и отсылать обратно с новым запросом к серверу

Используются в веб-приложениями для:

  • Управления сеансами пользователя, аутентификации
  • Персонализации содержимого страниц
  • Отслеживания поведения пользователя

diagram

Заголовки Set-Cookie и Cookie #

Заголовки Set-Cookie устанавливаются сервером в рамках своего ответа

Простой заголовок может выглядит так:

Set-Cookie: <имя cookie>=<значение cookie>

Таких заголовков в ответе сервера может быть несколько:

Set-Cookie: yummy_cookie=choco
Set-Cookie: tasty_cookie=strawberry

Браузер будет передавать их клиенту в рамках заголовка Cookie:

Cookie: yummy_cookie=choco; tasty_cookie=strawberry

От клиента на сервер может передаваться несколько куков

Время жизни куков #

  • Сессионные куки удаляются при закрытии клиента, то есть существуют только на протяжении текущего сеанса. Однако время завершения сеанса определяется клиентом, что может сделать их вечными
  • Постоянные куки удаляются не с закрытием клиента, а при наступлении определённого интервала времени
Set-Cookie: id=5aoeu; Expires=Wed, 20 Nov 2010 10:15:00 GMT;

Безопасность HTTP куков #

  • Данные передаются в открытом виде, для защиты нужно использовать SSL-шифрование
  • Для защиты данных куки от доступа из JavaScript, необходимо использовать атрибут HttpOnly
  • Можно явно указать домены, с которых будет доступна информация с помощью атрибутов Domain и Path
  • «Раздражающее» сообщение о куках необходимо по законодательству сообщать пользователю об их использовании веб-приложением и их назначением. Многие JavaScript-библиотеки добавляют свои куки

Работа с куками в http4k #

Для описания куки используется класс Cookie, включающий поля

  • val domain: String? — домен, для которого данная кука действует
  • val expires: LocalDateTime? — время истечения действия куки
  • val httpOnly: Boolean — ограничение доступа из контекста JavaScript
  • val maxAge: Long? — время жизни куки
  • val name: String — наименование куки
  • val path: String? — путь в домене, который может иметь доступ к куки
  • val sameSite: SameSite? — политика передачи куки от браузера к серверу
  • val secure: Boolean — необходимо ли использовать защищённое соединение
  • val value: String — значение куки

Управление куки в запросах #

Для чтения куков из запросов и записи куков в ответы предлагаются следующие методы

  • fun Request.cookies(): List<Cookie> — получить весь набор куков
  • fun Request.cookie(name: String): Cookie? — получить куки по имени
  • fun Response.cookie(cookie: Cookie): Response — добавить куки к ответу
  • fun Request.cookie(name: String, value: String): Request — добавить простую куки по названию и значению
  • fun Response.invalidateCookie(name: String, domain: String? = null): Response — установить пустое значение для куки, чтобы клиент её удалил

Существуют и другие методы, но указанных методов должно хватить для решения большинства задач

В подсистеме линз есть объект Cookies, позволяющий сформировать линзы для куков:

val cookieLens: BiDiLens<Request, Cookie> = Cookies.optional("auth_token")

Аутентификация с сохранением сессии #

Нет строгого стандарта (например RFC) который определял бы процедуру авторизации с помощью формы, каждый разработчик может реализовать логику самостоятельно

Ключевая задача — сформировать токен аутентификации и сохранить его в куки

diagram

Аутентификация по токенам #

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

  • Веб-приложение, обеспечивающее аутентификацию, называется identity provider или authentication service (в случае, если происходит ещё и авторизация)
  • Другие приложения в рамках данной схемы называются service provider

diagram

Аутентификация браузера в провайдере #

Предыдущий процесс можно реализовать в специализированных клиентах, однако при использовании браузера процедура становится более сложной

diagram

Варианты аутентификации по токену #

Существует множество стандартов, среди наиболее популярных: OAuth, OAuth 2.0, OpenID Connect, SAML, WS-Federation

Внутри токена содержится дополнительная информация:

  • кто сгенерировал токен
  • кто может быть получателем токена
  • срок действия
  • набор сведений об аутентифицированной сущности

При получении токена его необходимо проверить на соответствие требованиям приложения

Форматы токенов #

Simple Web Token #

Набор пар имя-значение в формате кодирования HTML form, описывает стандартные ключи Issuer, Audience, ExpiresOn и HMACSHA256. Токен подписывается симметричным ключом

JSON Web Token, JWT #

Содержит три блока, разделённых точками: заголовок, набор полей и подпись. Первые два блока закодированы в JSON-формате и закодированы в base64. Подпись может быть сформирована как симметричными, так и ассиметричными алгоритмами шифрования

Security Assertion Markup Language (SAML) #

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

Стандарты OAuth и OpenID Connect #

Данные стандарты предназначены в первую очередь для решения задачи по предоставлению доступа одного приложения к другому от имени пользователя

Приложению для своей работы могут потребоваться данные из другой системы:

  • Список контактов данного пользователя
  • Список файлов данного пользователя
  • Почтовый адрес данного пользователя

В рамках стандарта OpenID Connect на основе OAuth разработан слой учётных данных, в рамках которого сервер авторизации предоставляет идентификационный токен

Большие технологические компании и государства предоставляет возможности по аутентификации с помощью данных протоколов: Госуслуги, VK, Яндекс, Сбербанк

Реализация аутентификации по паролю #

Для упрощения данной задачи в промышленных приложениях лучше всего делегировать задачи идентификации и аутентификации внешним приложениям с использованием протоколов OpenID Connect и OAuth 2.0

Для решения задачи аутентификации своими силами необходимо:

  • Безопасно сохранять данные для идентификации и аутентификации
  • Формировать средства быстрой проверки аутентификации, токены
  • Сохранять токены в куках HTTP-протокола
  • Реализовать аутентификацию пользователя посредством проверки токена

По возможности стоит ограничить время жизни токена или реализовать систему отзыва токена при подозрении на их компроментацию (реализация кнопки «выйти из всех браузеров»)

Хранение и проверка сессионных токенов #

Для проверки сессионных токенов необходимо либо:

  • доверять данным, пришедшим от пользователя, т.е. отказаться от проверки данных
  • хранить сессионный токен на стороне сервера для сверки
  • применять схемы с шифрованием данных или подписью данных в токене

Хранение сессионных токенов на стороне сервера несёт ряд сложностей:

  • сколько токенов может быть выдано конкретному пользователю?
  • сколько пространства потребуется для хранения всех выданных токенов?
  • сколько времени уйдёт на выявление их корректности?

Последняя схема имеет ряд проблем:

  • надо корректно реализовать криптографические схемы
  • сложно отозвать скомпрометированные токены

JWT-токены #

JSON Web Token — открытый стандарт, который определяет компактный и безопасный способ передачи данных в формате JSON-объекта

  • Безопасность достигается благодаря использованию цифровой подписи
  • Наличие подписи позволяет верифицировать данные внутри подписи
  • Злоумышленнику чрезвычайно трудно сфабриковать подпись
  • Данные JWT-токена передаются открыто, нельзя помещать туда конфиденциальные данные

Структура JWT-токена #

JWT включает в себя три части: заголовок, содержимое и подпись. Элементы отделены друг от друга точками

xxxxx.yyyyy.zzzzz

Элементы токена, заголовок #

Включает в себя указание типа токена (JWT) и алгоритм подписи:

{
    "alg": "HS256",
    "typ": "JWT"
}

Данная информация кодируется с помощью кодировки Base64Url:

ew0KICAgICJhbGciOiAiSFMyNTYiLA0KICAgICJ0eXAiOiAiSldUIg0KfQ

Элементы токена, содержимое #

Основная часть токена содержит ряд утверждений о том, для кого был сформирован токен, обычно данные о пользователе

Зарегистрированные утверждения не являются обязательными, но рекомендуются к использованию: iss (издатель), exp (время истечения), aud (аудитория) и другие

Публичные утверждения могут быть установлены любыми службами, но их следует зарегистрировать в IANA

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

{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}

Данная часть тоже кодируется в Base64Url

Элементы токена, подпись #

Подпись строится из следующих элементов:

  • Берётся Base64-строка заголовка
  • Берётся Base64-строка содержимого
  • Объединяется и выполняется подпись

Например, при использовании алгоритма HMAC SHA256 подпись строится так:

HMACSHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  secret
)

Полученные элементы объединяются через точки для получения JWT-токена:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Операции над токеном #

Для реализации работы с токеном приложению необходимо выполнять операции:

  • Создание нового токена в случае успешной аутентификации пользователя
  • Проверять переданный от пользователя токен
  • Извлекать данные из переданного пользователем токена

Для реализации в приложении можно воспользоваться одной из библиотек

В рамках курса рассмотрим библиотеку java-jwt

  • Данную библиотеку можно использовать для создания коммерческих решений
  • Можно использовать на любой JVM-платформе
  • Поддерживает много стандартов шифрования токена

Библиотека java-jwt #

Создание токена #

try { // Необходимо обеспечить хранение секретной строки в настройках
    Algorithm algorithm = Algorithm.HMAC512(secret);
    String token = JWT.create()
        .withIssuer("ru.example")
        .sign(algorithm);
} catch (exception: JWTCreationException){
    // Неправильная конфигурация или ошибка конвертации утверждений
}

Обработка токена #

val algorithm = Algorithm.RSA256(rsaPublicKey, rsaPrivateKey);
val verifier: JWTVerifier = JWT.require(algorithm)
    .withIssuer("ru.example")
    .build(); // Можно создать единожды для приложения
String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJpc3MiOiJhdXRoMCJ9.AbIJTDMFc7yUa5MhvcP03nJPyCPzZtQcGEp-zWfOkEE";
try {
    val decodedJWT: DecodedJWT = verifier.verify(token);
} catch (JWTVerificationException exception){
    // Неправильная подпись или утверждения
}

Авторизация действий в приложении #

  • Каждый запрос в протоколе HTTP выполняется независимо друг от друга
  • Веб-браузер посылает с каждым запросом данные с помощью механизма куков
  • В куках может храниться JWT-токен с аутентификационными данными
  • При получении запроса приложению необходимо:
    1. Извлечь JWT-токен из целевой куки
    2. Верифицировать подпись JWT-токена
    3. Извлечь идентификационные данные для JWT-токена
    4. На основании идентификационных данных выносится решение о доступе к запросу
  • Шаги № 1-3 являются общими для множества запросов, их следует расположить в фильтре для всех обработчиков, которые
  • Шаг № 4 может быть выполнен на любом уровне обработки запросов

Уровни авторизации действий #

При разработке приложений на библиотеке http4k можно выполнять:

  • На уровне фильтров
  • На уровне маршрутизатора
  • На уровне обработчика запроса
  • Внутри операции над данными

Внутри приложения стоит выбрать не более двух подходов к решению этой задачи

  • Для большинства приложений не является необходимым выполнять проверку авторизации при доступе к статическим данным
  • Не рекомендуется засорять маршрутизатор дополнительными проверками

Авторизация на уровне вида #

Предложенная ранее схема должна быть реализована в приложении: несанкционированный доступ к действиям над данными приложения должен быть невозможен

Однако только реализация данного подхода усложняет работу пользователя:

  • Пользователь видит возможность выполнения действия
  • Пользователь пытается выполнить действие
  • Система проверяет возможность выполнения действия и отказывает пользователю (возможно некрасиво)
  • Пользователь не доволен :(

Рекомендуется адаптировать пользовательский интерфейс приложения: не показывать на страницах элементы, которые пользователь не может вызвать

  • Отдельные интерфейсы для разных групп пользователей
  • Адаптация интерфейса в зависимости от возможностей пользователя

Ролевая модель доступа #

Если мы хотим реализовать проверку прав доступа на всех уровнях приложения, то необходимо описать права в отдельной сущности

Предлагается следующий подход к моделированию прав:

  1. Определите список действий в приложении, доступ к которым должен быть ограничен
    • Просмотр списка зарегистрированных пользователей
    • Добавление и редактирование новости компании
  2. Определите список ролей внутри системы:
    • Менеджер пользователей системы
    • Редакторы сайта
    • Анонимные пользователи
  3. Сформируйте наборы прав для каждой из ролей
  4. Определите: действует ли фактор владения на возможность выполнения действий. Например редактированием сообщения может как редактор сайта, так и автор сообщения

Кодирование ролей #

Самый простой способ кодирования — использование классов данных

data class Permissions(
    val manageUsers: Boolean = false,
    val manageNews: Boolean = false,
) {
    companion object {
        val USER_EDITOR = Permissions(manageUsers = true)
        val NEWS_EDITOR = Permissions(manageNews = true)
        val ANONIMOUS = Permissions()
    }
}

Использование класса позволяет учесть как базовые возможности роли пользователя, так и возможности, связанные с владенеием данными

Извлечение роли в запросе #

Ввиду того, что определение роли необходимо:

  • Выполнять при каждом запросе
  • Проверять на всех уровнях приложения

Удобно реализовать эту логику на уровне фильтра http-запроса:

diagram

  • На этапе обработки JWT-токена можно получить уникальный идентификатор пользователя
  • На этапе определения роли по полученному идентификатору можно определить роль пользователя
  • На уровне обработчика HTTP-запроса можно учесть фактор владения данными

Передача данных из фильтра #

Базовый интерфейс фильтров в http4k передаёт данные внутреннему HTTP-обработчику только в формате объекта Request, передача дополнительных данных через этот интерфейс не предусмотрена

Рассмотрим последний вариант

Работа хранилища контекста запросов #

diagram

Хранилище контекстных данных позволяет удобным образом хранить данные для одного запроса, удовлетворяя требованиям поточной безопасности

Для записи и извлечения данных из хранилища можно воспользоваться линзами

Данные в хранилище типизированы, т.е. не надо выполнять преобразование данных много раз

По окончании обработки запроса хранилище очищается

Особенности реализации #

  • Объект хранилища запросов необходимо создать при запуске приложения
  • Можно определить ряд линз, позволяющих сохранять и считывать данные из контекста
  • Добавить в начало цепочек фильтров фильтр, инициализирующий работу хранилища для запроса

Рассмотрим пример

Линзу для взаимодействия с хранилищем необходимо предоставить соответствующим HTTP-обработчикам

Создание хранилища #

Для обеспечения конкуретного доступа из нескольких потоков нужен специальный объект

val contexts = RequestContexts()

Также выполняем инициализацию на соответствующем потоке:

val router: RoutingHttpHandler = ... // Приложение
val baseApp = // Приложение с фильтрами
    ErrorFilter(htmlView)
    .then(router)
val app = // 
    ServerFilters.InitialiseRequestContext(contexts)
        .then(AddState(contexts))

Использование линз #

В хранилище можно помещать любые объекты:

data class SharedState(val message: String)
val key = RequestContextKey.required<SharedState>(contexts)

Далле линзу можно применить к запросу, чтобы установить:

val initialRequest: Request
val request = request.with(key of SharedState("hello there"))

Или считать значение:

val request: Request
val storedData: SharedState = key(request)

Настроенную линзу необходимо передать в фильтры или обработчики, которым нужен доступ

Доступ к контексту на уровне вида #

На уровне вида приложение может захотеть иметь информацию:

  • Об имени пользователя
  • О статусе авторизации пользователя

Для всех страниц, отображаемых пользователю (для отображения информации в навигационной панели)

На настоящий момент подсистема вида не поддерживает возможности прямого доступа к хранилищу контекста, поэтому предлагается собственный подход к формированию слоя отображения

  1. Создать собственный компонент отображения Pebble-шаблонов
  2. Ассоциировать с данным компонентом линзы для доступа к контексту запроса
  3. Получить данные из контекста для передачи в вид
  4. Использовать данные внутри вида