Веб-токен JSON (JWT произносится как «jot») - это стандарт IETF для безопасной и компактной передачи заявлений в формате JSON между заинтересованными сторонами. JWT стал открытым стандартом авторизации на основе токенов.

JSON Web Token (JWT) - это компактное, безопасное для URL средство
представления претензий, передаваемых между двумя сторонами. Утверждения в JWT
кодируются как объект JSON, который используется в качестве полезной нагрузки структуры JSON
веб-подписи (JWS) или как открытый текст в веб-
шифровании JSON (JWE). структура, обеспечивающая цифровую
подпись заявлений или защиту целостности с помощью кода аутентификации сообщения
(MAC) и / или шифрование
.

Свойства JWT

  • Компактный
  • Заявления в кодировке JSON
  • Безопасный URL (кодировка URL base 64)
  • Автономный (следовательно, возможна реализация авторизации без сохранения состояния)

Авторизация с помощью JWT

Когда дело доходит до авторизации, есть два основных подхода.

  1. Бессессионный токен на основе (JWT)
  2. На основе сеанса (файлы cookie)

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

  1. Сервер хранит справочные объекты по идентификатору сеанса
  2. У клиента есть сеанс (файл cookie), сохраненный в приложении с данными сервера, такими как домен

Когда клиенту требуется доступ к ресурсам сервера, он отправляет сеанс (cookie) с HTTP-запросами. Сервер проверяет сеанс, чтобы предоставить доступ к ресурсам. Сервер должен дополнительно прочитать ссылочные объекты, связанные с сеансом, чтобы идентифицировать инициатора запроса. Есть некоторые ограничения этого процесса, которые устраняются системами авторизации на основе токенов JWT.

Преимущества авторизации на основе токенов

  • Без гражданства
  • Децентрализованная авторизация (удобство микросервисов)
  • Гибридное, удобное для одностраничных приложений
  • Удобное мобильное приложение
  • Централизованное хранилище токенов не требуется

Варианты JWT

Follow - это образец JWT, взятый из (https://openid.net/specs/draft-jones-json-web-signature-04.html). Он состоит из трех частей, разделенных точкой (.), Которые представляют собой значения обычного текста в кодировке URL-адреса base64.

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJqb2UiLCJleHAiOjEzMDA4MTkzODAsImh0dHA6Ly9leGFtcGxlLmNvbS9pc5c19p0cGxlLmNvbS9pc5c19pc6c5c5c5c5c5c5c5c5c6c5c6c5c6c5c5c5c5c5c5c5

jwt.io используется для декодирования этого токена и проверки содержимого.

«Три части» токена

Заголовок: {«typ»: «JWT», «alg»: «HS256»}

Заголовок, известный как JOSE (Подпись и шифрование объекта Javascript), содержит информацию о знаке и / или алгоритме шифрования, с помощью которого создается токен. Приведенный выше пример подписан с использованием HS256 (HMAC с SHA-256), который является симметричным алгоритмом.

Полезная нагрузка:

Полезные данные содержат два типа утверждений: Зарегистрированные утверждения и Пользовательские утверждения. Зарегистрированные утверждения четко определены в RFC. Некоторые из них: iss (эмитент), exp (срок действия), sub (тема), aud ( зрительская аудитория). Более подробную информацию можно найти в RFC документе . Зарегистрированные утверждения можно использовать в качестве дополнительных проверок для токена. Пользовательские утверждения могут быть любыми парами ключ / значение, которые могут быть определены в зависимости от требований приложения.

Подпись:

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

Варианты JWT JWS и JWE

JWT называется JWS (веб-подпись JSON), если токен подписан, как в приведенном выше примере. Токен JWS имеет целостность (невозможно изменить токен, не сделав подпись недействительной). Токен JWS можно легко декодировать с помощью кодировщика base64, поэтому это не лучший вариант для передачи конфиденциальных данных. JWE (веб-шифрование JSON) решает эту проблему, внедряя шифрование в JWT. JWS - это хорошо известный вариант реализации JWT. Доступны библиотеки и множество образцов для проверки, декодирования и генерации токенов JWS. Поэтому JWE подробно обсуждается с примерами кодов.

JWE

JWE - это стандартный способ шифрования утверждений токена JWT. Сегменты кода объясняют, как загрузить ключи, зашифровать токен и расшифровать токен. Эти коды написаны на node js с использованием библиотеки cisco node-jose.

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

Пара ключей создается с помощью открытых команд SSL

openssl genrsa -des3 -out private.pem 2048
openssl rsa -in private.pem -outform PEM -pubout -out public.pem
openssl rsa -in private.pem -out plain_private.pem

cat plain_private.pem

cat public.pem

Ключ Импорт образца кода:

const { JWK } = require('node-jose');
let makeKeys = async () => {
    let privateKey = `-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAwiSxcx5xFsNCjfDTOXz0M6RivgPXTTKb/PH6/x4vYl1xg/vI
rG1yDGyFuyAizxNaGtA+qo2CvM3iATyDWQg3/8vDWiD4cIvUD2WE3XXewjyeGzQs
o26DceRxopI2tlSgvznjifetGCp+oj4BWrTCqLF1AsR2ioUS7vuXMlpxXFhJfyEG
oWuQaZDwoz7CZv1KrQwlRTtkqtn4IeXpVcgWhg/1r0iRsvNJDokyiMVY8hrrvza2
j31JGaKYqCNYSC8Jf5EV7ONQhYGgQeRVxn8jS8UhLOFXDdNtgHwKi02tF1bmb3Ko
/s3vzPvFB8C7CsYRtHAzcx/xor2/CbT0QGYfUQIDAQABAoIBADgdoQj0UJ3SvKcI
aBViz7cpmbzwoUfYDAx16SXalLmq5sfOfDeSvvdmWtU2ubj/D/lWHfbwRkzKebv/
wLt4S69tNz8S7pyhXh9BKcFVc5jTKqQUVZ67r9S4wjvKZXQYTApZ8jIL3AVzKv08
TOnp+6YE/RxaqyToyAs5v383czLMdSM9bThDw+IZHfKFcl2iyUI4XVaPLDZPzUlG
MW13ypwFQXbdro9pnMs9v2573WumIVvbiLqIdxK1anag0KbCnCY2hDmoLNv7Mohz
1pFiqEStI6DQsYfEm/geIIL9nLhA8QJ28B9DtLKqRN7oWl2w5BiMEDCrmVK/4niM
lsqF1ZECgYEA5zVsjoJzRCBgZ5dRx9BOu9MKXQ+h00LS3+JaApEzW+QV0DplTvb5
aRUHhTIhUig2WdQVrEiBVXKg9eYmOBXOpX5pgDO5XJ16KcjdHcwMwoYAcUPIT5Y2
tMgoX+jCgTt95cnLKhAnSlH9cCyq+nmcdfDNuvNx8HJO7P33MaVDHCUCgYEA1vXX
cBWM+sGkUxpJNwlgA8bO7xq2kpbM3gqa0z6SKQhxnnmPyoKsZSHdwQMUCKIWBLjU
n27STmXm2SblT1cT39MCED2t0U7N2D1KUQfZkFaN4gImF77iZBWIfDLObAom6JKg
gER0glakAjbDMKq90mVzrKrR6XsD3az+AJE1eL0CgYBbHKB04E9QD3ouGGFv3lTI
i3fQCHL676Bt/aL+0/1rPsyhzAFURtsuX80g3gpnd9VDPOJ1i/T7mTp47IMvItjW
OqSUks1/A8e6Y59POLPmjCvsdoufYVCZmS7f7LeJeco5HXZkUw1Iqlq3M8MFBZt0
lrpb68eAu2sC1WnuNHPnyQKBgQC1LFb7vO4pmnOpJwp2PIyUIkfe9qDSRA8/Rajg
Smhd9SPt8X7jq+cpBbYlKzcDX7k3GaD7DyhpszEx7LpweG/jwbCHh7SsKMMNcfrk
+LzCDnFe/3ijotqkiBGUvC2GmbfZZVupQAkCoYtx4j35Eq8SWTd5XC+3nVrQxzD9
wWVT8QKBgBylVSsVeU7H/RT3RZ/qgWnpllBzGJic8X6lfyTHVD/9dO9XMzuD7R+/
VFc9Ib71QPzyq39BX+YiCI+xwDGARgO3GGA4vxvF46Tq0rYUpZ1LiC4raO4I5xrl
NxZCLVlhLWU27FR7xu8YN18mRCCwz1FCcsEtNU4nrBK2IIIwGLCf
-----END RSA PRIVATE KEY-----`;
    let publicKey = `-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwiSxcx5xFsNCjfDTOXz0
M6RivgPXTTKb/PH6/x4vYl1xg/vIrG1yDGyFuyAizxNaGtA+qo2CvM3iATyDWQg3
/8vDWiD4cIvUD2WE3XXewjyeGzQso26DceRxopI2tlSgvznjifetGCp+oj4BWrTC
qLF1AsR2ioUS7vuXMlpxXFhJfyEGoWuQaZDwoz7CZv1KrQwlRTtkqtn4IeXpVcgW
hg/1r0iRsvNJDokyiMVY8hrrvza2j31JGaKYqCNYSC8Jf5EV7ONQhYGgQeRVxn8j
S8UhLOFXDdNtgHwKi02tF1bmb3Ko/s3vzPvFB8C7CsYRtHAzcx/xor2/CbT0QGYf
UQIDAQAB
-----END PUBLIC KEY-----`
    const jwKeys = await Promise.all([
        JWK.asKey(privateKey, "pem"),
        JWK.asKey(publicKey, "pem")
    ]);
    let keystore = JWK.createKeyStore();
    await keystore.add(jwKeys[0]);
    await keystore.add(jwKeys[1]);
}
makeKeys();

Метод JWK.asKey используется для импорта ключей из строк в формате PEM. Формат ключа может быть передан в качестве второго параметра методу, и он возвращает ключ в формате JWK в качестве вывода. Импортированный ключ можно добавить в хранилище ключей и позже передать при шифровании и дешифровании токена JWE.

Пример кода генерации JWE:

const { JWK, JWE } = require('node-jose');
encrypt = async (raw, format = 'compact', contentAlg = "A256GCM", alg = "RSA-OAEP") => {
    let _publicKey = `-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwiSxcx5xFsNCjfDTOXz0
M6RivgPXTTKb/PH6/x4vYl1xg/vIrG1yDGyFuyAizxNaGtA+qo2CvM3iATyDWQg3
/8vDWiD4cIvUD2WE3XXewjyeGzQso26DceRxopI2tlSgvznjifetGCp+oj4BWrTC
qLF1AsR2ioUS7vuXMlpxXFhJfyEGoWuQaZDwoz7CZv1KrQwlRTtkqtn4IeXpVcgW
hg/1r0iRsvNJDokyiMVY8hrrvza2j31JGaKYqCNYSC8Jf5EV7ONQhYGgQeRVxn8j
S8UhLOFXDdNtgHwKi02tF1bmb3Ko/s3vzPvFB8C7CsYRtHAzcx/xor2/CbT0QGYf
UQIDAQAB
-----END PUBLIC KEY-----`
    let publicKey = await JWK.asKey(_publicKey, "pem");
    const buffer = Buffer.from(JSON.stringify(raw))
    const encrypted = await JWE.createEncrypt({ format: format, contentAlg: contentAlg, fields: { alg: alg } }, publicKey)
        .update(buffer).final();
    return encrypted;
}
let raw = {
    "mobileNumber": "1234567890",
    "customerId": "000000000",
    "sessionId": "3a600342-a7a3-4c66-bbd3-f67de5d7096f",
    "exp": 1645544094,
    "jti": "f3902a08-0e24-4dcc-bed1-f4cd9611bfad"
};
encrypt(raw);

JWE зашифрован с помощью загруженного publicKey. Строку ниже можно использовать для объяснения процесса шифрования. Важно понимать процесс шифрования, чтобы выяснить, как используются эти параметры.

const encrypted = await JWE.createEncrypt({ format: format, contentAlg: contentAlg, fields: { alg: alg } }, publicKey).update(buffer).final()

формат: форматы сериализации. JWE имеет два формата сериализации: компактная сериализация и сериализация JSON, которые представляют зашифрованные полезные данные.

contentAlg: алгоритм шифрования контента (A256GCM).

alg: алгоритм шифрования ключа CEK (RSA-OAEP)

Процесс шифрования:

contentAlg (A256GCM) - это алгоритм шифрования содержимого, который является AES (расширенный стандарт шифрования ) алгоритм GCM (режим счетчика Галуа) с 256-битным ключом. Это симметричный ключ, который работает быстрее при шифровании большого содержимого. CEK (ключ шифрования содержимого) - это случайный 256-битный ключ, сгенерированный библиотекой, которая шифрует полезную нагрузку. Зашифрованный ключ также шифруется с использованием алгоритма, определенного свойством algo.

алгоритм (RSA-OAEP), асимметричное шифрование которого использует алгоритм RSA с методом OAEP (оптимальное асимметричное шифрование с заполнением). Зашифрованный CEK также помещается в токен JWE, который может использоваться получателем для расшифровки полезной нагрузки. Этот метод называется шифрованием ключей или переносом ключей. Более подробную информацию можно найти в RFC.

Токен JWE, созданный с помощью приведенного выше кода:

«EyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkEyNTZHQ00iLCJraWQiOiJHUHZ0TFN3S29BMTJ1aW9Xc3IzeWhhdWNJWG12RHVUbWNUcXk1QS1EbVZzIn0.ai5j5Kk43skqPLwR0Cu1ZIyWOTUpLFKCN5cuZzxHdp0eXQjYLGpj8jYvU8yTu9rwZQeN9EY0_81hQHXEzMQgfCsRm0HXjcEwXIny-FH8N2WtYCjWrQubAIbnrqnNP-8TeCNZaShEMwYcVLUls8Yik2s6LiHTb9FTNfbMxyBlaQ4v8Q2rnKQ4tto5IG7D_dRC59WDmN2XkVfw3DWNbOxb34wgKHq0Zb7YcflQyXYxqkOAU9Xeuv_urFAhUyCzHt8W2vbqUtSosmHGoLf64QKUwqKietOVEI8tAruj-JTHUTR4P_-ZHy9jEgTNQLfZlkBDEDnXme2tcYqHj9XiidiXpw.thh69dp0Pz73kycQ.eF5bjuqtF60gW8O8cXKiYyDsBPX8OL0GQfhOxwGWUmYtHOds7FJWTNoSFnv5E6A_Bgn_2W2JUD1kniYUR1yOPnEJfHQ_SXMLJECZ8mBpHlYZWl3-39nGESxfdx1oXP1pO_ahP9eWtBCeS3X8DgPV0sLu1N-0ox6iZxTQ_PU314yTiUobhemNfT9ORghnOSApHi_HCKk9G1ABklbP82flcBfzA1tWKBw.HRhA5nn8HLsvYf8F-BzQew»

Сгенерированный компактный сериализованный токен состоит из пяти разделов в кодировке URL base64, разделенных точкой (.).

это…

  1. Заголовок JOSE: {"alg": "RSA-OAEP", "enc": "A256GCM", "kid": "GPvtLSwKoA12uioWsr3yhaucIXmvDuTmcTqy5A-DmVs"}
  2. Зашифрованный CEK
  3. Зашифрованная полезная нагрузка (CEK)
  4. Инициализированный вектор
  5. Тег аутентификации

Эквивалентная сериализация JSON выглядит, как показано ниже.

Расшифровка и проверка:

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

const { JWK, JWE, parse } = require('node-jose');
decrypt = async (encryptedBody) => {
    let _privateKey = `-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAwiSxcx5xFsNCjfDTOXz0M6RivgPXTTKb/PH6/x4vYl1xg/vI
rG1yDGyFuyAizxNaGtA+qo2CvM3iATyDWQg3/8vDWiD4cIvUD2WE3XXewjyeGzQs
o26DceRxopI2tlSgvznjifetGCp+oj4BWrTCqLF1AsR2ioUS7vuXMlpxXFhJfyEG
oWuQaZDwoz7CZv1KrQwlRTtkqtn4IeXpVcgWhg/1r0iRsvNJDokyiMVY8hrrvza2
j31JGaKYqCNYSC8Jf5EV7ONQhYGgQeRVxn8jS8UhLOFXDdNtgHwKi02tF1bmb3Ko
/s3vzPvFB8C7CsYRtHAzcx/xor2/CbT0QGYfUQIDAQABAoIBADgdoQj0UJ3SvKcI
aBViz7cpmbzwoUfYDAx16SXalLmq5sfOfDeSvvdmWtU2ubj/D/lWHfbwRkzKebv/
wLt4S69tNz8S7pyhXh9BKcFVc5jTKqQUVZ67r9S4wjvKZXQYTApZ8jIL3AVzKv08
TOnp+6YE/RxaqyToyAs5v383czLMdSM9bThDw+IZHfKFcl2iyUI4XVaPLDZPzUlG
MW13ypwFQXbdro9pnMs9v2573WumIVvbiLqIdxK1anag0KbCnCY2hDmoLNv7Mohz
1pFiqEStI6DQsYfEm/geIIL9nLhA8QJ28B9DtLKqRN7oWl2w5BiMEDCrmVK/4niM
lsqF1ZECgYEA5zVsjoJzRCBgZ5dRx9BOu9MKXQ+h00LS3+JaApEzW+QV0DplTvb5
aRUHhTIhUig2WdQVrEiBVXKg9eYmOBXOpX5pgDO5XJ16KcjdHcwMwoYAcUPIT5Y2
tMgoX+jCgTt95cnLKhAnSlH9cCyq+nmcdfDNuvNx8HJO7P33MaVDHCUCgYEA1vXX
cBWM+sGkUxpJNwlgA8bO7xq2kpbM3gqa0z6SKQhxnnmPyoKsZSHdwQMUCKIWBLjU
n27STmXm2SblT1cT39MCED2t0U7N2D1KUQfZkFaN4gImF77iZBWIfDLObAom6JKg
gER0glakAjbDMKq90mVzrKrR6XsD3az+AJE1eL0CgYBbHKB04E9QD3ouGGFv3lTI
i3fQCHL676Bt/aL+0/1rPsyhzAFURtsuX80g3gpnd9VDPOJ1i/T7mTp47IMvItjW
OqSUks1/A8e6Y59POLPmjCvsdoufYVCZmS7f7LeJeco5HXZkUw1Iqlq3M8MFBZt0
lrpb68eAu2sC1WnuNHPnyQKBgQC1LFb7vO4pmnOpJwp2PIyUIkfe9qDSRA8/Rajg
Smhd9SPt8X7jq+cpBbYlKzcDX7k3GaD7DyhpszEx7LpweG/jwbCHh7SsKMMNcfrk
+LzCDnFe/3ijotqkiBGUvC2GmbfZZVupQAkCoYtx4j35Eq8SWTd5XC+3nVrQxzD9
wWVT8QKBgBylVSsVeU7H/RT3RZ/qgWnpllBzGJic8X6lfyTHVD/9dO9XMzuD7R+/
VFc9Ib71QPzyq39BX+YiCI+xwDGARgO3GGA4vxvF46Tq0rYUpZ1LiC4raO4I5xrl
NxZCLVlhLWU27FR7xu8YN18mRCCwz1FCcsEtNU4nrBK2IIIwGLCf
-----END RSA PRIVATE KEY-----`;
    let keystore = JWK.createKeyStore();
    await keystore.add(await JWK.asKey(_privateKey, "pem"));
    let outPut = parse.compact(encryptedBody);
    let decryptedVal = await outPut.perform(keystore);
    let claims = Buffer.from(decryptedVal.plaintext).toString();
    return claims;
}
decrypt('eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkEyNTZHQ00iLCJraWQiOiJHUHZ0TFN3S29BMTJ1aW9Xc3IzeWhhdWNJWG12RHVUbWNUcXk1QS1EbVZzIn0.ai5j5Kk43skqPLwR0Cu1ZIyWOTUpLFKCN5cuZzxHdp0eXQjYLGpj8jYvU8yTu9rwZQeN9EY0_81hQHXEzMQgfCsRm0HXjcEwXIny-FH8N2WtYCjWrQubAIbnrqnNP-8TeCNZaShEMwYcVLUls8Yik2s6LiHTb9FTNfbMxyBlaQ4v8Q2rnKQ4tto5IG7D_dRC59WDmN2XkVfw3DWNbOxb34wgKHq0Zb7YcflQyXYxqkOAU9Xeuv_urFAhUyCzHt8W2vbqUtSosmHGoLf64QKUwqKietOVEI8tAruj-JTHUTR4P_-ZHy9jEgTNQLfZlkBDEDnXme2tcYqHj9XiidiXpw.thh69dp0Pz73kycQ.eF5bjuqtF60gW8O8cXKiYyDsBPX8OL0GQfhOxwGWUmYtHOds7FJWTNoSFnv5E6A_Bgn_2W2JUD1kniYUR1yOPnEJfHQ_SXMLJECZ8mBpHlYZWl3-39nGESxfdx1oXP1pO_ahP9eWtBCeS3X8DgPV0sLu1N-0ox6iZxTQ_PU314yTiUobhemNfT9ORghnOSApHi_HCKk9G1ABklbP82flcBfzA1tWKBw.HRhA5nn8HLsvYf8F-BzQew');

Заключение

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