Перейти к содержимому

Как проверить jwt токен на валидность

  • автор:

Как проверить Jwt(Keycloak) на валидность?

Однако SECRET_KEY надо еще получить c Keyckloak.
В правильном ли направлении я иду?
Если да, то никто не знает как получить этот секрет.
——————————————————-
Возможно есть другой, более правильный способ проверить токен доступа.

P.S. Можете скидывать материал или доку. Стараюсь разобраться в материале, но пока тону в нем.

  • Вопрос задан более года назад
  • 518 просмотров

1 комментарий

Средний 1 комментарий

Как проверить jwt токен на валидность

JSON Web Token, или просто JWT, представляет собой строку, полученную на основе формата JSON, и используется в качестве более безопасной и простой альтернативы сессиям и файлам cookie для авторизации.

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

JWT формируют три части:

  • заголовок ( header );
  • данные ( payload );
  • подпись ( signature ).

Заголовок представляет собой объект JSON и описывает сам токен с помощью следующих свойств:

  • alg — алгоритм шифрования, используемый для подписи JWT, если токен не подписывается, то значением должно быть none (обязательный параметр);
  • typ — тип токена, необходимо указывать со значением «JWT», если могут использоваться токены другого типа (необязательный параметр);
  • ctp — тип данных, необходимо указывать со значением «JWT», если в payload присутствуют пользовательские ключи.

В данных, которые также передаются объектом JSON, указывается необходимая информация о пользователе. Также возможно задание значений предопределенных ключей (все они не обязательны) для описания конфигурации токена:

  • iss — приложение, создавшее токен;
  • sub — назначение JWT;
  • aud — массив получателей токена;
  • exp — дата и время, указанное в миллисекундах, прошедших с 01.01.1970, до наступления которого JWT будет валиден;
  • nbf — дата и время, указанное в миллисекундах, прошедших с 01.01.1970, до наступления которого JWT будет не валиден;
  • iat — дата и время создания JWT, указанное в миллисекундах, прошедших с 01.01.1970;
  • jti — уникальный идентификатор токена.

Заголовок и данные используются для вычисления значения подписи по указанному в заголовке в свойстве alg алгоритму шифрования.

Далее формируется сам JWT.

Сгенерированный JWT отправляется клиенту, где он сохраняется в localStorage или sessionStorage , и будет отправляться клиентом серверу при каждом HTTP запросе в заголовке Authorization .

Authorization: Bearer

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

Рассмотрим пример использования в Node.js JWT.

 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
[  "id": 1, "login": "user1", "password": "password1" >,  "id": 2, "login": "user2", "password": "password2" >,  "id": 3, "login": "user3", "password": "password3" > ] 
 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76
const express = require('express'), app = express(), crypto = require('crypto'), users = require('./users'); const host = '127.0.0.1'; const port = 7000; const tokenKey = '1a2b-3c4d-5e6f-7g8h'; app.use(express.json()); app.use((req, res, next) =>  if (req.headers.authorization)  let tokenParts = req.headers.authorization .split(' ')[1] .split('.'); let signature = crypto .createHmac('SHA256', tokenKey) .update(`$tokenParts[0]>.$tokenParts[1]>`) .digest('base64'); if (signature === tokenParts[2]) req.user = JSON.parse( Buffer.from( tokenParts[1], 'base64' ).toString('utf8') ); next(); > next(); >); app.post('/api/auth', (req, res) =>  for (let user of users)  if ( req.body.login === user.login && req.body.password === user.password )  let head = Buffer.from( JSON.stringify( alg: 'HS256', typ: 'jwt' >) ).toString('base64'); let body = Buffer.from( JSON.stringify(user) ).toString('base64'); let signature = crypto .createHmac('SHA256', tokenKey) .update(`$head>.$body>`) .digest('base64'); return res.status(200).json( id: user.id, login: user.login, token: `$head>.$body>.$signature>`, >); > > return res .status(404) .json( message: 'User not found' >); >); app.get('/user', (req, res) =>  if (req.user) return res.status(200).json(req.user); else return res .status(401) .json( message: 'Not authorized' >); >); app.listen(port, host, () => console.log(`Server listens http://$host>:$port>`) ); 

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

Для экономии времени и избежания реализации собственного алгоритма формирования в Node.js JWT можно использовать npm модуль jsonwebtoken .

 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
const express = require('express'), app = express(), jwt = require('jsonwebtoken'), users = require('./users'); const host = '127.0.0.1'; const port = 7000; const tokenKey = '1a2b-3c4d-5e6f-7g8h'; app.use(express.json()); app.use((req, res, next) =>  if (req.headers.authorization)  jwt.verify( req.headers.authorization.split(' ')[1], tokenKey, (err, payload) =>  if (err) next(); else if (payload)  for (let user of users)  if (user.id === payload.id)  req.user = user; next(); > > if (!req.user) next(); > > ); > next(); >); app.post('/api/auth', (req, res) =>  for (let user of users)  if ( req.body.login === user.login && req.body.password === user.password )  return res.status(200).json( id: user.id, login: user.login, token: jwt.sign( id: user.id >, tokenKey), >); > > return res .status(404) .json( message: 'User not found' >); >); app.get('/user', (req, res) =>  if (req.user) return res.status(200).json(req.user); else return res .status(401) .json( message: 'Not authorized' >); >); app.listen(port, host, () => console.log(`Server listens http://$host>:$port>`) ); 

akatasonov / tokens.md

Аутентификация(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.

    ------------------------------------------------------------------------------------------------- | id | username | refreshTokensMap ------------------------------------------------------------------------------------------------- | 1 | alex | < refreshTokenTimestamp1: 'refreshTokenBody1', refreshTokenTimestamp2: 'refreshTokenBody2'>------------------------------------------------------------------------------------------------- 
    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. Удаляет старый рефреш токен
      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, deviceID, fingerprint браузера, генерить рандомный randomUserID
      • дополнительно шифровать токены (в nodejs например crypt >> aes-256)
      • зашивать в payload также IP/подсеть владельца токена. В этом случае при каждой попытке зайти с новой точки доступа к интерету придется перелогиниватся.

      JSONWebToken

      JSONWebToken — это библиотека для создания (подписания) и подтверждения (проверки) токенов, используемых для аутентификации/авторизации пользователей. Данная библиотека является реализацией JSON Web Tokens для Node.js .

      Установка​

      yarn add jsonwebtoken # или npm i jsonwebtoken 

      Подписание (создание) токена​

      const jwt = require('jsonwebtoken') jwt.sign(payload, secretOrPrivateKey, [options, callback])  // Пример jwt.sign(  username: 'John', email: 'john@email.com' >, 'secret',   expiresIn: '1h' > ) 

      Если передан колбек, то метод выполняется асинхронно. Данный колбек получает объект ошибки err . В противном случае, метод выполняется синхронно, возвращая токен в виде строки.

      Аргументы​

      • payload — полезная нагрузка: объект, буфер или строка, представляющие валидный JSON. Объект преобразуется с помощью метода JSON.stringify()
      • secretOrPrivateKey — строка, буфер или объект, содержащие секрет для алгоритмов HMAC или зашифрованный с помощью схемы PEM приватный ключ для RSA и ECDSA

      Настройки​

      • Registered claim names
      • Creating and Validating JWTs
      • algorithm (по умолчанию HS256 )
      • expiresIn — время, в течение которого токен считается действительным: 100 — 100 с, ‘100’ — 100 мс, ‘1h’ — 1 час, ‘2d’ — 2 дня
      • notBefore — время, по истечении которого токен будет считаться действительным
      • audience
      • issuer
      • jwtid
      • subject
      • noTimestamp
      • header
      • keyid
      • mutatePayload — если имеет значение true , функция sign() будет модифицировать объект payload напрямую. Это бывает полезным, когда нам нужна «сырая» ссылка на полезную нагрузку после применения к ней настроек, но до шифрования.

      Настройки expiresIn , notBefore , audience , subject , issuer могут быть определены прямо в payload как exp , nbf , aud , sub и iss , соответственно.

      Заголовок может быть кастомизирован через объект options.header .

      В сгенерированный jwt по умолчанию включается iat (время выпуска, создания), если не определено noTimestamp: true .

      Пример синхронного подписания токена с использованием RSA SHA256 ​
      const privateKey = fs.readFileSync('private.key') const token = jwt.sign( name: 'John Smith' >, privateKey,  algorithm: 'RS256' >) 
      Пример асинхронного подписания токена​
      jwt.sign( name: 'John Smith' >, privateKey,  algorithm: 'RS256' >, (err, token) =>   if (err) console.error(err) console.log(token) >) 

      Подтверждение (проверка) токена​

      const jwt = required('jsonwebtoken') jwt.verify(token, secretOrPublicKey, [options, callback]) 

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

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

      Аргументы​

      • token
      • secretOrPublicKey — если jwt.verify() вызывается асинхронно, данный аргумент может быть функцией, запрашивающей секрет или публичный ключ. Некоторые библиотеки в качестве секрета ожидают получить строку в формате base64. В этом случае вместо secret следует передать Buffer.from(secret, ‘base64’)

      Настройки​

      • algorithms — массив с названиями разрешенных алгоритмов ( [‘HS256’, ‘HS384’] )
      • audience
      • complete — возвращает объект < payload, header, signature >вместо содержимого полезной нагрузки
      • issuer
      • jwtid
      • ignoreExpiration — если true , время жизни токена будет игнорироваться
      • ignoreNotBefore
      • subject
      • clockTolerance — допустимая разница во времени в сек
      • maxAge — максимально допустимый «возраст» токена (100, ‘100’, ‘1h’, ‘2d’)
      • clockTimestamp — время в сек, используемое в качестве текущего для всех сравнений
      • nonce — используется в Open ID для ID токенов

      Примеры​

      // Синхронное подтверждение симметричного токена try   const decoded = jwt.verify(token, 'secret') console.log(decoded.name) // John > catch (err)   console.error(err) >  // или jwt.verify(token, 'secret', (err, decoded) =>   if (err) console.error(err) console.log(decoded.name) // John >)  // Подтверждение асимметричного токена const cert = fs.readFileSync('public.pem') // получаем публичный ключ jwt.verify(token, cert, (err, decoded) =>   if (err) console.error(err) console.log(decoded.name) // John >)  // Подтверждение опциональных настроек const cert = fs.readFileSync('public.pem') // получаем публичный ключ jwt.verify(token, cert,  audience: 'urn:john', issuer: 'urn:issuer', jwtid: 'jwtid', subject: 'subject' >, (err, decoded) => >)  // Подтверждение с помощью колбека `getKey()` const jwksClient = require('jwks-rsa') const client = jwksClient(  jwksUri: 'https://example.com/.well-known/jwks.json' >) function getKey(header, callback)   client.getSigningKey(header.kid, (err, key) =>   const signingKey = key.publicKey || key.rsaPublicKey callback(null, signingKey) >) > jwt.verify(token, getKey, options, (err, decoded) =>   if (err) console.error(err) console.log(decoded.name) // John >) 

      Расшифровка (декодирование) токена​

      jwt.decode(token, [options]) 

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

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