IT-инфраструктура для бизнеса и творчества

JWT — как безопасный способ аутентификации и передачи данных

JSON Web Token (JWT) — это открытый стандарт (RFC 7519) для создания токенов доступа, основанный на формате JSON. Как правило, используется для передачи данных для аутентификации в клиент-серверных приложениях. Токены создаются сервером, подписываются секретным ключом и передаются клиенту, который в дальнейшем использует данный токен для подтверждения своей личности.

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

В этой статье разберу, что такое Access токен, Refresh токен и как с ними работать.

Для дальнейших разборов будет использован токен:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE1ODEzNTcwMzl9.E4FNMef6tkjIsf7paNrWZnB88c3WyIfjONzAeEd4wF0

После того, как посетитель прошел авторизацию в нашей системе, указав свой логин и пароль, система выдает ему 2 токена: access token и refresh токен.

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

Токен разделен на три основные группы: заголовок, полезные данные и сигнатура, разделенные между собой точкой.

Заголовок

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 - это первая часть токена - есть заголовок. Она закодирована в Base64 и если её раскодировать, получим строку:

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

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

const header = atob('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9'); console.log(header);

typ - это наш тип токена JWT. Alg - алгоритм шифрования HMAC-SHA256. Их может быть несколько, но здесь буду говорить именно об этом алгоритме.

PAYLOAD или полезные данные

Вторым блоком идет eyJ1c2VyX2lkIjoxLCJleHAiOjE1ODEzNTcwMzl9

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

{"user_id":1,"exp":1581357039}

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

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

Сигнатура

Последняя часть токена - наиболее важная. У нас это E4FNMef6tkjIsf7paNrWZnB88c3WyIfjONzAeEd4wF0

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

Она получается примерно следующим образом:

Берем заголовок, например {"alg":"HS256","typ":"JWT"} и кодируем его в base64, получаем ту самую часть eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

Тоже самое проделываем с данными eyJ1c2VyX2lkIjoxLCJleHAiOjE1ODEzNTcwMzl9

После этого склеиваем их и получаем eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE1ODEzNTcwMzl9

Далее эти данные шифруем с помощью нашего алгоритма HMAC-SHA256 и ключа.

Условный псевдокод:

const header = '{"alg":"HS256","typ":"JWT"}' // строка const payload = '{"user_id":1,"exp":1581357039}' // строка // кодируем заголовок и данные в base64 const headerBase64 = base64urlEncode(header) const payloadBase64 = base64urlEncode(payload) // склеиваем точкой полученные строки const data = headerBase64 + '.' + payloadBase64 // кодируем алгоритмом шифрования нашим ключем шифрования const secret = '123456' const sig = HMAC-SHA256(data, secret) // и, наконец, получаем окончательный токен const jwt = data + '.' + sig

Проверка токена

Для проверка токена необходимо проделать ту же операцию.

Берем склейку заголовок + данные, кодируем с помощью алгоритма HMAC-SHA256 и нашего приватного ключа. А далее берем сигнатуру с токена и сверяем с результатом кодирования. Если результаты совпадают - значит данные подтверждены и можно быть уверенным, что они не были подменены.

Refresh token

Основной токен, про который шла речь выше, обычно имеет короткий срок жизни - 15-30 минут. Больше давать не стоит.

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

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

У него, обычно, нет какой-то структуры и это может быть некая случайная строка.

Для проекта odo24.ru я использовал следующий подход.

Генерируется Access токен и после случайная строка, например T6cjEbghMZmybUd_fhE

С нашего нового Access токена eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE1ODEzNTcwMzl9.E4FNMef6tkjIsf7paNrWZnB88c3WyIfjONzAeEd4wF0 беру последние шесть знаков, получаю Ed4wF0

Склеиваю и получаю рефреш токен T6cjEbghMZmybUd_fhEEd4wF0

Это сделано для привязки Access токена к Refresh. Для получения новых токенов необходимо передать эти два токена. Делается проверка на их связку и только после валидируется Access токен. Если и второй этап прошел успешно, тогда получаем с базы данных по текущему user_id рефреш токен и сверяем с тем, что к нам пришел. Если они совпадают, тогда генерируются новые токены и в базе данных обновляется Refresh токен на новый.

Где хранить токены?

В моем случае я разделил оба токена и храню в разных местах. Access токен нужен только для идентификации пользователя и на клиенте (JS) он не нужен, поэтому он передается в Cookie (http only).

Refresh токен хранится в LocalStorage и используется только когда Access токен перестал быть актуальным.

Зачем 2 токена?

Представим ситуацию, когда у нас каким-то образом украли Access токен. Да, это уже плохо и где-то у нас брешь в безопасности. Злоумышленник в этом случае сможет им воспользоваться не более чем на 15-30 минут. После чего токен "протухнет" и перестанет быть актуальным. Ведь нужен второй токен для продления.

Если украли Refresh токен, то без Access токена (который недоступен в JS) продлить ничего нельзя и он оказывается просто бесполезным.

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

Постскриптум

В своей реализации Refresh токена использовал общую длину 24 знака. Первые 6 знаков - это дата его "протухания", следующие 12 знаков - случайно сгенерированные данные. И в конце 6 знаков - это часть Access токена последней части сигнатуры.

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

Дата содержит год, месяц, день, час и минуты. Хранится в ASCII

Кодирование даты на Golang:

// приводим к целочисленному числу uint32. Итого 32 бита. // расчет простой: год 12 бит, месяц 4 бита, день 5 бит и т.д. Таким образом в аккурат умещаемся в 32 бита или 4 байта. date := uint32(year<<20) | uint32(month<<16) | uint32(day<<11) | uint32(hour<<6) | uint32(minute) // в цикле кодируем байты в ASCII. 1 знак это шесть бит. Итого и получаем шесть знаков даты по таблице ASCII - печатные знаки. for n := 0; n < 6; n++ { b6Bit = byte(date>>i) & 0x3F sBuilder.WriteByte(byte8bitToASCII(b6Bit)) ... }

Всю реализацию на Go можно изучить на Github

Заключение

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

{ "author_name": "Павел Кочетов", "author_type": "self", "tags": [], "comments": 3, "likes": 9, "favorites": 25, "is_advertisement": false, "subsite_label": "dev", "id": 106534, "is_wide": true, "is_ugc": true, "date": "Tue, 11 Feb 2020 21:00:09 +0300", "is_special": false }
(function () { let cdnUrl = `https://specialsf378ef5-a.akamaihd.net/SelectelBranding/images/` let previousArticleNumber = null let currentArticleNumber = 0 let platform = 'Desktop' let articles = [ { name: 'camera', url: `${cdnUrl}CameraCat`, text: 'умную камеру для\u00A0наблюдения за\u00A0котиками', link: 'https://vc.ru/selectel/306690', num: 3 }, { name: 'chill', url: `${cdnUrl}ChillCat`, text: 'трекер, который подскажет, когда пора отдохнуть', link: 'https://vc.ru/promo/288561-eye-tracker', num: 1 }, { name: 'cloud', url: `${cdnUrl}CloudCat`, text: 'котика: даёшь ему «пять», а\u00A0он делает бэкап в облако', link: 'https://vc.ru/dev/294799-maneki-neko', num: 2 } ] let buttonCycle = document.querySelector('.button--cycle') let buttonChoose = document.querySelector('.button--choose') let buttonMobile = document.querySelector('.button--mobile') let textField = document.querySelector('.selectel-footer-subtitle') let imageAgent = document.querySelector('.image--agent') let banner = document.querySelector('.selectel-footer') buttonCycle.addEventListener('click', cycleClick) buttonChoose.addEventListener('click', () => sendEvent(`Promo ${articles[currentArticleNumber].num} Left`, 'Click')) buttonMobile.addEventListener('click', () => sendEvent(`Promo ${articles[currentArticleNumber].num} Left`, 'Click')) let media = window.matchMedia("(max-width: 570px)") media.addEventListener('change', matchMedia) function matchMedia() { if (media.matches) { platform = 'Mobile' } else { platform = 'Desktop' } update() } matchMedia() function cycleClick(event) { sendEvent(`Promo ${articles[currentArticleNumber].num} Right`, 'Click') if (event) { event.preventDefault() event.stopPropagation() } window.open('https://vc.ru/tag/selectelDIY', '_blank') //cycle(event) } function cycle(event) { // incrementArticleNumber() textField.innerHTML = generatedText() imageAgent.src = articles[currentArticleNumber].url + platform + '.svg?3' imageAgent.setAttribute("class", "") imageAgent.classList.add('image--agent', articles[currentArticleNumber].name) banner.href = articles[currentArticleNumber].link } function update() { banner.href = articles[currentArticleNumber].link imageAgent.src = articles[currentArticleNumber].url + platform + '.svg' textField.innerHTML = generatedText() } function incrementArticleNumber() { previousArticleNumber = currentArticleNumber if (currentArticleNumber >= articles.length - 1) { currentArticleNumber = 0 } else { currentArticleNumber++ } } const sendEvent = (label, action = 'Click') => { const value = `SelectelDIY — loc: Footer — ${label} — ${action}`; if (window.dataLayer !== undefined) { window.dataLayer.push({ event: 'data_event', data_description: value, }); } }; function generatedText() { let defaultText if (platform === 'Desktop') { defaultText = `Мы тут собрали %text%. Хотите научим?` } else { defaultText = `Мы тут собрали %text%.` } return defaultText.replace('%text%', articles[currentArticleNumber].text) } function getRandom(min, max) { min = Math.ceil(min) max = Math.floor(max) return Math.floor(Math.random() * (max - min + 1)) + min } (function create() { currentArticleNumber = getRandom(0, articles.length - 1) cycle() let page = document.querySelector('.page--entry') if (page) { function insertAfter() { let parents = page.querySelectorAll('[data-id="7"]') let referenceNode = parents[0] referenceNode.parentNode.insertBefore(banner, referenceNode.nextSibling); loaded() } setTimeout(() => insertAfter(), 0) } }()) function loaded() { banner.classList.add('loaded') } loadImages([ `${cdnUrl}CameraCatDesktop.svg`, `${cdnUrl}ChillCatDesktop.svg`, `${cdnUrl}CloudCatDesktop.svg`, `${cdnUrl}CameraCatMobile.svg`, `${cdnUrl}ChillCatMobile.svg`, `${cdnUrl}CloudCatMobile.svg?3`, ]) function loadImages(urls) { return Promise.all(urls.map(function (url) { return new Promise(function (resolve) { var img = document.createElement('img'); img.onload = resolve; img.onerror = resolve; img.src = url; }); })); } }())
0
3 комментария
Популярные
По порядку

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

0

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

0

Мне не понятно два момента:
1) Зачем устанавливать время жизни для refresh, если он обновляется при обновлении access 
2) Это запись refresh token в бд, а что делать если например я авторизовался через пк, а потом через телефон ?

0
Читать все 3 комментария
Как OTUS стал платформой для самореализации. История преподавателя

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

Как не попасть в карьерную ловушку тимлида: личный опыт

Кажется, что тимлиду просто некуда расти: дальше надо либо идти в менеджмент, либо наоборот, становиться узконаправленным разработчиком. По просьбе «Лаборатории Касперского» Евгений Мацюк, который прошел в компании неординарный путь, рассказал о своих карьерных развилках во время и после тимлидства, а также поделился опытом горизонтального роста.

«Яндекс.Маркет» просто издевается

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

Потратил 400 тысяч рублей на накрутку поведенческих в Гугле. Подвожу итог

Всем привет. Это уже моя пятая статья про накрутку ПФ в поисковых системах Яндекс и Гугл. Довольно много о проверенных гипотезах было изложено в первой статье про Гугл, в этой же, я подведу небольшой итог по экспериментам за последние 8 месяцев.

Обучаюсь контекстной рекламе. Пока что Яндекс.Директ, в дальнейшем и Google.Adwords. Встала такая проблема, как практиковаться, как взять первые проекты для опыта чтобы начать работать на фрилансе?

История о том, как Илон Маск бросил все дела ради работы новостным ведущим в России

А ещё выучил русский язык и переехал в РФ, чтобы работать на YouTube канале 42CHANNEL. А о том, как Илон в своём познании настолько преисполнился, что как будто бы уже 100 триллионов миллиардов лет проживает на триллионах и триллионах таких же планет, читайте в статье.

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

Сервис предоставляется бесплатно.

Инвесторы подняли на 1000% стоимость акций компании Phunware — разработчика приложения для президентской кампании Трампа Статьи редакции

Бывший президент объявил о запуске собственной соцсети и её выходе на биржу.

Cloud CDN: что это такое, как устроено и кому нужно. Разбираем на примере бургеров

Cloud CDN — это сеть быстрой доставки статического контента в формате услуги облачного провайдера. Объяснить, как работает технология, проще всего на примере — сравнить Cloud CDN с популярным продуктом, который выглядит плюс-минус одинаково вне зависимости от того, заказали вы его в Москве, Питере или Нью-Йорке. Знакомьтесь: классический бургер.…

Исследование: сотрудники хотели бы иметь комнату отдыха, бесплатный сок, а работодатели уже готовы покупать ЗОЖ-снеки

Онлайн-сервис доставки продуктов и товаров СберМаркет и исследовательское агентство Research Me спросили сотрудников, как они хотели бы питаться в офисе и что в нем видеть. В опросе приняли участие более 1500 работающих людей по всей России. Сервис также спросил работодателей – В2В-клиентов СберМаркета: что они покупают в офис, что точно никогда…

Как AliExpress забрал у меня $300

24.08 - оплачиваю заказ.

null