Перейти к содержимому
CalabДокументация

Поток входа (Authorization Code + PKCE)

Пошаговый вход «через Calab» — от кнопки в вашем приложении до проверенного ID token и профиля.

Calab поддерживает один поток — Authorization Code с PKCE S256. Он одинаков для всех типов клиентов; разница только в том, как клиент аутентифицируется на /token.

В примерах:

ЗначениеПример
Issuer пространстваhttps://id.example.com/oidc/workspaces/0192a000-0000-7000-8000-0000000000aa
client_idcalab_client_e07JrYTVpGfRYJ0pQaF6yzkME6S9W856DgsKNWOKvgE
Секрет (только веб с сервером)calab_os_VWMV4RGIRsisgKhdE7ohFW_QoAdNWS4n9GkM3Ty1kb4
redirect_urihttps://crm.example.com/auth/calab/callback

Схема

Последовательность входа через Calab: браузер, приложение, /authorize, экран согласия, /token, /userinfo Браузер Приложение /authorize Согласие /token /userinfo 1. «Войти через Calab» 2. 302 → /authorize 3. GET /authorize (PKCE, state, nonce) 4. 303 → согласие 5. вход в Calab и «Разрешить» 6. 303 → redirect_uri?code&state&iss 7. code, state, iss 8. POST /token: code + code_verifier 9. access_token, id_token, refresh_token проверить state, iss, подпись и nonce в id_token 10. GET /userinfo, Authorization: Bearer access_token 11. sub, name, email

Сплошные стрелки — запросы, пунктир — ответы и перенаправления. Шаги 3–6 проходят в браузере пользователя; шаги 8–11 — сервер-сервер (для SPA и нативных приложений — прямо из клиента).

Шаг 1. Подготовьте PKCE, state и nonce

Для каждой попытки входа сгенерируйте и сохраните в сессии вашего приложения:

  • code_verifier — 43–128 символов из A–Z a–z 0–9 - . _ ~ (обычно 32 случайных байта в base64url);
  • code_challenge = BASE64URL(SHA-256(code_verifier)) без = — ровно 43 символа;
  • state — случайная строка 1–512 байт, защита от CSRF;
  • nonce — случайная строка 1–512 байт, привязывает ID token к этой попытке.
# Пример генерации в shell (openssl ≥ 1.1)
CODE_VERIFIER=$(openssl rand -base64 32 | tr '+/' '-_' | tr -d '=')
CODE_CHALLENGE=$(printf '%s' "$CODE_VERIFIER" | openssl dgst -sha256 -binary | openssl base64 | tr '+/' '-_' | tr -d '=')
STATE=$(openssl rand -hex 16)
NONCE=$(openssl rand -hex 16)

Шаг 2. Отправьте пользователя на /authorize

Перенаправьте браузер на authorization_endpoint:

text
https://id.example.com/oidc/workspaces/0192a000-0000-7000-8000-0000000000aa/authorize
  ?response_type=code
  &client_id=calab_client_e07JrYTVpGfRYJ0pQaF6yzkME6S9W856DgsKNWOKvgE
  &redirect_uri=https%3A%2F%2Fcrm.example.com%2Fauth%2Fcalab%2Fcallback
  &scope=openid%20profile%20email
  &state=4f1c2a…
  &nonce=9b7e30…
  &code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
  &code_challenge_method=S256
ПараметрОбязателенЗначение
response_typeдаcode
client_idдаИз настроек клиента
redirect_uriдаТочно один из зарегистрированных
scopeдаopenid и по желанию profile, email — не шире, чем разрешено клиенту
state, nonceда1–512 байт
code_challengeда43 символа base64url
code_challenge_methodдаS256 (plain не поддерживается)
response_modeнетТолько query
promptнетlogin, consent, их сочетание или none (только отдельно)
max_ageнетСекунды; требует, чтобы вход в Calab был не старше

Каждый параметр — ровно один раз. request, request_uri, claims, id_token_hint, resource и audience отклоняются с invalid_request; acr_values принимается, но игнорируется. Можно отправить те же параметры формой POST (application/x-www-form-urlencoded, без query-строки).

Шаг 3. Пользователь входит и соглашается

Calab перенаправляет браузер на экран согласия по адресу identity (https://id.example.com/oauth/consent?request=…). Если человек не вошёл в Calab — сначала обычный вход; если пространство требует SSO — подтверждение через корпоративный IdP. Затем экран «Разрешить вход в приложение?» с названием приложения, пространством, именем аккаунта и запрошенными данными. Подробности — Экран согласия.

Запрос на вход живёт 10 минут.

Шаг 4. Примите ответ на redirect_uri

Успех:

text
https://crm.example.com/auth/calab/callback
  ?code=calab_oc_Laufm1V1F0gnbG0nHG4NcwvsM6wHkhQOmk4AH8CzCSw
  &state=4f1c2a…
  &iss=https%3A%2F%2Fid.example.com%2Foidc%2Fworkspaces%2F0192a000-0000-7000-8000-0000000000aa

Отказ или ошибка — тот же адрес с error вместо code (например, error=access_denied), тоже со state и iss.

Обязательно проверьте:

  1. state совпадает с сохранённым в сессии;
  2. iss равен issuer этого пространства (защита от подмены провайдера, RFC 9207).

Шаг 5. Обменяйте code на токены

POST на token_endpoint, тело — application/x-www-form-urlencoded, без query-строки:

ПараметрЗначение
grant_typeauthorization_code
codeИз шага 4
redirect_uriТот же, что в шаге 2, байт в байт
code_verifierИз шага 1
client_idТолько для публичных клиентов (SPA, нативные)

Конфиденциальный клиент аутентифицируется только заголовком Authorization: Basic base64(client_id:secret) (обе части предварительно URL-кодируются по RFC 6749). client_secret в теле и client_assertion не принимаются (invalid_client). Параметр scope в обмене кода запрещён.

curl -s https://id.example.com/oidc/workspaces/0192a000-0000-7000-8000-0000000000aa/token \
  -u 'calab_client_e07JrYTVpGfRYJ0pQaF6yzkME6S9W856DgsKNWOKvgE:calab_os_VWMV4RGIRsisgKhdE7ohFW_QoAdNWS4n9GkM3Ty1kb4' \
  -d grant_type=authorization_code \
  -d code=calab_oc_Laufm1V1F0gnbG0nHG4NcwvsM6wHkhQOmk4AH8CzCSw \
  --data-urlencode redirect_uri=https://crm.example.com/auth/calab/callback \
  -d code_verifier="$CODE_VERIFIER"

Для публичного клиента уберите заголовок Authorization и добавьте client_id в тело.

Ответ:

JSON
{
  "access_token": "calab_oa_TvZdRRrAeU21FG2au71ffTJmnZjhaX536xpn9Kw1S9o",
  "token_type": "Bearer",
  "expires_in": 300,
  "scope": "email openid profile",
  "refresh_token": "calab_or_g7KMt03Mx5dxElMQi0aw3v-eG08Xrqf6Pv9QQNDSjZQ",
  "id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6Ik…"
}

refresh_token есть только если у клиента включено «Разрешить продолжение доступа» и пользователь согласился на это. Код одноразовый и живёт 60 секунд; повторное предъявление уже использованного кода тем же клиентом отзывает всё, что было по нему выдано.

Шаг 6. Проверьте ID token

ID token — JWT, подписанный RS256. Возьмите ключ по kid из jwks_uri и проверьте:

ClaimПроверка
issРавен issuer пространства
audРавен вашему client_id
exp, iatТокен не истёк (живёт столько же, сколько access token — до 5 минут)
nonceРавен сохранённому в шаге 1
auth_timeКогда человек на самом деле входил — используйте с max_age

Пример полезной нагрузки:

JSON
{
  "iss": "https://id.example.com/oidc/workspaces/0192a000-0000-7000-8000-0000000000aa",
  "sub": "7qCZMpOp2e-PYx7A9VRiAGUu8o-MF1kgOZG0umGs_HM",
  "aud": "calab_client_e07JrYTVpGfRYJ0pQaF6yzkME6S9W856DgsKNWOKvgE",
  "iat": 1792141200,
  "exp": 1792141500,
  "auth_time": 1792140900,
  "nonce": "9b7e30…",
  "name": "Анна Петрова",
  "email": "anna@example.com",
  "email_verified": true
}

Идентифицируйте человека по паре (iss, sub), а не по email: sub постоянен для участника в этом пространстве, email может поменяться или отсутствовать. acr и amr Calab не выдаёт.

import { createRemoteJWKSet, jwtVerify } from 'jose';

const jwks = createRemoteJWKSet(new URL(`${issuer}/jwks`));
const { payload } = await jwtVerify(tokens.id_token, jwks, {
  issuer,
  audience: process.env.CALAB_CLIENT_ID,
  algorithms: ['RS256'],
});
if (payload.nonce !== session.nonce) throw new Error('nonce mismatch');
const accountKey = `${payload.iss}|${payload.sub}`;

Шаг 7 (необязательно). Запросите профиль

Если нужны свежие данные — GET /userinfo с access token. Подробно и про продление доступа — UserInfo, refresh и revoke.

Тихий повторный вход

prompt=none выдаёт код без интерфейса, если у человека есть живая сессия Calab в этом браузере и он уже давал согласие этому клиенту на те же scopes. Иначе Calab сразу вернётся на redirect_uri с одной из ошибок: login_required, consent_required или interaction_required — и тогда запустите обычный вход.

Готовые библиотеки

Calab следует OIDC Discovery, поэтому обычно достаточно указать issuer пространства, client_id, секрет и redirect URI. Убедитесь, что библиотека: включает PKCE S256, отправляет nonce, для веб-клиента использует client_secret_basic и проверяет параметр iss в ответе авторизации.