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 токенах.

(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 комментария
Запустить игру и не прогореть: зачем нужны маркетинговые исследования в гейминге и как их проводить

Маркетинговое исследование помогает экономить сотни тысяч долларов и выпускать продукты, которые приносят миллионы.

Удаленное трудоустройство - плюсы и минусы. Юридический взгляд
AWS анонсировала платформу Amplify Studio для разработки приложений с минимальным написанием кода Статьи редакции

Платформа подключена к онлайн-сервису Figma.

Яндекс Маркет не возвращает деньги за потерянный товар

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

Питч-дейтинг выпуск третий: комбайнеры

Рассказываем историю продакта «Яндекса», который решил помочь фермерам, а также составляем свой словарик стартапера.

«Альфа-банк» доставляет карту, самостоятельно вскрывая конверт, в котором видно все данные

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

Такие дела: жалоба на «Яндекс.Такси» за отказ перевозить собаку-поводыря

Девушка с нарушением зрения подала жалобу в прокуратуру из-за того, что водители популярного сервиса такси «Яндекс Go» слишком часто отказываются возить ее с собакой-поводырем.

Маркировка молочной продукции

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

Наводим порядок и изучаем rocket sience. Доклады Go meetup

На прошедшем Go meetup спикеры из Evrone, «Ситимобил» и «Авито» учили правильной организации кода микросервиса, рассказывали, как вырастить MVP в полноценную масштабируемую архитектуру, и разобраться с мусором и алгоритмами управления памятью. Все доклады записаны в студии и доступны для просмотра.

Google запланировала выпустить «умные» часы Pixel Watch в 2022 году — Insider Статьи редакции

Компания предложила сотрудникам протестировать устройства и дать обратную связь.

null