JWT (JSON Web Token) представляет собой компактный токен, состоящий из трёх частей, разделённых точками:
Формат:
header.payload.signature
Каждая часть закодирована в Base64URL, что требует отдельной обработки при декодировании и проверке.
Web Crypto API не предоставляет встроенного декодера Base64URL, поэтому преобразование выполняется вручную:
function base64UrlDecode(str) {
const base64 = str
.replace(/-/g, '+')
.replace(/_/g, '/')
.padEnd(str.length + (4 - (str.length % 4)) % 4, '=');
const binary = atob(base64);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
return bytes;
}
Для JSON-частей:
function decodeJwtPart(part) {
const bytes = base64UrlDecode(part);
return JSON.parse(new TextDecoder().decode(bytes));
}
function parseJwt(token) {
const [header, payload, signature] = token.split('.');
return {
header: decodeJwtPart(header),
payload: decodeJwtPart(payload),
signature: base64UrlDecode(signature)
};
}
Перед криптографической проверкой важно убедиться, что алгоритм соответствует ожидаемому:
function validateAlg(header, expectedAlg) {
if (header.alg !== expectedAlg) {
throw new Error('Unsupported algorithm');
}
}
На практике часто используются:
HS256 использует симметричный ключ. Web Crypto API позволяет
проверить подпись через HMAC.
async function importHmacKey(secret) {
const keyData = new TextEncoder().encode(secret);
return crypto.subtle.importKey(
'raw',
keyData,
{ name: 'HMAC', hash: 'SHA-256' },
false,
['verify']
);
}
Подпись считается по строке:
header.payload
function getSignedData(token) {
const [header, payload] = token.split('.');
return new TextEncoder().encode(`${header}.${payload}`);
}
async function verifyHs256(token, secret) {
const { signature } = parseJwt(token);
const key = await importHmacKey(secret);
const data = getSignedData(token);
return crypto.subtle.verify(
'HMAC',
key,
signature,
data
);
}
RS256 использует асимметричную криптографию: публичный ключ применяется для проверки подписи.
Публичный ключ обычно предоставляется в формате SPKI (SubjectPublicKeyInfo).
async function importRsaPublicKey(pem) {
const binary = pem
.replace(/-----BEGIN PUBLIC KEY-----/, '')
.replace(/-----END PUBLIC KEY-----/, '')
.replace(/\s/g, '');
const der = base64UrlDecode(binary);
return crypto.subtle.importKey(
'spki',
der,
{ name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' },
false,
['verify']
);
}
async function verifyRs256(token, publicKey) {
const { signature } = parseJwt(token);
const key = await importRsaPublicKey(publicKey);
const data = getSignedData(token);
return crypto.subtle.verify(
'RSASSA-PKCS1-v1_5',
key,
signature,
data
);
}
Общий алгоритм включает несколько этапов:
async function verifyJwt(token, options) {
const { header, payload } = parseJwt(token);
validateAlg(header, options.alg);
let isValid = false;
if (header.alg === 'HS256') {
isValid = await verifyHs256(token, options.secret);
}
if (header.alg === 'RS256') {
isValid = await verifyRs256(token, options.publicKey);
}
if (!isValid) {
return false;
}
return validateClaims(payload, options);
}
JWT payload часто содержит служебные поля:
exp — время истеченияnbf — начало действияiat — время выпускаiss — издательaud — аудиторияfunction validateTimeClaims(payload) {
const now = Math.floor(Date.now() / 1000);
if (payload.exp && now >= payload.exp) {
return false;
}
if (payload.nbf && now < payload.nbf) {
return false;
}
return true;
}
function validateClaims(payload, options) {
if (!validateTimeClaims(payload)) {
return false;
}
if (options.issuer && payload.iss !== options.issuer) {
return false;
}
if (options.audience && payload.aud !== options.audience) {
return false;
}
return true;
}
Web Crypto API работает асинхронно и строго изолированно:
crypto.subtleТиповые причины отказа в проверке:
header.payload при подписанииtry {
const result = await verifyJwt(token, config);
} catch (e) {
// криптографическая ошибка или некорректный формат
}
При работе с Web Crypto API критично учитывать:
none алгоритмаКорректная архитектура проверки JWT обычно разделяется на уровни:
Такое разделение снижает риск логических ошибок и упрощает аудит безопасности.