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

Получение первого токена

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

В примерах предполагается ISSUER=http://localhost:3000 и публичный клиент с id demo, у которого зарегистрирован redirect URI http://localhost:8080/callback. Подставьте свои значения со страницы Регистрация первого клиента.

  1. Сгенерируйте verifier и challenge. PKCE здесь обязателен для каждого запроса с response_type=code, а единственный принимаемый метод — S256.

    // save as pkce.ts, run with: bun pkce.ts
    const verifier = Buffer.from(
    crypto.getRandomValues(new Uint8Array(32))
    ).toString('base64url');
    const challenge = new Bun.CryptoHasher('sha256')
    .update(verifier)
    .digest('base64url');
    console.log({ verifier, challenge });

    Сохраните verifier: на шаге 4 вы отправите его на эндпоинт токенов.

  2. Отправьте браузер на эндпоинт авторизации. Соберите URL со своим challenge:

    http://localhost:3000/auth
    ?response_type=code
    &client_id=demo
    &redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Fcallback
    &scope=openid
    &state=8f2c1d
    &nonce=b41e07
    &code_challenge=<challenge>
    &code_challenge_method=S256

    (Всё в одну строку, без отступов.) state вы проверяете сами на обратном пути; nonce возвращается в ID-токене, чтобы вы могли связать его с этим запросом. Именно scope=openid делает запрос запросом OpenID Connect и приводит к выдаче ID-токена. Для refresh-токена нужны ещё offline_access в scope (области доступа) и prompt=consent в этом URL, и refresh_token среди grant types клиента. У клиента, созданного в консоли, по умолчанию есть только authorization_code. Если не выполнено хотя бы одно из трёх условий, scope молча отбрасывается, без ошибки: вы получаете ответ с токенами, в котором нет refresh_token. Кроме того, сервер должен включать offline_access в список поддерживаемых scope, как он и делает по умолчанию; пока это не так, эндпоинт токенов отклоняет любой refresh-токен с unsupported_grant_type.

  3. Войдите и дайте согласие. Сервер перенаправляет на собственные экраны взаимодействия: форму входа для бакета, на который указывает проект этого клиента, а затем экран согласия со списком scope (для нового клиента согласие включено по умолчанию). Подтверждение на обоих экранах отправляет браузер на

    http://localhost:8080/callback?code=<code>&state=8f2c1d&iss=http%3A%2F%2Flocalhost%3A3000

    На порту 8080 ничего не обязано слушать; скопируйте code из адресной строки браузера. Убедитесь, что state вернулся неизменным, а iss — это ваш issuer.

  4. Обменяйте код на эндпоинте токенов. Код одноразовый и живёт 60 секунд, так что сделайте это сразу. Если не успеете, начните заново с шага 2:

    Окно терминала
    curl -s http://localhost:3000/token \
    -H 'Content-Type: application/x-www-form-urlencoded' \
    -d grant_type=authorization_code \
    -d code=<code> \
    -d client_id=demo \
    -d redirect_uri=http://localhost:8080/callback \
    -d code_verifier=<verifier>

    redirect_uri должен побайтно совпадать с указанным на шаге 2 — при обмене кода сервер сравнивает его со значением, записанным вместе с кодом, и отклоняет несовпадение. Для конфиденциального клиента уберите client_id из тела и вместо этого пройдите аутентификацию: -u demo:<secret> для client_secret_basic или -d client_id=demo -d client_secret=<secret> для client_secret_post.

    Ответ — стандартный ответ эндпоинта токенов:

    {
    "access_token": "…",
    "token_type": "Bearer",
    "expires_in": 3600,
    "id_token": "…",
    "scope": "openid"
    }
  5. Декодируйте ID-токен. Это подписанный JWT; полезная нагрузка — средний сегмент в base64url:

    Окно терминала
    echo '<id_token>' | cut -d. -f2 | tr '_-' '/+' | base64 -d 2>/dev/null | jq .

    Сверьте iss с вашим ISSUER, aud — с id вашего клиента, а nonce — со значением из шага 2. Настоящий клиент также проверяет подпись ключом из GET /jwks, выбранным по kid токена. Полезная нагрузка сообщает и то, как пользователь вошёл: amr равен ["pwd"] для пароля или ["pwd", "otp", "mfa"] с одноразовым кодом, поэтому, чтобы требовать второй фактор, проверяйте наличие mfa, а не само наличие claim.

  6. Вызовите эндпоинт UserInfo с токеном доступа:

    Окно терминала
    curl -s http://localhost:3000/userinfo -H 'Authorization: Bearer <access_token>' | jq .

    Что вернётся, определяется выданными scope и claims, настроенными на сервере.

Два экрана, через которые проходит шаг 3, в развёртывании по умолчанию:

Экран входа сервера: поле имени пользователя, поле пароля, флажок «запомнить меня» и кнопка входа.
Экран входа, который показывается для бакета, на который указывает проект клиента.
Экран согласия с названием клиента и списком запрашиваемых им scope.
Экран согласия перечисляет то, что было запрошено. `offline_access` появляется, только если запрос также содержит `prompt=consent`.

Большинство эндпоинтов управляется флагом, и выключенный флаг означает, что эндпоинт не обслуживается вообще. Такой отказ намеренно неотличим от пути, которого у сервера нет. Поэтому 404 на одном из них — это ответ конфигурации, а не дефект:

  • Доступны всегда, без флага: /health, /.well-known/openid-configuration, /.well-known/security.txt, /jwks, /auth и /token. Всё в этом пошаговом разборе, кроме шага 6, относится к этой группе.
  • Включены, пока вы их не выключите: /userinfo (userinfo.enabled) и выход, инициированный RP (rpInitiatedLogout.enabled).
  • Выключены, пока вы их не включите: pushed authorization requests, интроспекция, отзыв токенов, динамическая регистрация клиентов, device flow, CIBA и управляющая плоскость MCP.

Полная таблица с флагом для каждого эндпоинта — в справочнике эндпоинтов; сами флаги — в справочнике настроек. Настройка вступает в силу, как только вы её сохраните.