---
title: "Получение первого токена | FoxAuth"
description: "Authorization Code с PKCE вручную через curl: от challenge до декодированного ID-токена и вызова UserInfo."
canonical: "https://foxauth.dev/ru/docs/get-started/first-token/"
lastmod: "2026-10-08T16:42:48+03:00"
---
Получение первого токена

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

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

Заметка

Пути эндпоинтов ниже — значения по умолчанию этого сервера. Не зашивайте их в код настоящего клиента.
Вместо этого прочитайте GET /.well-known/openid-configuration и используйте то, что там объявлено.
Этот документ также отражает, какие возможности включены, поэтому он и есть честный ответ на вопрос
«что поддерживает это развёртывание».
Сейчас это важнее, чем раньше. Если проект вашего клиента указывает на созданный вами бакет, этот
бакет — отдельный издатель (issuer): его эндпоинты находятся под <ISSUER>/<slug>, а
discovery-документ — по адресу <ISSUER>/<slug>/.well-known/openid-configuration. Запрос для такого
клиента, отправленный на корневой /auth, отклоняется с unauthorized_client — «client is not
authorized at this address», — потому что клиент можно использовать только по адресу бакета, которому
он принадлежит. В пошаговом разборе ниже используется бакет по умолчанию redfox, который
обслуживается в корне.

Сгенерируйте 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 вы отправите его на эндпоинт токенов.

Отправьте браузер на эндпоинт авторизации. Соберите 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.

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

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

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

Обменяйте код на эндпоинте токенов. Код одноразовый и живёт 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"

}

Декодируйте 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.

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

Окно терминала

curl -s http://localhost:3000/userinfo -H 'Authorization: Bearer <access_token>' | jq .

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

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

Экран входа, который показывается для бакета, на который указывает проект клиента.

Экран согласия перечисляет то, что было запрошено. `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.

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

ПредыдущаяРегистрация первого клиентаСледующаяЗащита MCP-сервера с помощью OAuth
