Featured image of post OAuth 2.0 и OIDC: аутентификация, авторизация и проверка токенов

OAuth 2.0 и OIDC: аутентификация, авторизация и проверка токенов

Аутентификация и авторизация, роли OAuth 2.0 и OIDC, код авторизации с PKCE и проверка ID-токена: формулы и примеры на JavaScript и Python.

OAuth 2.0 — это механизм авторизации, который предоставляет приложениям ограниченный доступ. OpenID Connect (OIDC) добавляет к нему способ передать клиенту результат аутентификации конечного пользователя.

В статье разграничены аутентификация и авторизация, назначение токенов, код авторизации с PKCE и проверка ID-токена. Код иллюстрирует отдельные операции, а не полноценный вход в систему. Для рабочего сервиса используйте библиотеку OIDC и настройки провайдера, соответствующие вашим требованиям.

1. Аутентификация и авторизация

Аутентификация / AuthN

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

Авторизация / AuthZ

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

ПараметрАутентификацияАвторизация
ВопросКто это?Что разрешено?
СокращениеAuthNAuthZ
Рассматриваемый механизмOIDC передаёт результат аутентификацииOAuth 2.0 предоставляет ограниченный доступ
Пример токенаID-токен OIDCТокен доступа OAuth 2.0

Не каждый способ аутентификации выдаёт ID-токен. OAuth 2.0 также не определяет весь контроль доступа внутри приложения. Получив токен, API всё равно должен проверить разрешения для конкретного ресурса и действия.

Сам OAuth 2.0 не стандартизирует проверку клиентом результата аутентификации конечного пользователя. Нельзя считать вход выполненным только потому, что получен токен доступа. Используйте протокол, например OIDC, позволяющий проверить этот результат.

2. Роли и типы предоставления доступа в OAuth 2.0

2.1 Ограниченный доступ

OAuth 2.0 позволяет клиенту получить токен доступа от имени владельца ресурса или от собственного имени. Делегирование доступа без передачи пароля пользователя клиенту — важное свойство потока кода авторизации.

2.2 Четыре роли

  1. Владелец ресурса (Resource Owner) — субъект, способный разрешить доступ к защищённому ресурсу. Это не обязательно человек.
  2. Клиент (Client) — приложение, использующее защищённые ресурсы.
  3. Сервер авторизации (Authorization Server) — сервер, который проверяет авторизацию, аутентификацию клиента и другие условия применяемого потока, затем выдаёт токены.
  4. Сервер ресурсов (Resource Server) — сервер, который проверяет токены и разрешения и предоставляет защищённые ресурсы.

2.3 Основные типы грантов

Код авторизации (Authorization Code Grant)

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

Неявный грант (Implicit Grant)

В этом случае токен доступа возвращается непосредственно в ответе авторизации. Возникают риски утечки, внедрения и повторного использования токенов. RFC 9700 рекомендует НЕ использовать этот грант (SHOULD NOT), если не выполнены указанные условия снижения рисков. Для новой конфигурации выбирайте код авторизации с PKCE. Это не означает одинакового запрета всех типов ответов OIDC.

Пароль владельца ресурса (ROPC)

При этом гранте клиент получает пароль пользователя напрямую. Хотя RFC 6749 описывает такой механизм, обновлённые рекомендации RFC 9700 ЗАПРЕЩАЮТ его использование (MUST NOT). Миграцию устаревших систем нельзя представлять как рекомендуемое исключение.

Учётные данные клиента (Client Credentials Grant)

Конфиденциальный клиент аутентифицирует себя и получает токен. Он может обращаться к ресурсам под своим контролем или к ресурсам другого владельца по предварительной договорённости с сервером авторизации. Обычно это обмен между системами без взаимодействия с конечным пользователем; клиент не обязательно становится владельцем ресурса.

2.4 Код авторизации и PKCE

PKCE (Proof Key for Code Exchange) связывает клиента, начавшего запрос авторизации, с клиентом, обменивающим код на токены. Это помогает защититься от перехвата и внедрения кодов. Для публичных клиентов PKCE обязателен, а для конфиденциальных также рекомендуется. Он не заменяет аутентификацию, необходимую конфиденциальному клиенту.

Для каждой транзакции авторизации клиент создаёт новый секретный code_verifier. RFC 7636 задаёт длину 43–128 символов из латинских букв, цифр и -._~ и рекомендует не менее 256 бит энтропии. Кодирование 32 байт от криптографически стойкого генератора случайных значений в Base64url без заполнения даёт 43 символа.

Для S256 значение challenge вычисляется так. Завершающие символы заполнения = в Base64url опускаются.

\[ \text{code\_challenge}=\operatorname{BASE64URL}\!\left(\operatorname{SHA256}\!\left(\operatorname{ASCII}(\text{code\_verifier})\right)\right) \]

В запросе авторизации отправляют code_challenge и code_challenge_method=S256, а в запросе токена — code_verifier, сохранённый для той же транзакции. Сервер проверяет его по методу и challenge, записанным вместе с кодом. Нельзя переключаться на plain только потому, что S256 завершился ошибкой.

Общая схема потока

  sequenceDiagram
    autonumber
    participant U as "Пользователь / браузер"
    participant C as "Клиент"
    participant AS as "Сервер авторизации / OP OIDC"
    participant RS as "Сервер ресурсов"
    Note over C: "Сохранить verifier и state для транзакции; для OIDC также nonce"
    C->>U: "Перенаправить к конечной точке авторизации"
    U->>AS: "Запрос: challenge и S256; для OIDC openid и nonce"
    AS-->>U: "При необходимости запросить аутентификацию и согласие"
    U->>AS: "Выполнить аутентификацию и авторизацию"
    AS-->>U: "Перенаправить на зарегистрированный redirect_uri с code и state"
    U->>C: "Обратный вызов: code и state"
    Note over C: "Проверить транзакцию, state и ожидаемого издателя"
    C->>AS: "Обменять code с verifier и требуемой аутентификацией клиента"
    Note over AS: "Проверить код, привязки и PKCE"
    AS-->>C: "Токен доступа; для OIDC также ID-токен"
    Note over C: "Для OIDC проверить ID-токен и nonce"
    C->>RS: "Вызвать API с токеном доступа"
    RS-->>C: "Проверить токен и разрешения, затем вернуть ресурс"

Это обзор успешного потока. Защищайте обмен с помощью TLS. Реализация также должна обрабатывать проверку адресов перенаправления, однократное использование кодов, ответы с ошибками, истечение срока транзакции и защиту от атак mix-up при работе с несколькими серверами авторизации.

Генерация параметров PKCE (JavaScript / Web Crypto API)

Эти браузерные функции требуют безопасного контекста, например HTTPS. generatePkce() только возвращает параметры: она не отправляет запрос авторизации и не сохраняет транзакцию. Не выводите verifier в журналы или URL. Свяжите его с транзакцией, начатой в данном браузере, и удалите после обмена или истечения срока.

 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
function base64url(bytes) {
    return btoa(String.fromCharCode(...bytes))
        .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}

function generateCodeVerifier() {
    const bytes = new Uint8Array(32);
    crypto.getRandomValues(bytes);
    return base64url(bytes);
}

async function generateCodeChallenge(codeVerifier) {
    if (typeof codeVerifier !== 'string' ||
        codeVerifier.length < 43 || codeVerifier.length > 128 ||
        /[^A-Za-z0-9._~-]/.test(codeVerifier)) {
        throw new TypeError('Invalid PKCE verifier');
    }
    const data = new TextEncoder().encode(codeVerifier);
    const digest = await crypto.subtle.digest('SHA-256', data);
    return base64url(new Uint8Array(digest));
}

async function generatePkce() {
    const code_verifier = generateCodeVerifier();
    return {
        code_verifier,
        code_challenge: await generateCodeChallenge(code_verifier),
        code_challenge_method: 'S256'
    };
}

Код использует все биты 32 случайных байт. Старый пример соединял только последние две шестнадцатеричные цифры каждого из 28 случайных 32-битных целых чисел: получал 896 бит, но сохранял лишь 224. Это усечение исправлено.

3. OIDC и ID-токены

3.1 Слой аутентификации поверх OAuth

В OIDC клиент (Relying Party) запрашивает аутентификацию с областью openid, а OpenID Provider (OP) возвращает результат в ID-токене. Пользователь не обязательно вводит пароль каждый раз: OP может использовать существующий сеанс или другой способ аутентификации.

3.2 Разделение назначения токенов

  • Токен доступа — учётные данные для обращения к серверу ресурсов. Это может быть JWT или непрозрачная строка; OAuth 2.0 не задаёт единого формата. Клиент должен следовать требованиям API, не предполагая внутренней структуры токена.
  • ID-токен — JWT с результатом аутентификации, предназначенным клиенту. До использования проверьте подпись, утверждения и привязку к запросу аутентификации. Одного декодирования недостаточно; отправлять его вместо токена доступа к API тоже нельзя.

3.3 Структура JWT и подписи

Здесь рассматривается JWS Compact Serialization для подписанного, но не зашифрованного JWT: три части, разделённые точками. JWT также может быть зашифрован как JWE или иметь вложенную структуру, поэтому не всегда состоит из трёх частей.

  1. Header (заголовок) — поля, например алгоритм подписи alg и идентификатор ключа kid.
  2. Payload (полезная нагрузка) — утверждения об издателе, получателях, времени истечения срока, субъекте и других данных.
  3. Signature (подпись) — подпись или MAC, созданные указанным алгоритмом. Рассматриваемый RS256 использует цифровую подпись RSA.

Заголовок и полезная нагрузка лишь закодированы в Base64url. Подпись не скрывает их содержимое. См. также различия между шифрованием, хешированием и подписью.

Основные обязательные утверждения ID-токена

  • iss — идентификатор издателя OP. Он должен точно совпадать с ожидаемым издателем.
  • sub — уникальный в пределах этого издателя идентификатор пользователя, который никогда не переназначается. При работе с несколькими OP идентифицируйте пользователя по сочетанию iss и sub.
  • aud — предполагаемые получатели. Убедитесь, что включён client_id клиента, и не принимайте дополнительных получателей, которым клиент не доверяет.
  • exp — время истечения срока. Текущее время должно быть раньше него.
  • iat — время выпуска. Политика клиента определяет допустимый возраст токена и другие ограничения.

Если в запросе аутентификации отправлялся nonce, требуйте наличия этого утверждения и проверяйте значение. Дополнительные утверждения или max_age также требуют соответствующих проверок.

Входные данные подписи RS256

Пусть H — защищённый заголовок в UTF-8, P — полезная нагрузка в UTF-8, а B64u — Base64url без заполнения. Подписывается следующая последовательность байт; || означает конкатенацию.

\[ M=\operatorname{ASCII}\!\left(\operatorname{B64u}(H)\,||\,\texttt{.}\,||\,\operatorname{B64u}(P)\right),\qquad S=\operatorname{Sign}_{\mathrm{RS256}}(sk,M) \]

RS256 использует RSASSA-PKCS1-v1_5 с SHA-256. Хеширование является частью этого алгоритма подписи; формула не добавляет ещё один внешний хеш. Третья часть токена — B64u(S). Проверяйте подпись открытым ключом доверенного OP с заданным алгоритмом. Нельзя в общем случае описывать проверку подписи как расшифровку открытым ключом.

Непроверенный токен не должен определять разрешённые алгоритмы или источник ключей. Используйте доверенный JWKS из проверенной конфигурации или процедуры Discovery OP и обрабатывайте ротацию и кеширование ключей. kid служит для выбора внутри этого доверенного набора, а не для загрузки ключей по произвольному URL из токена.

Частичный пример проверки на Python

Функция PyJWT ниже поддерживает ограниченную конфигурацию: RS256, отсутствие шифрования, единственный получатель в виде строки и nonce, уже отправленный в запросе. Вызывающий код передаёт в signing_key открытый ключ из JWKS доверенного OP. Обязательные утверждения должны присутствовать и иметь допустимые значения. Если присутствует azp, в этой конфигурации оно должно совпадать с client_id.

Эта функция не завершает вход. Вызывающий код должен проверять и поддерживать связь обратного вызова с исходной транзакцией, state, обмен кода, ожидаемого издателя и однократное использование nonce. Несколько получателей, шифрование, дополнительные требования аутентификации, max_age и acr находятся вне рамок примера.

 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
import hmac
import math
import jwt


def verify_id_token(id_token, signing_key, *, issuer, client_id, expected_nonce):
    if not isinstance(expected_nonce, str) or not expected_nonce:
        raise ValueError('A nonce from the stored transaction is required')
    claims = jwt.decode(
        id_token,
        signing_key,
        algorithms=['RS256'],
        audience=client_id,
        issuer=issuer,
        options={
            'require': ['iss', 'sub', 'aud', 'exp', 'iat', 'nonce'],
            'strict_aud': True,
        },
    )
    for name in ('exp', 'iat'):
        value = claims[name]
        if (isinstance(value, bool) or not isinstance(value, (int, float))
                or (isinstance(value, float) and not math.isfinite(value))):
            raise jwt.InvalidTokenError('Invalid NumericDate')
    subject = claims['sub']
    if (not isinstance(subject, str) or not subject or not subject.isascii()
            or len(subject) > 255):
        raise jwt.InvalidTokenError('Invalid subject')
    nonce = claims['nonce']
    if (not isinstance(nonce, str) or not hmac.compare_digest(
            nonce.encode('utf-8'), expected_nonce.encode('utf-8'))):
        raise jwt.InvalidTokenError('Nonce mismatch')
    if 'azp' in claims and claims['azp'] != client_id:
        raise jwt.InvalidTokenError('Authorized party mismatch')
    return claims

Функция возвращает утверждения, проверенные в этих пределах. Не объявляйте аутентификацию успешной только по корректной подписи и не связывайте учётные записи по непроверенным имени или адресу электронной почты.

4. Вопросы безопасности

4.1 state, nonce, PKCE и CSRF

Если используется state, безопасно свяжите непредсказуемое значение с транзакцией, начатой в браузере. При обратном вызове проверьте значение, срок действия и то, что оно ещё не использовано, затем пометьте его использованным. Совпадение строк само по себе не предотвращает все виды CSRF.

RFC 9700 также описывает возможность полагаться на CSRF-защиту PKCE после подтверждения поддержки сервером и применение nonce OIDC. Эти меры по-прежнему требуют привязки к конкретной транзакции и корректной проверки. state, nonce и PKCE выполняют разные функции; постоянные значения или пропущенные сравнения не дают ожидаемой защиты. Несколько серверов авторизации также требуют защиты от mix-up.

4.2 Истечение срока и токены обновления

Короткий срок действия токена доступа помогает ограничить время использования украденного токена. Значение вроде 15 минут — пример проектного решения, а не универсальное требование. При проверке exp подписанного JWT условие проверки срока имеет вид:

\[ T_{\mathrm{now}}<T_{\mathrm{exp}},\qquad T_{\mathrm{exp}}=T_{\mathrm{iat}}+D_{\mathrm{lifetime}}\ \text{только при таком определении}\ T_{\mathrm{now}}<T_{\mathrm{iat}}+D_{\mathrm{lifetime}} \]

Это проверка только срока действия, а не доказательство полной действительности токена или прав доступа. Также проверяйте алгоритм, ключ, издателя, получателей и, когда применимо, nbf, статус отзыва и связь с запросом аутентификации. Явно управляйте допустимым расхождением часов, если оно предусмотрено.

Выдача токенов обновления необязательна. Без них клиенту может потребоваться новая транзакция авторизации. Для токенов обновления публичных клиентов RFC 9700 требует привязки к отправителю (sender constraints) или ротации. Эта привязка криптографически связывает токен с предъявляющим его клиентом. Отзыв токена обновления не обязательно немедленно делает недействительными уже выданные токены доступа.

4.3 Выбор потока и реализации

Для веб-приложений, SPA и нативных приложений с участием пользователя исходите из кода авторизации с PKCE. Выбирайте конфигурацию с учётом типа клиента, хранения токенов, поддержки провайдера и модели угроз. Не навязывайте тот же поток другим задачам, например M2M с учётными данными клиента.

Заключение

Используйте токены доступа OAuth 2.0 для обращения к ресурсам, а ID-токены OIDC проверяйте как результаты аутентификации для клиента. Проверяйте привязку кода к PKCE, подпись, обязательные утверждения, nonce и связь обратного вызова с исходной транзакцией.

Источники

comments powered by Disqus