Jwt токен где хранится
Перейти к содержимому

Jwt токен где хранится

  • автор:

Про токены, JSON Web Tokens (JWT), аутентификацию и авторизацию. Token-Based Authentication

Аутентификация(authentication, от греч. αὐθεντικός [authentikos] – реальный, подлинный; от αὐθέντης [authentes] – автор) — это процесс проверки учётных данных пользователя (логин/пароль). Проверка подлинности пользователя путём сравнения введённого им логина/пароля с данными сохранёнными в базе данных.

Авторизация(authorization — разрешение, уполномочивание) — это проверка прав пользователя на доступ к определенным ресурсам.

Например после аутентификации юзер sasha получает право обращатся и получать от ресурса «super.com/vip» некие данные. Во время обращения юзера sasha к ресурсу vip система авторизации проверит имеет ли право юзер обращатся к этому ресурсу (проще говоря переходить по неким разрешенным ссылкам)

  1. Юзер c емайлом sasha_gmail.com успешно прошел аутентификацию
  2. Сервер посмотрел в БД какая роль у юзера
  3. Сервер сгенерил юзеру токен с указанной ролью
  4. Юзер заходит на некий ресурс используя полученный токен
  5. Сервер смотрит на права(роль) юзера в токене и соотвественно пропускает или отсекает запрос

Собственно п.5 и есть процесс авторизации.

Дабы не путатся с понятиями Authentication/Authorization можно использовать псевдонимы checkPassword/checkAccess(я так сделал в своей API)

JSON Web Token (JWT) — содержит три блока, разделенных точками: заголовок(header), набор полей (payload) и сигнатуру. Первые два блока представлены в JSON-формате и дополнительно закодированы в формат base64. Набор полей содержит произвольные пары имя/значения, притом стандарт JWT определяет несколько зарезервированных имен (iss, aud, exp и другие). Сигнатура может генерироваться при помощи и симметричных алгоритмов шифрования, и асимметричных. Кроме того, существует отдельный стандарт, отписывающий формат зашифрованного JWT-токена.

Пример подписанного JWT токена (после декодирования 1 и 2 блоков):

< alg: "HS256", typ: "JWT" >.< iss: "auth.myservice.com", aud: "myservice.com", exp: 1435937883, userName: "John Smith", userRole: "Admin" >.S9Zs/8/uEGGTVVtLggFTizCsMtwOJnRhjaQ2BMUQhcY 

Токены предоставляют собой средство авторизации для каждого запроса от клиента к серверу. Токены(и соотвественно сигнатура токена) генерируются на сервере основываясь на секретном ключе(который хранится на сервере) и payload’e. Токен в итоге хранится на клиенте и используется при необходимости авторизации како-го либо запроса. Такое решение отлично подходит при разработке SPA.

При попытке хакером подменить данные в header’ре или payload’е, токен cтанет не валидным, поскольку сигнатура не будет соответствовать изначальным значениям. А возможность сгенерировать новую сигнатуру у хакера отсутствует, поскольку секретный ключ для зашифровки лежит на сервере.

access token — используется для авторизации запросов и хранения дополнительной информации о пользователе (аля user_id, user_role или еще что либо, эту информацию также называет payload)

refresh token — выдается сервером по результам успешной аутентификации и используется для получения нового access token’a и обновления refresh token’a

Каждый токен имеет свой срок жизни, например access: 30мин, refresh: 60дней

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

Роль рефреш токенов и зачем их хранить в БД. Рефреш на сервере хранится для учета доступа и инвалидации краденых токенов. Таким образом сервер наверняка знает о клиентах которым стоит доверять(кому позволено авторизоваться). Если не хранить рефреш токен в БД то велика вероятность того что токены будут бесконтрольно гулять по рукам злоумышленников. Для отслеживания которых нам прийдется заводить черный список и периодически чистить его от просроченных. В место этого мы храним лимитированный список белых токенов для каждого юзера отдельно и в случае кражи у нас уже есть механизм противодействия(описано ниже).

Схема создания/использования токенов (api/auth/login):

  1. Пользователь логинится в приложении, передавая логин/пароль на сервер
  2. Сервер проверят подлинность логина/пароля, в случае удачи генерирует и отправляет клиенту два токена(access, refresh) и время смерти access token’а ( expires_in поле, в unix timestamp). Также в payloadrefresh token’a добавляется user_id
"accessToken": ". ", "refreshToken": ". ", "expires_in": 1502305985425 
  1. Клиент сохраняет токены и время смерти access token’а, используя access token для последующей авторизации запросов
  2. Перед каждым запросом клиент предварительно проверяет время жизни access token’а (из expires_in )и если оно истекло использует refresh token чтобы обновить ОБА токена и продолжает использовать новый access token

Схема рефреша токенов (одна сессия/устройство, api/auth/refresh-tokens):

  1. Клиент(фронтенд) проверяет перед запросом не истекло ли время жизни access token’на
  2. Если истекло клиент отправляет на auth/refresh-token URL refresh token
  3. Сервер берет user_id из payload’arefresh token’a по нему ищет в БД запись данного юзера и достает из него refresh token
  4. Сравнивает refresh token клиента с refresh token’ом найденным в БД
  5. Проверяет валидность и срок действия refresh token’а
  6. В случае успеха сервер:
    1. Создает и перезаписывает refresh token в БД
    2. Создает новый access token
    3. Отправляет оба токена и новый expires_in access token’а клиенту

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

    Если рассматривать возможность аутентификации на более чем одном девайсе/браузере(мульти сессии): необходимо хранить весь список валидных рефреш токенов юзера. Если юзер авторизовался более чем на ±10ти устройствах(что есть весьма подозрительно), автоматически инвалидоровать все рефреш токены кроме текущего и отправлять email с security уведомлением. Как вариант список токенов можно хранить в jsonb(если используется PostgreSQL).

    Схема рефреша токенов (мульти сессии/несколько устройств, api/auth/refresh-tokens):

    Для использования возможности аутентификации на более чем одном девайсе необходимо хранить все рефреш токены по каждому юзеру. Я этот список храню в записи юзера в виде JSONB. Во время кажого процесса логина необходимо добавлять IP/Fingerprint пользователя-владельца логина/пароля в белый список.

    ------------------------------------------------------------------------------------------------- | id | username | refreshTokensMap | whitelistIP ------------------------------------------------------------------------------------------------- | 1 | alex | < refreshTokenTimestamp1: 'refreshTokenBody1', refreshTokenTimestamp2: 'refreshTokenBody2'>| ['111.111.111.111', '222.222.222.222'] ------------------------------------------------------------------------------------------------- 
    1. Клиент(фронтенд) проверяет перед запросом не истекло ли время жизни access token’на
    2. Если истекло клиент отправляет на auth/refresh-token URL refresh token
    3. Сервер берет user_id из payload’arefresh token’a по нему ищет в БД запись данного юзера
      1. Проверяет IP юзера запрашиваемого обновление токенов с белым списком, если все успешно достает refresh token из записи в refreshTokensMap
      2. Если IP юзера отсутствует в белом списке, редиректит на страницу логина
      1. Удаляет старый рефреш токен
      2. Проверяет количество уже существующих решфреш токенов.
      3. Если их больше 10, удаляет все токены, создает новый и запиывает его в БД.
      4. Если их меньше 10 просто создает и записывает новый в БД.
      5. Создает новый access token
      6. Отправляет оба токена и новый expires_in access token’а клиенту

      Таким образом если юзер залогинился на пяти устройствах, рефреш токены будут постоянно обновлятся и все счастливы. Но если с аккаунтом юзера начнут производить подозрительные действия(попытаются залогинится более чем на 10ти устройствах) система сбросит все сессии(рефреш токены) кроме последней.

      Как дополнительная мера можно вообще заблокировать данного юзера при попытке залогинится более чем на 10ти устройствах. С возможностью разблокировки только через email. Но в этом случае нам необходимо будет во время каждого рефреша проверять список токенов на наличие мертвых(не валидных).

      Ключевой момент:

      В момент рефреша то есть обновления access token’a обновляются ОБА токена. Но как же refresh token может сам себя обновить, он ведь создается только после успешной аунтефикации ? refresh token в момент рефреша сравнивает себя с тем refresh token’ом который лежит в БД и вслучае успеха, а также если у него не истек срок, система рефрешит токены. Внимание при обновлении refresh token’a продливается также и его срок жизни.

      Возникает вопрос зачем refresh token’y срок жизни, если он обновляется каждый раз при обновлении access token’a ? Это сделано на случай если юзер будет в офлайне более 60 дней, тогда прийдется заново вбить логин/пароль.

      В случае кражи токенов (когда когда юзер логинится только с одного устройства: одна сессия):

      1. Хакер воспользовался access token’ом
      2. Закончилось время жизни access token’на
      3. Клиент хакера отправляет refresh token
      4. Хакер получает новую пару токенов
      5. На сервере создается новая пара токенов(«от хакера»)
      6. Юзер пробует зайти на сервер >> обнаруживается что токены невалидны
      7. Сервер перенаправляет юзера на форму аутентификации
      8. Юзер вводит логин/пароль
      9. Создается новая пара токенов >> пара токенов «от хакера» становится не валидна

      Проблема: Поскольку refresh token продлевает срок своей жизни каждый раз при рефреше токенов >> хакер пользуется токенами до тех пор пока юзер не залогинится.

      В случае кражи токенов (когда когда юзер логинится с нескольких устройства: мульти сессии):

      Во время кажого процесса логина необходимо добавлять IP/Fingerprint пользователя-владельца логина/пароля в белый список. Таким образом при каждой попытке зайти с новой точки доступа придется перелогиниватся.

      1. Хакер воспользовался access token’ом
      2. Закончилось время жизни access token’на
      3. Клиент хакера отправляет refresh token
      4. Сервер смотрит IP адрес хакера
      5. Сервер не находит IP адрес хакера в белом списке и удаляет refresh token из БД (можно так же забанить этот IP)
      6. Сервер логирует попытку несанкционированного обновления токенов
      7. Сервер перенапрявляет харека на станицу логина. Хакер идет лесом
      8. Юзер пробует зайти на сервер >> обнаруживается что refresh token отсутствует
      9. Сервер перенаправляет юзера на форму аутентификации
      10. Юзер вводит логин/пароль

      Пример имплементации:

      Чтиво:

      • Заметка базируется на: https://habrahabr.ru/company/Voximplant/blog/323160/
      • https://tools.ietf.org/html/rfc6749
      • https://www.digitalocean.com/community/tutorials/oauth-2-ru
      • https://jwt.io/introduction/
      • https://auth0.com/blog/using-json-web-tokens-as-api-keys/
      • https://auth0.com/blog/cookies-vs-tokens-definitive-guide/
      • https://auth0.com/blog/ten-things-you-should-know-about-tokens-and-cookies/
      • https://auth0.com/blog/refresh-tokens-what-are-they-and-when-to-use-them/
      • https://habr.com/company/dataart/blog/262817/
      • https://habr.com/post/340146/
      • https://habr.com/company/mailru/blog/115163/
      • https://scotch.io/tutorials/authenticate-a-node-js-api-with-json-web-tokens
      • https://www.youtube.com/watch?v=Ngh3KZcGNaU
      • https://www.youtube.com/playlist?list=PLvTBThJr861y60LQrUGpJNPu3Nt2EeQsP
      • https://egghead.io/courses/json-web-token-jwt-authentication-with-node-js
      • https://www.digitalocean.com/community/tutorials/oauth-2-ru
      • https://github.com/shieldfy/API-Security-Checklist/blob/master/README-ru.md

      And why JWT is bad

      • http://cryto.net/~joepie91/blog/2016/06/13/stop-using-jwt-for-sessions/
      • http://cryto.net/~joepie91/blog/2016/06/19/stop-using-jwt-for-sessions-part-2-why-your-solution-doesnt-work/
      • https://medium.com/@cjainn/anatomy-of-a-jwt-token-part-1-8f7616113c14
      • https://medium.com/@cjainn/anatomy-of-a-jwt-token-part-2-c12888abc1a2
      • https://scotch.io/bar-talk/why-jwts-suck-as-session-tokens
      • https://t.me/why_jwt_is_bad

      Подделка невозможна: как устроен токен и какие задачи можно решить с помощью JWT-авторизации

      Привет! Меня зовут Александр Бричак, я Golang Developer в NIX. В этой статье я расскажу об авторизации с помощью JSON Web Token.

      Експертний курс від mate.academy: Fullstack Web Development.
      Відкрийте світ розробки у свій вільний час.

      Вы узнаете, как устроен токен и какие типичные задачи решаются с помощью такой авторизации. Также я поделюсь best practices использования JWT и немного затрону тему безопасности.

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

      И эти понятия нужно уметь разделять:

      Ефективний курс від skvot: Основи 3D-моделювання в ZBrush.
      Звільніть свою творчість.

      • Идентификация. Это самый первый этап, где юзеру фактически задается вопрос «Кто ты?». В качестве ответа пользователь предоставляет свой логин, email и т.п.
      • Аутентификация. На следующем этапе юзер подтверждает, что он тот, за кого себя выдает. Для этого он вводит пароль, доказывая регистрацию в системе.
      • Авторизация. На финальном этапе система проверяет, что подтвердивший свою личность пользователь действительно имеет право обращаться к ресурсу.

      В статье я буду в основном говорить об аутентификации. Зачастую используя термин «авторизация», говорят об аутентификации, поэтому в этом материале я буду использовать термины попеременно в одном и том же смысле.

      Обратите внимание на курс от Mate Academy. На нем вы получите знания, которые потребуется вам для повседневной работы. А менторы помогут вам закрепить знания на практике и ответить на все ваши вопросы.

      Аутентификация с помощью сессий

      На иллюстрации ниже изображена традиционная схема аутентификации с помощью сессий. Представим фронтенд-приложение в браузере и бэкенд на сервере. Когда пользователь для входа в систему вводит во фронтенде свои креды, логин и пароль, на бэкенде сервер проверяет их. Если все в порядке, он подтверждает аутентификацию юзера возвращением ответа «200» и добавляет cookies с идентификатором сессии — Session ID. После этого стартует сессия пользователя, и он может работать с системой.

      Когда юзер выполняет новые запросы, браузер отправляет их на сервер и автоматически снабжает каждый из них той самой Session ID. Задачи сервера на данном этапе: вытащить из запроса эти куки, распарсить и проверить их правильность. Если все окей, то пользовательская сессия валидна — и можно отдать юзеру запрошенный контент.

      JWT-авторизация

      Аутентификация с помощью сессий

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

      По этой причине появилось такое понятие, как Session storage — это место на сервере для хранения информации, привязанной к Session ID.

      Ефективний курс від robotdreams: Blockchain-розробник.
      Революційні рішення в технологіях.

      Практически все современные фреймворки предлагают широкий выбор вариантов для Session storage:

      • файловое хранилище сервера;
      • основная или дополнительная база данных;
      • key-value-хранилище (например, Redis).

      JWT-авторизация

      Описанная схема прекрасно работает в пределах одного домена, но вызывает определенные проблемы при архитектуре с микросервисами.

      Это демонстрирует изображенная ниже схема, на которой представлены два сервиса Protected Endpoint — они защищены и должны предоставлять пользователям ресурсы по их запросам. Также в системе есть Authentication Server — этот сервер отвечает за изначальный логин пользователей и проверку валидности Session ID, которую юзер передает со своим запросом.

      JWT-авторизация

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

      Можно поставить сервер аутентификации перед Protected Endpoint — то есть объявить его API Gateway. Другой вариант — разрешить защищенным эндпоинтам напрямую соединяться с Session storage. Но проверка Session ID все равно останется так называемым bottleneck «Узкое место» — это процесс, ограниченная пропускная способность которого снижает пропускную способность всей цепочки процессов. .

      Експертний курс від laba: Стратегічний маркетинг.
      Розвивайте бізнес з глибоким пониманням ринку.

      Более того, при установке cookie можно указать, к какому домену она относится. Тогда браузер во время обращения к другим доменам не будет подставлять ее в запрос. Но если защищенные эндпоинты микросервисов будут находиться на разных доменах, то будет сложно устанавливать cookie с Session ID. Кроме того, есть вероятность CSRF-атак, ведь они как раз нацелены на cookies с таким содержанием, которые предоставляют доступ юзерам к защищенным ресурсам.

      JSON Web Tokens

      Учитывая указанные проблемы, в сообществе разработчиков много лет назад появилась идея использовать некую строку, которую с одной стороны невозможно подделать, с другой — которую каждый ресурсный сервер мог бы сам проверить на валидность. В качестве такой строки оказалось удобно использовать JWT — JSON Web Token.

      JWT представляет собой строку из трех частей, разделенных точками. На иллюстрации ниже приведены значения каждой части:

        Первая — это header, заголовок токена. В нем указан алгоритм и тип.

      Значения каждой из частей в JWT

      Значения каждой из частей в JWT

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

      Но никто не может прочитать записанное в третьей части — и соответственно никто не может подделать токен.

      Если злоумышленник, например, укажет во второй части другое имя пользователя, то изменится и подпись в третьей части. Но поскольку у злоумышленника нет секретного ключа, то он не сможет правильно подписать токен. JWT попросту не будет валидным. Благодаря этому любой сервер, имеющий секретную строку для подписания токена, сможет проверить его валидность и вытащить информацию из второй части payload.

      Перейдем к следующей схеме на иллюстрации ниже. Схема похожа на аутентификацию с помощью сессий, но есть одно отличие. После введения кредов сервер дает юзеру не cookies с Session ID, а токен. Затем клиентское приложение добавляет токен к каждому запросу в виде специального заголовка Authorization. При этом оно вписывает в него слово «Bearer» (предъявитель), а после пробела — сам токен. Также на схеме представлен Auth middleware. Это достаточно стандартная часть любого фреймворка, которая умеет проверять, в частности, валидность токена. То есть обработка на сервере проходит этап проверки.

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

      JWT-авторизация

      В контексте обсуждения JWT обязательно стоит упомянуть протокол OAuth 2.0, который построен на основе данной структуры (см. схему ниже). Он позволяет пользователям авторизоваться в приложениях и получать токен для обращения к ресурсным серверам, используя, например, Google- или Facebook-приложения.

      В таком случае идентификация и аутентификация перекладывается на сторонних провайдеров:

      JWT-авторизация

      Отдельно стоит обратить внимание на access- и refresh-токены. Как правило, access-токен — это некий короткоживущий токен, который много раз используется приложением для обращения к серверу. Когда срок жизни токена истекает, можно обратиться к серверу для продления срока. Для этого и понадобится refresh-токен. Он одноразовый, но имеет более долгий срок жизни и позволяет получить новую пару токенов. Схема их использования приведена на этой схеме:

      JWT-авторизация

      Благодаря такому решению появляется возможность обезопасить юзера. Если в незащищенном http-соединении злоумышленник украдет access-токен, он не сможет им долго пользоваться. Через определенное время access-токен утратит валидность. Настоящий пользователь со своим refresh-токеном сможет получить новую пару, а вот злоумышленник — нет.

      Проблемы использования токенов

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

      Но отсюда следует проблема: если выдать пользователю новую пару токенов, а старая пара еще валидна, то средств ее заэкспайрить нет.

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

      Из этого вытекает следующая сложность — невозможно провести полноценный логаут. Во фронтенд-приложении после нажатия кнопки выхода пользователь уверен, что он вылогинился из системы. Но выданные ему токены проживут еще некоторое время! По этой же причине невозможно вылогинить юзера в случае утери или кражи токена.

      А еще есть проблемы при краже секретной строки. Да, это происходит не часто, потому что этот ключ лежит на сервере. Но если строка украдена, то все токены будут скомпрометированными.

      Есть проблемы с хранением токенов на фронтенде — нередко их помещают прямо в local storage. Так удобнее для разработчика, но это требует дополнительных средств для повышения секьюрности системы.

      Различные типовые задачи с использованием JWT

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

      Более интересные задачи — Logout и Only one active device, которые в принципе связаны между собой.

      Logout предполагает, что после нажатия кнопки логаута во фронтенд-приложении надо с подтверждением выхода еще и инвалидировать сами токены, сделать их недействительными.

      Что касается Only one active device , то эта задача позволяет юзеру быть одновременно залогиненным только на одном устройстве. Если он войдет в аккаунт с другого устройства, то выданные первому устройству токены должны потерять свою валидность. Подробнее о реализации обоих механизмов и их связи я расскажу далее.

      Еще одна интересная задача — с автоматическим логаутом. Она встречается в приложениях достаточно часто, а для некоторых типов приложений (например, дающих доступ к сенситивной или финансовой информации) даже является обязательной.. Суть такая: если залогиненый в приложение пользователь неактивен в течение некоторого заданного времени, то он должен быть автоматически вылогинен из системы без возможности ее использовать заново, пока не будут повторно введены креды.

      Logout и Only one active device

      Давайте подробнее рассмотрим задачи авторизации Logout и Only one active device. Сами по себе токены задуманы как stateless, то есть они не предназначены для размещения на сервере. А идея заключается в том, чтобы предусмотреть возможность хранения какой-то информации о них на сервере. Способы могут быть разными. Например, можно создать blacklist для токенов, которые должны стать невалидными.

      Такие токены будут храниться в «черном списке», пока не истечет срок их использования. С другой стороны, можно создать на сервере whitelist, что может оказаться даже несколько проще. Этот список станет реестром выданных токенов, с которым может сверяться система. Если предъявленный юзером токен не просрочен и указан в «белом списке», то он валиден.

      Хранение самих токенов в базе данных сложно назвать безопасным. Можно ли как-то выйти из этой ситуации? Да. Обратите внимание на иллюстрацию ниже: в разделе payload добавился пункт hash. Это некая рандомно сформированная строка. Она записывается в payload токена и в хранилище на сервере с привязкой к идентификатору пользователя.

      Когда юзер предъявляет токен, система парсит его, проверяет срок действия токена и его подпись секретной строкой. Затем вытаскивает hash и сверяет его с пользовательским ID в хранилище. Это может быть key-value хранилище, основная база данных или другое хранилище, которое предоставит современный фреймворк.

      JWT-авторизация

      За счет использования поля hash схема взаимодействия клиента и сервера становится такой (см. следующую картинку). Сначала браузер отправляет на сервер креды. Сервер проверяет их и при корректно введенных данных записывает в Token (hash) storage связку из User ID и hash для access- и refresh-токенов, а после этого — отдает их. Далее пользовательский браузер предъявляет access-токен в API. Для проверки аутентификации сервис снова обращается к базе данных, находит связку hash с User ID и, если все в порядке, возвращает ответ с запрошенной информацией.

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

      JWT-авторизация

      Как это связано с Only one active device? Если в базе данных разрешено хранить по каждому User ID несколько хешей для нескольких access- или refresh-токенов, то это позволяет пользователю логиниться одновременно с разных девайсов. Но если мы разрешим хранить в базе данных только один хэш для каждого User ID , то это и будет реализация Only one active device. Как только юзер логинится с другого девайса, старая связка перезаписывается. Из-за этого предыдущий девайс больше не сможет обращаться к серверу.

      Automatic logout

      Немало вопросов на практике возникает с automatic logout. Может показаться, что задачу можно решить путем манипуляции сроков действия access- и refresh-токенов. То есть после обращения пользователя к бэкенду система выдает ему эти токены со сроком действия, который нужен четко для автоматического логаута. И тогда если токен просрочен, то логаут состоится. Ниже на иллюстрации приведена именно такая схема:

      JWT-авторизация

      Но есть проблема. Допустим, надо автоматически вылогинивать пользователя в случае неактивности в течение 10 минут. Под неактивностью следует понимать, что к серверу никто не обращался.

      Представим, что в нулевой момент времени юзер выполнил вход, ему был выдан access-токен на 10 минут, зато refresh-токен более «долгий», как это и задумано в архитектуре. Здесь может возникнуть следующая ситуация: если до истечения 10 минут пользователь обратится к серверу, то access-токен еще действует, и юзер получает ресурсы. Но если он отправит запрос через 11 минут, access-токен будет недействителен. Вроде бы должен произойти автоматический логаут, но refresh-токен еще остается действительным. В итоге пользователь может обратиться на эндпоинт и получить новую пару токенов. Поэтому такая схема не подходит.

      На схеме ниже изображен альтернативный вариант — когда у access- и refresh-токенов одинаковый срок действия. Если в нулевой момент времени пользователь получил пару токенов и на девятой минуте законнектился к API, то эта пара токенов еще действительна. И это, по идее, должно бы обнулить счетчик и позволить юзеру обращаться к бэкенду на 19-й минут (9 истекших плюс 10 новых). Но когда клиент запросит ресурсы на 11-й минуте, его пара токенов окажется просроченной.

      Поэтому придется повторно вводить креды, а значит, задача так и не решена. Что делать?

      JWT-авторизация

      Как вариант решения, если задан достаточно длинный срок автоматического логаута (допустим, два часа), то можно оставить два часа как срок действия refresh-токена, а для access-токена этот параметр ограничить в минуту. Тогда в случае неактивности в течение «2 часов + 1 минуты» пользователя будет вылогинивать из системы. Но слишком часто менять access-токен тоже неправильно, как и менять access- и refresh-токены после каждого обращения к защищенному эндпоинту.

      Решим эту задачу с помощью механизма автоматической экспирации записей, который реализован в key-value базе Redis. В таком случае производится манипуляция не сроком службы токенов, а сроком хранения пары токенов в базе данных. Этот параметр обозначают как TTL. На иллюстрации ниже показана такая схема.

      JWT-авторизация

      Допустим, что автоматический логаут должен происходить после 10 минут неактивности пользователя, срок действия access-токена составляет 20 минут, а refresh-токена — 60 минут:

      1. В нулевой момент времени пользователь проходит процедуру аутентификации и получает пару токенов. В это же время токены записываются в Redis с TTL, 10 минут.
      2. На втором этапе показано пользователь обращается к API через 9 минут. Поскольку срок действия автоматического логаута не наступил, юзер получает запрошенные ресурсы, но TTL в Redis при этом сдвигается на 10 минут.
      3. Третий этап на схеме: пользователь делает запрос на 19-й минуте, у него еще валидный access-токен, и он по-прежнему получает ресурсы.
      4. На четвертом этапе, когда пользователь обратится после 20-й минуты с просроченным access- и действующим refresh-токеном, он получит новую пару токенов. Они так же записываются в базу Redis c TTL в 10 минут.
      5. На пятом этапе показано, как пользователь обращается к API через 15 минут. В данном случае срок автоматического логаута истек. И когда Middleware авторизации пойдет проверять запись с токенами в Redis, то ничего не обнаружит — данные в Redis заэкспайрились и автоматически удалились. Все работает так, как нужно.

      Сравнение сессий и JWT

      Наверняка кто-то скажет: «Ура, вы переоткрыли механизм сессий», и в этом есть доля правды. Токен нужен для подтверждения аутентификации пользователя. А идентификатор сесии — это признак пользовательской сессии: находится ли сейчас юзер в системе, может ли он работать с ней, или сессия завершена.

      В интернете вам может попасться такая немного саркастическая картинка:

      JWT-авторизация

      Здесь показано, как попытки использования JWT в качестве идентификатора сессии приводят к тем же проблемам, что и с сессиями. Как только пользователь хочет сделать логаут, нужно инвалидировать токен и поменять секретную строку. Но при смене этого ключа будут разлогинены все пользователи. Если же изменять секретную строку только этого пользователя, то нужно хранилище для таких ключей. В итоге все приходит фактически к воспроизведению механизма сессий. Ведь изначально JWT не требовал хранилищ, а получилось, что для решения определенных задач без них не обойтись.

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

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

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

      Best practices в JWT

      И в завершении приведу best practices при использовании этого вида токенов, которых следует придерживаться:

      • Обеспечение безопасности соединения. Протокол HTTPS позволяет, например, предотвратить атаки Man-in-the-Middle, когда злоумышленник может подсмотреть передаваемые в заголовке токены.
      • Хранение строк в защищенном месте на сервере. Это вроде бы очевидно, но иногда такие строки могут храниться в неподходящих для этого местах (например, в директории на сервере с открытым публичным доступом).
      • Не хранить токены в БД . Как я уже отметил, в БД можно хранить идентификатор, который содержится и в токене. По идентификатору можно проверить токен.
      • Работа с парой токенов . Повторяться про короткоживущий access- и долгоживущий refresh-токены не буду, ограничусь просто напоминанием о них.
      • Не хранить токены в local storage или session storage браузера. Такой способ хранения чувствителен к XSS-атакам. Любой код, который злоумышленник ухитрился выполнить на странице других пользователей, может получить доступ к local storage. Вы можете поместить токены в такое хранилище, но тогда нельзя забывать о защите системы от XSS-атак.
      • Всегда валидировать данные пользователя. Сюда добавлю и экранирование спецсимволов при выводе данных на страницу. Такие механизмы сделают невозможным запуск на нашем сервере чужого кода.
      • Ограничивать источники подгружаемых ресурсов. Для этого понадобится заголовок Content-Security-Policy. Если выполняется должен только загруженный с сервера JavaScript-код, то надо написать self в качестве Content-Security-Policy. Тогда JavaScript-код из других источников не сможет быть запущен на странице.
      • Флаг HttpOnly для cookie и ограничение Same Site. Для cookie встречаются такие реализации access- и refresh-токенов, где первый передается в теле ответа, а второй — в cookie. Таким образом можно избавиться от необходимости его хранения в local storage. Правда, надо защититься от CSRF-атак. В этом поможет использование для cookie флага HttpOnly и ограничение SameSite, которое сейчас активно вводится браузерами.
      • CSRF-токены. Хороший механизм защиты от CSRF-атак — использование CSRF-токенов — специальных токенов, которые сервер присылает клиенту в cookie, при этом каждый запрос клиента к серверу должен сопровождаться cookie с этим токеном и/или HTTP-заголовком X-CSRF-Token, содержащим этот токен.

      Использование JWT — хороший инструмент с достаточно интересным кругом решаемых задач. Но в этой теме нет единственно правильных и абсолютно однозначных решений. К вопросу безопасности систем всегда нужно подходить комплексно и до мелочей прорабатывать разные варианты. Удачи!

      Если вы нашли ошибку, пожалуйста, выделите фрагмент текста и нажмите Ctrl+Enter.

      О хранении JWT токенов в браузерах

      Открытый стандарт JWT официально появился в 2015 (rfc7519) обещая интересные особенности и широкие перспективы. Правильное хранение Access токена является жизненно важным вопросов при построении системы авторизации и аутентификации в современном Web, где становятся все популярнее сайты, построенные по технологии SPA.

      Неправильное хранение токенов ведет к их краже и переиспользованию злоумышленниками.

      Так и где хранить?

      Рассмотрим основные варианты хранения JWT Access токена в браузере:

      1. Local Storage/Session Storage – метод небезопасный и подвержен атакам типа XSS, особенно если Вы подключаете скрипты из сторонних CDN (добавление integrity атрибута не может гарантировать 100% безопасность), либо не уверены что подключаемые Вами скрипты не имеют возможности «слить» данные из хранилищ на сторону. Более того если Local Storage доступен между табами то Session Storage доступен только в одной вкладке и открытие сайта в новой вкладке лишь вызовет новый раунд авторизации/рефреша Access токена.
      2. Хранение токена в локальной переменной внутри замыкания тоже не обеспечивает должной безопасности потому что атакующий может, например, проксировать функцию fetch и отправить токен на левый сайт. Также это не решает проблему двух вкладок – нет безопасного способа передать токен из одной вкладки в другую.
      3. Cookies. Вот мы вернулись к старым «печенькам» которые использовались для хранения cookie sessions. Простое хранения Access токена в cookie чревато атакой CSRF. Более того оно не защищает от XSS атак. Для защиты от CSRF нужно ставить параметр Cookie SameSite в режим Strict– этим можно добиться того что все запросы, которые идут с других сайтов, не будут содержать Ваши credentials, что автоматически лишит атакующего возможности произвести CSRF атаку.
        В отличии от первых двух вариантов здесь есть и плюс – Access токен невозможно получить через JS если использовать флаг httpOnly, добавление Secure также усилит защиту от сниффинга.

      Что в итоге?

      Cookies при правильном использовании являются адекватным и наиболее безопасным на данный момент решением для хранения JWT Access токена и должны следовать следующим правилам:

      1. Быть установленными для API домена/пути чтобы избежать оверхеда при запросах к статичным файлам (публичным картинкам/стилям/js файлам).
      2. Иметь флаг Secure (для передачи только по https).
      3. Иметь флаг httpOnly (для невозможности получения доступа из JavaScript).
      4. Атрибут SameSite должен быть Strict для защиты от CSRF аттак, запретит передачу Cookie файлов если переход к вашему API был не с установленого в Cookie домена.
      1. Content-Security-Policy – ограничение доверенных доменов для предотвращения возможных XSS атак
      2. Заголовок X-Frame-Options для защиты от атак типа clickjacking.
      3. X-XSS-Protection – принудительно включить встроенный механизм защиты браузера от XSS атак.
      4. X-Content-Type-Options – для защиты от подмены MIME типов.

      Ограничения

      Не смотря на то что атрибут SameSite поддерживается во многих популярных браузерах , существуют также браузеры которые не поддерживают его или поддерживают частично (привет IE и Safari для мака). Для этих случаев нужен fallback к CSRF токенам. В этом случае вместе с запросами к API надо передавать и CSRF токен. Правильный CSRF токен должен генерироваться сервером с учетом Fingerprint’a пользователя дабы минимизировать вероятность его подмены.

      • Информационная безопасность
      • Веб-разработка
      • JavaScript
      • API

      JWT-авторизация на сервере — Веб-разработка на Go

      Представим, что мы разрабатываем страницу с профилем пользователя в социальной сети. У пользователей есть возможность редактировать свои профили. Нам нужно определять, что HTTP-запрос на изменение профиля пришел от владельца страницы. Если не сделать такую проверку, то злоумышленники смогут изменять данные профилей других пользователей.

      В этом уроке мы разберем тему JWT-авторизации и ее реализацию в Go. Это важная тема, потому что авторизация — это ключевая часть безопасности любого веб-приложения.

      Аутентификация и авторизация

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

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

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

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

      Два самых популярных способа авторизации:

      • С помощью сессий
      • С помощью токенов

      У каждого из этих способов есть свои уникальности, преимущества и недостатки. Ознакомиться более детально с ними можно в дополнительных материалах к уроку. Мы же рассмотрим на практике одну из реализаций авторизации с помощью токенов — JWT.

      JWT-авторизация

      JWT (JSON Web Token) — это специальный формат токена, который позволяет безопасно передавать данные между клиентом и сервером. Например, клиентом может быть веб-браузер или мобильное приложение, сервером — сервер с Go веб-приложением.

      JWT-токен состоит из трех частей, которые разделены точкой:

      • Header или заголовок — информация о токене, тип токена и алгоритм шифрования
      • Payload или полезные данные — данные, которые мы хотим передать в токене. Например, имя пользователя, его роль, истекает ли токен. Эти данные представлены в виде JSON-объекта
      • Signature или подпись — подпись токена, которая позволяет проверить, что токен не был изменен

      Обычный токен имеет формат:

      Рассмотрим пример реального токена с разбором каждой части. Предположим, наше веб-приложение сгенерировало следующий JWT-токен:
      Заголовок обычно состоит из JSON-объекта с двумя свойствами:
      • Тип токена, который в нашем случае — JWT
      • Алгоритм шифрования, который в нашем случае — HMAC SHA256

      Далее этот JSON-объект хэшируется с помощью Base64Url-кодирования, чтобы представить его в виде компактной строки.

      Таким образом, в нашем примере заголовок JWT-токена имеет следующее значение:

       "alg": "HS256", "typ": "JWT" > 

      Payload

      Вторая часть токена — это полезная нагрузка в виде JSON-объекта. Она содержит различные данные об авторизованном пользователе. Значение этой части JWT-токена различно в каждом веб-приложении. Мы можем записать здесь любые публичные данные, которые могут быть полезны при авторизации.

      Как и заголовок JWT-токена, полезная нагрузка хэшируется с помощью Base64Url-кодирования для представления в виде компактной строки.

      В нашем примере полезная нагрузка JWT-токена имеет следующее значение:

       "sub": "1234567890", "name": "John Doe", "iat": 1516239022 > 

      Названия некоторых полей могут показаться непонятными с первого взгляда. Например, поле sub означает идентификатор пользователя, а поле iat — время создания токена. При составлении полей полезной нагрузки рекомендуется учитывать имена из документации IANA (Internet Assigned Numbers Authority) . Это поможет избежать конфликтов имен с общепринятыми нормами. Поэтому мы использовали название sub, вместо привычного user_id.

      Основная причина, почему названия полей в полезной нагрузке JWT-токена пишутся сокращенно, — это уменьшение размера токена после шифрования.

      Signature

      Чтобы создать подпись, мы должны взять закодированный заголовок, закодированную полезную нагрузку, секретную строку и зашифровать эти данные. При этом нужно использовать алгоритм шифрования из заголовка JWT-токена.

      В нашем примере для создания подписи используется алгоритм шифрования HMAC SHA256 и секретная строка «your-256-bit-secret»:

      ( base64UrlEncode(header) + "." + base64UrlEncode(payload), your-256-bit-secret ) 

      Подпись используются, чтобы проверить, что сообщение не было изменено при передаче. Она также позволяет подтвердить, что отправитель JWT-токена является тем, кем он представляется.

      Собираем все части JWT-токена

      В результате генерации JWT-токена получаются три Base64-URL-закодированные строки, которые разделены точкой. Значение JWT-токена является компактным и легко передается в HTTP-запросах.

      Если вы хотите попрактиковаться с JWT, можно использовать онлайн инструмент jwt.io Debugger для декодирования, проверки и генерации JWT-токенов.

      Мы разобрали, из чего состоит JWT-токен. Теперь рассмотрим алгоритм работы с JWT-токеном в веб-приложениях.

      Алгоритм работы с JWT-токеном

      Процесс аутентификации и авторизации с JWT-токеном между веб-браузером и веб-приложением выглядит следующим образом:

      1. Веб-браузер отправляет запрос веб-приложению с логином и паролем
      2. Веб-приложение проверяет логин и пароль, и если они верны, то генерирует JWT-токен и отправляет его веб-браузеру. При генерации JWT-токена веб-приложение ставит подпись секретным ключом, который хранится только в веб-приложении
      3. Веб-браузер сохраняет JWT-токен и отправляет его вместе с каждым запросом в веб-приложение
      4. Веб-приложение проверяет JWT-токен и если он верный, то выполняет действие от имени авторизованного пользователя

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

      Подпись токена происходит с помощью шифрования. С помощью подписи веб-приложение проверяет, что токен действительно был сгенерирован им. Шифрование может осуществляться различными алгоритмами. Например, алгоритмом HS256 — HMAC с SHA-256.

      Мы рассмотрели основы JWT-авторизации и поняли, что веб-приложение должно генерировать JWT-токены и подписывать их секретным ключом, который хранится только в веб-приложении. Рассмотрим, как это можно реализовать в Go-приложении.

      JWT-авторизация в Go веб-приложении

      Реализуем аутентификацию и авторизацию в социальной сети, которая написана на Go с использованием микрофреймворка Fiber. Для этого реализуем следующие функции:

      • Регистрация пользователя
      • Вход в аккаунт — аутентификация
      • Получение информации о своем аккаунте — только для авторизованных пользователей

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

      Регистрация аккаунта

      Начнем разработку социальной сети с функции регистрации аккаунта. Когда пользователь заходит на веб-сайт, он видит форму регистрации с тремя полями: имя, электронная почта и пароль. После заполнения формы пользователь нажимает кнопку «Зарегистрироваться», и веб-браузер отправляет HTTP-запрос POST /register в наше веб-приложение. Мы реализуем обработчик этого HTTP-запроса следующим образом:

      package main import ( "errors" "fmt" "github.com/gofiber/fiber/v2" "github.com/sirupsen/logrus" ) func main()  app := fiber.New() authHandler := &AuthHandler&AuthStoragemap[string]User<>>> app.Post("/register", authHandler.Register) logrus.Fatal(app.Listen(":80")) > type ( // Обработчик HTTP-запросов на регистрацию и аутентификацию пользователей AuthHandler struct  storage *AuthStorage > // Хранилище зарегистрированных пользователей // Данные хранятся в оперативной памяти AuthStorage struct  users map[string]User > // Структура данных с информацией о пользователе User struct  Email string Name string password string > ) // Структура HTTP-запроса на регистрацию пользователя type RegisterRequest struct  Email string `json:"email"` Name string `json:"name"` Password string `json:"password"` > // Обработчик HTTP-запросов на регистрацию пользователя func (h *AuthHandler) Register(c *fiber.Ctx) error  regReq := RegisterRequest<> if err := c.BodyParser(&regReq); err != nil  return fmt.Errorf("body parser: %w", err) > // Проверяем, что пользователь с таким email еще не зарегистрирован if _, exists := h.storage.users[regReq.Email]; exists  return errors.New("the user already exists") > // Сохраняем в память нового зарегистрированного пользователя h.storage.users[regReq.Email] = User Email: regReq.Email, Name: regReq.Name, password: regReq.Password, > return c.SendStatus(fiber.StatusCreated) > 

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

      После регистрации пользователь может войти в свой аккаунт, и далее мы реализуем эту возможность.

      Вход в аккаунт

      Когда пользователь заходит на страницу входа в аккаунт, он видит форму с двумя полями: электронная почта и пароль. Эти поля являются учетными данными пользователя. После заполнения формы пользователь нажимает кнопку «Войти», и веб-браузер отправляет HTTP-запрос POST /login в наше веб-приложение. Обработчик этого запроса будет выглядеть следующим образом:

      // Структура HTTP-запроса на вход в аккаунт type LoginRequest struct  Email string `json:"email"` Password string `json:"password"` > // Структура HTTP-ответа на вход в аккаунт // В ответе содержится JWT-токен авторизованного пользователя type LoginResponse struct  AccessToken string `json:"access_token"` > var ( errBadCredentials = errors.New("email or password is incorrect") ) // Секретный ключ для подписи JWT-токена // Необходимо хранить в безопасном месте var jwtSecretKey = []byte("very-secret-key") // Обработчик HTTP-запросов на вход в аккаунт func (h *AuthHandler) Login(c *fiber.Ctx) error  regReq := LoginRequest<> if err := c.BodyParser(&regReq); err != nil  return fmt.Errorf("body parser: %w", err) > // Ищем пользователя в памяти приложения по электронной почте user, exists := h.storage.users[regReq.Email] // Если пользователь не найден, возвращаем ошибку if !exists  return errBadCredentials > // Если пользователь найден, но у него другой пароль, возвращаем ошибку if user.password != regReq.Password  return errBadCredentials > // Генерируем JWT-токен для пользователя, // который он будет использовать в будущих HTTP-запросах // Генерируем полезные данные, которые будут храниться в токене payload := jwt.MapClaims "sub": user.Email, "exp": time.Now().Add(time.Hour * 72).Unix(), > // Создаем новый JWT-токен и подписываем его по алгоритму HS256 token := jwt.NewWithClaims(jwt.SigningMethodHS256, payload) t, err := token.SignedString(jwtSecretKey) if err != nil  logrus.WithError(err).Error("JWT token signing") return c.SendStatus(fiber.StatusInternalServerError) > return c.JSON(LoginResponseAccessToken: t>) > 

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

      Чтобы сгенерировать JWT-токен, мы используем библиотеку jwt-go . Благодаря этому все тонкости формирования и шифрования токена скрыты от нас. Все, что от нас требуется, — это указать секретный ключ для подписи токена и полезные данные, которые будут храниться в токене.

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

      Теперь в нашей социальной сети пользователи могут регистрироваться и аутентифицироваться — входить в свои аккаунты. Далее реализуем функцию получения информации о своем аккаунте, которая будет доступна только авторизованным пользователям.

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

      Когда пользователь прошел аутентификацию, он получил JWT-токен, который будет использовать в последующих HTTP-запросов для авторизации. Есть разные способы передавать токен в HTTP-запросе. Для этого можно использовать заголовок Authorization, параметр запроса или даже куки веб-браузера. Мы будем использовать заголовок Authorization, так как это наиболее распространенный способ передачи JWT-токена в HTTP-запросе.

      Когда пользователь заходит на свою страницу, веб-браузер отправляет HTTP-запрос GET /profile в наше веб-приложение. Обработчик этого запроса выглядит следующим образом:

      package main import ( . jwtware "github.com/gofiber/contrib/jwt" jwt "github.com/golang-jwt/jwt/v5" . ) const ( contextKeyUser = "user" ) func main()  app := fiber.New() . // Группа обработчиков, которые требуют авторизации authorizedGroup := app.Group("") authorizedGroup.Use(jwtware.New(jwtware.Config SigningKey: jwtware.SigningKey Key: jwtSecretKey, >, ContextKey: contextKeyUser, >)) authorizedGroup.Get("/profile", userHandler.Profile) logrus.Fatal(app.Listen(":80")) > // Структура HTTP-ответа с информацией о пользователе type ProfileResponse struct  Email string `json:"email"` Name string `json:"name"` > func jwtPayloadFromRequest(c *fiber.Ctx) (jwt.MapClaims, bool)  jwtToken, ok := c.Context().Value(contextKeyUser).(*jwt.Token) if !ok  logrus.WithFields(logrus.Fields "jwt_token_context_value": c.Context().Value(contextKeyUser), >).Error("wrong type of JWT token in context") return nil, false > payload, ok := jwtToken.Claims.(jwt.MapClaims) if !ok  logrus.WithFields(logrus.Fields "jwt_token_claims": jwtToken.Claims, >).Error("wrong type of JWT token claims") return nil, false > return payload, true > // Обработчик HTTP-запросов на получение информации о пользователе func (h *UserHandler) Profile(c *fiber.Ctx) error  jwtPayload, ok := jwtPayloadFromRequest(c) if !ok  return c.SendStatus(fiber.StatusUnauthorized) > userInfo, ok := h.storage.users[jwtPayload["sub"].(string)] if !ok  return errors.New("user not found") > return c.JSON(ProfileResponse Email: userInfo.Email, Name: userInfo.Name, >) > 

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

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

      • SigningKey — секретный ключ JWT-токена
      • ContextKey — название поля, по которому хранится объект JWT-токена авторизованного пользователя. Этот объект можно использовать в любом обработчике группы authorizedGroup

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

      Мы реализовали все части аутентификации и авторизации нашей социальной сети. Теперь соберем все вместе и проверим, что веб-приложение работает.

      Проверяем веб-приложение

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

      package main import ( "errors" "fmt" "github.com/gofiber/fiber/v2" jwtware "github.com/gofiber/contrib/jwt" jwt "github.com/golang-jwt/jwt/v5" "github.com/sirupsen/logrus" "time" ) const ( contextKeyUser = "user" ) func main()  app := fiber.New() authStorage := &AuthStoragemap[string]User<>> authHandler := &AuthHandlerstorage: authStorage> userHandler := &UserHandlerstorage: authStorage> // Группа обработчиков, которые доступны неавторизованным пользователям publicGroup := app.Group("") publicGroup.Post("/register", authHandler.Register) publicGroup.Post("/login", authHandler.Login) // Группа обработчиков, которые требуют авторизации authorizedGroup := app.Group("") authorizedGroup.Use(jwtware.New(jwtware.Config SigningKey: jwtware.SigningKey Key: jwtSecretKey, >, ContextKey: contextKeyUser, >)) authorizedGroup.Get("/profile", userHandler.Profile) logrus.Fatal(app.Listen(":80")) > type ( // Обработчик HTTP-запросов на регистрацию и аутентификацию пользователей AuthHandler struct  storage *AuthStorage > // Хранилище зарегистрированных пользователей // Данные хранятся в оперативной памяти AuthStorage struct  users map[string]User > // Структура данных с информацией о пользователе User struct  Email string Name string password string > ) // Структура HTTP-запроса на регистрацию пользователя type RegisterRequest struct  Email string `json:"email"` Name string `json:"name"` Password string `json:"password"` > // Обработчик HTTP-запросов на регистрацию пользователя func (h *AuthHandler) Register(c *fiber.Ctx) error  regReq := RegisterRequest<> if err := c.BodyParser(&regReq); err != nil  return fmt.Errorf("body parser: %w", err) > // Проверяем, что пользователь с таким email еще не зарегистрирован if _, exists := h.storage.users[regReq.Email]; exists  return errors.New("the user already exists") > // Сохраняем в память нового зарегистрированного пользователя h.storage.users[regReq.Email] = User Email: regReq.Email, Name: regReq.Name, password: regReq.Password, > return c.SendStatus(fiber.StatusCreated) > // Структура HTTP-запроса на вход в аккаунт type LoginRequest struct  Email string `json:"email"` Password string `json:"password"` > // Структура HTTP-ответа на вход в аккаунт // В ответе содержится JWT-токен авторизованного пользователя type LoginResponse struct  AccessToken string `json:"access_token"` > var ( errBadCredentials = errors.New("email or password is incorrect") ) // Секретный ключ для подписи JWT-токена // Необходимо хранить в безопасном месте var jwtSecretKey = []byte("very-secret-key") // Обработчик HTTP-запросов на вход в аккаунт func (h *AuthHandler) Login(c *fiber.Ctx) error  regReq := LoginRequest<> if err := c.BodyParser(&regReq); err != nil  return fmt.Errorf("body parser: %w", err) > // Ищем пользователя в памяти приложения по электронной почте user, exists := h.storage.users[regReq.Email] // Если пользователь не найден, возвращаем ошибку if !exists  return errBadCredentials > // Если пользователь найден, но у него другой пароль, возвращаем ошибку if user.password != regReq.Password  return errBadCredentials > // Генерируем JWT-токен для пользователя, // который он будет использовать в будущих HTTP-запросах // Генерируем полезные данные, которые будут храниться в токене payload := jwt.MapClaims "sub": user.Email, "exp": time.Now().Add(time.Hour * 72).Unix(), > // Создаем новый JWT-токен и подписываем его по алгоритму HS256 token := jwt.NewWithClaims(jwt.SigningMethodHS256, payload) t, err := token.SignedString(jwtSecretKey) if err != nil  logrus.WithError(err).Error("JWT token signing") return c.SendStatus(fiber.StatusInternalServerError) > return c.JSON(LoginResponseAccessToken: t>) > // Обработчик HTTP-запросов, которые связаны с пользователем type UserHandler struct  storage *AuthStorage > // Структура HTTP-ответа с информацией о пользователе type ProfileResponse struct  Email string `json:"email"` Name string `json:"name"` > func jwtPayloadFromRequest(c *fiber.Ctx) (jwt.MapClaims, bool)  jwtToken, ok := c.Context().Value(contextKeyUser).(*jwt.Token) if !ok  logrus.WithFields(logrus.Fields "jwt_token_context_value": c.Context().Value(contextKeyUser), >).Error("wrong type of JWT token in context") return nil, false > payload, ok := jwtToken.Claims.(jwt.MapClaims) if !ok  logrus.WithFields(logrus.Fields "jwt_token_claims": jwtToken.Claims, >).Error("wrong type of JWT token claims") return nil, false > return payload, true > // Обработчик HTTP-запросов на получение информации о пользователе func (h *UserHandler) Profile(c *fiber.Ctx) error  jwtPayload, ok := jwtPayloadFromRequest(c) if !ok  return c.SendStatus(fiber.StatusUnauthorized) > userInfo, ok := h.storage.users[jwtPayload["sub"].(string)] if !ok  return errors.New("user not found") > return c.JSON(ProfileResponse Email: userInfo.Email, Name: userInfo.Name, >) > 

      Запускаем веб-приложение и отправляем запрос на регистрацию нового пользователя:

      --location --request POST 'http://localhost/register' \ --header 'Content-Type: application/json' \ --data-raw '< "email": "john@doe.com", "name": "John", "password": "pickles" >' 

      В ответ получаем:

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

      Теперь попробуем пройти аутентификацию этого пользователя:

      --location --request POST 'http://localhost/login' \ --header 'Content-Type: application/json' \ --data-raw '< "email": "john@doe.com", "password": "pickles" >' 

      В ответ приходит:

      "access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2Njg5NTEwMTcsInN1YiI6ImpvaG5AZG9lLmNvbSJ9.Q3k6yMFYtuzPyjoZYpIHibJQPey29QWmlHfwS2A3keM"> 

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

      Отправляем запрос на получение информации о пользователе:

      -v 'http://localhost/profile' \ --header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2Njg5NTE0NDEsInN1YiI6ImpvaG5AZG9lLmNvbSJ9.e4yIoGzQC8ckcRISBjt4g18S2VEBiHrRhXG7N39-7qI' 

      В ответ приходит:

      "email":"john@doe.com","name":"John"> 

      Так как мы передали JWT-токен в заголовке HTTP-запроса Authorization, веб-приложение авторизовало нас как пользователя john@doe.com, и вернуло информацию о нашем аккаунте.

      В последнем запросе есть один интересный момент. Мы поставили перед значением токена слово Bearer:

      Bearer переводится как носитель. Он дает веб-приложению понять, что в заголовке Authorization передан токен для авторизации. Принято считать, что это слово означает фразу: «Дай доступ к носителю этого токена». Если не указать слово Bearer перед значением, то авторизация не пройдет даже с корректным значением токена.

      Мы реализовали функцию регистрации, аутентификации и получение информации об аккаунте авторизованного пользователя. Для авторизации мы использовали JWT-токен, который генерируются при входе в аккаунт на 72 часа и передается в заголовке HTTP-запроса Authorization.

      Выводы

      • Аутентификация — это процесс проверки подлинности пользователя, который пытается получить доступ к веб-приложению

      Открыть доступ

      Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно

      • 130 курсов, 2000+ часов теории
      • 1000 практических заданий в браузере
      • 360 000 студентов

      Наши выпускники работают в компаниях:

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *