at_hash и c_hash: верификация токенов доступа

В спецификации OpenID Connect (OIDC), построенной поверх OAuth 2.0 и использующей JOSE (JSON Object Signing and Encryption), особое внимание уделяется проверке целостности токенов через дополнительные хэши, встроенные в ID Token. Среди них ключевую роль играют at_hash и c_hash.

Эти значения позволяют клиентскому приложению убедиться, что полученные access_token и code действительно соответствуют ID Token, подписанному сервером авторизации, и не были подменены в процессе передачи.


Основы JOSE в контексте токенов

JOSE — это набор стандартов, включающий:

  • JWS (JSON Web Signature)
  • JWE (JSON Web Encryption)
  • JWK (JSON Web Key)
  • JWA (JSON Web Algorithms)

В OpenID Connect именно JWS используется для подписания ID Token, который представляет собой JWT (JSON Web Token). Внутри JWT могут присутствовать специальные claims, среди которых:

  • at_hash — хэш access token
  • c_hash — хэш authorization code

Механизм работы at_hash

at_hash предназначен для проверки соответствия access_token, выданного вместе с ID Token.

Принцип формирования

  1. Берётся значение access_token
  2. Применяется хэш-функция (обычно SHA-256, зависит от alg)
  3. Результат усечён до половины длины хэша
  4. Полученный байтовый массив кодируется в base64url

Формально:

at_hash = BASE64URL( leftmost_half( HASH(access_token) ) )

Проверка на стороне клиента

После получения ответа от Authorization Server:

  • извлекается access_token
  • извлекается at_hash из ID Token
  • пересчитывается хэш access_token
  • сравнивается результат

Если значения не совпадают — токен считается недействительным или подменённым.


Пример проверки at_hash в Node.js с использованием Jose

Библиотека jose предоставляет удобные инструменты для работы с JWT и криптографией.

import { createHash } from 'crypto';
import { jwtVerify } from 'jose';

function base64url(input) {
  return input
    .toString('base64')
    .replace(/=/g, '')
    .replace(/\+/g, '-')
    .replace(/\//g, '_');
}

function computeAtHash(accessToken) {
  const hash = createHash('sha256').update(accessToken).digest();
  const leftHalf = hash.subarray(0, hash.length / 2);
  return base64url(leftHalf);
}

// Пример сравнения
const accessToken = 'eyJ...';
const expectedAtHash = 'abc123...'; // из ID Token

const calculated = computeAtHash(accessToken);

if (calculated !== expectedAtHash) {
  throw new Error('at_hash verification failed');
}

Механизм работы c_hash

c_hash применяется аналогично, но для authorization code, который используется в Authorization Code Flow.

Формирование c_hash

Процесс идентичен at_hash:

c_hash = BASE64URL( leftmost_half( HASH(authorization_code) ) )

Его наличие особенно важно в сценариях, где ID Token возвращается вместе с authorization code (например, hybrid flow).


Применение в различных потоках OAuth 2.0

Authorization Code Flow

  • ID Token может содержать at_hash
  • c_hash обычно отсутствует, если code уже обменян на сервере

Hybrid Flow

  • одновременно возвращаются code, id_token, иногда access_token
  • обязательна проверка c_hash и/или at_hash

Implicit Flow (устаревающий)

  • at_hash обязателен для проверки access_token

Алгоритмы и зависимость от alg

Размер хэша зависит от алгоритма подписи JWT:

alg hash размер усечения
HS256 / RS256 SHA-256 128 бит
HS384 / RS384 SHA-384 192 бит
HS512 / RS512 SHA-512 256 бит

Критически важно использовать правильный алгоритм, указанный в заголовке JWT (alg), иначе вычисленный at_hash будет некорректным.


Ошибки реализации и типичные проблемы

Несовпадение base64url

Одна из самых частых ошибок — использование стандартного base64 вместо base64url. Отличия:

  • +-
  • /_
  • удаляются =

Полный хэш вместо усечённого

Некоторые реализации ошибочно сравнивают полный SHA-хэш, хотя требуется только левая половина.


Игнорирование алгоритма подписи

at_hash нельзя вычислять без учёта alg. Например, SHA-256 и SHA-512 дадут несовместимые результаты.


Ошибки при работе с буферами

В Node.js важно использовать бинарные данные (Buffer), а не строковое представление хэша:

hash.subarray(0, hash.length / 2)

Проверка через библиотеку jose

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

import { jwtVerify, createRemoteJWKSet } from 'jose';

const JWKS = createRemoteJWKSet(new URL('https://issuer.example.com/.well-known/jwks.json'));

const { payload } = await jwtVerify(idToken, JWKS, {
  audience: 'client_id',
  issuer: 'https://issuer.example.com'
});

// payload.at_hash и payload.c_hash доступны для дополнительной проверки

При корректной настройке многие проверки выполняются автоматически, однако в высоконагруженных или security-critical системах часто добавляют ручную валидацию.


Криптографическая роль at_hash и c_hash

Эти значения не являются средствами шифрования или защиты содержимого токена. Их задача — контроль целостности и связности:

  • подтверждение, что access_token соответствует id_token
  • защита от подмены токенов в транспортном уровне
  • контроль корректности выдачи Authorization Server

Важный аспект безопасности

Отсутствие проверки at_hash и c_hash не делает систему сразу уязвимой, но:

  • открывает возможность подмены токенов при компрометации канала
  • нарушает модель доверия OIDC
  • противоречит требованиям спецификации OpenID Connect Core

Связь с подписанным JWT

at_hash и c_hash всегда проверяются после валидации подписи JWT. Порядок важен:

  1. Проверка подписи ID Token (JWS)
  2. Проверка issuer, audience, exp
  3. Проверка at_hash / c_hash

Нарушение порядка приводит к ложным ощущениям безопасности.


Итоговая логика проверки

JWT signature valid
    ↓
claims validated (iss, aud, exp)
    ↓
compute at_hash / c_hash
    ↓
compare with ID Token
    ↓
token accepted or rejected