> ## Documentation Index
> Fetch the complete documentation index at: https://veriqa.app/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Быстрый старт Node.js

> Подключение Node.js-приложения к Veriqa по стандартному OpenID Connect — облако, Docker или служба ОС.

Для не-.NET стеков Veriqa — обычный OpenID Connect провайдер. Пакета Veriqa для Node.js нет и не
требуется: вы пишете стандартного OIDC-клиента, а всю специфику (страница входа, QR, подтверждение
в мессенджере) Veriqa выполняет на своей стороне.

<Note>
  **Один код, любое развёртывание.** Приложению не важно, **где** работает Veriqa — в облаке Veriqa,
  в вашем контейнере Docker или службой (systemd либо служба Windows) на вашей машине. Варианты
  различаются **только адресом issuer'а** и тем, где заведён клиент; код приложения ниже одинаков
  для всех, а переключение сводится к одной переменной окружения.
</Note>

## 1. Установите пакеты

Рекомендуемая OIDC-библиотека — [`openid-client`](https://github.com/panva/openid-client)
(сертифицирована OpenID Foundation, актуальная линия **6.x**). Ни веб-фреймворка, ни хранилища
сессий она не тянет, а коду ниже нужно и то, и другое:

```bash theme={null}
npm install openid-client express express-session
```

`express` — фреймворк, под который написаны маршруты; `express-session` даёт им `req.session` — то
место, где PKCE-`code_verifier` и `state` ждут, пока пользователь подтверждает вход. Без любого из
пакетов первый же запуск падает на строке импорта с `ERR_MODULE_NOT_FOUND`, ещё не дойдя до Veriqa.

<Warning>
  Требуется **Node.js 20 или новее** — это минимум, объявленный библиотекой. Линия 6.x — полная
  переработка API: примеры для 5.x (`Issuer.discover`, `client.callback`) к ней **не подходят**.
</Warning>

<Note>
  **Пример — ESM.** `config.js` выполняет discovery через `await` на верхнем уровне модуля, а CommonJS
  так не умеет. Пропишите `"type": "module"` в `package.json` — иначе `node server.js` остановится на
  `Cannot use import statement outside a module`.
</Note>

## 2. Заведите клиента на стороне Veriqa

<Note>
  **Veriqa Cloud сейчас недоступен** — строки про облако описывают контракт его API. Используйте
  self-hosted; подробности — в [Veriqa Cloud](/docs/ru/cloud/overview).
</Note>

| Режим | Где заводится клиент | Что получаете |
| - | - | - |
| Облако | Раздел **Applications / OIDC clients** консоли. Вместе с проектом уже создано дефолтное приложение — **public-клиент с PKCE, без секрета и без `redirect_uri`**, так что первым делом добавьте свой `redirect_uri`. Адрес `issuer` — на экране Project settings, read-only | `issuer`, `client_id`, `redirect_uri` |
| Self-hosted | Секция `Veriqa:OpenIddict:Clients` в конфигурации сервера — см. [шаг 4 self-hosted quickstart](/docs/ru/quickstart/self-hosted#4-объявите-свой-клиент). Секция та же, как бы сервер ни был поставлен | То же, значения задаёте сами |

**У self-hosted две поставки, и для этой страницы они равнозначны.** Сервер работает в контейнере
Docker — [быстрый старт self-hosted](/docs/ru/quickstart/self-hosted) — либо ставится из архива юнитом
systemd или службой Windows, без Docker и без рантайма .NET на машине —
[установка службой ОС](/docs/ru/quickstart/os-service). Выбор определяет способ поставки сервера, а не
регистрацию клиента, адрес issuer'а и код ниже.

Что бы вы ни выбрали, у клиента должны быть объявлены разрешённые redirect URI (сверка **точным**
совпадением, wildcard не поддерживаются) и разрешённые scopes — включая `channel`, если приложению
нужны claims канала (шаг 7).

`redirect_uri` сверяется буквально, поэтому регистрируйте ровно тот URL, который обслуживает ваш
callback-маршрут. Для сервера из [шага 6](#6-соберите-сервер), запущенного как в
[шаге 9](#9-контрольная-точка), это `http://127.0.0.1:3000/callback` — зарегистрируйте именно его и
открывайте приложение в той же форме хоста (`127.0.0.1`, а не `localhost`).

**Секрет не обязателен.** Минимальный рабочий набор — `issuer`, `client_id`, `redirect_uri`:
public-клиент с PKCE, и это дефолт облака. Confidential-клиент нужен, только если приложение
серверное и вы хотите аутентифицировать его секретом; в облаке секрет выпускается отдельным явным
действием на экране Applications и показывается **один раз** — сохраните сразу.

Значения кладите в окружение, не в код. Задайте их в той оболочке, из которой будете запускать
приложение: первые две — то, что дал шаг 2, две последние — ваши:

```bash theme={null}
export VERIQA_ISSUER="https://auth.your-domain.com"      # адрес issuer'а
export VERIQA_CLIENT_ID="my-app"                         # заведённый вами клиент
export VERIQA_CLIENT_SECRET="…"                          # только для confidential-клиента
export APP_REDIRECT_URI="http://127.0.0.1:3000/callback"
export SESSION_SECRET="$(openssl rand -hex 32)"
```

`SESSION_SECRET` Veriqa не выдаёт — это ваша собственная случайная строка, ключ, которым
`express-session` подписывает cookie сессии. Держите его вне системы контроля версий и в каждом
окружении задавайте свой. Public-варианту `VERIQA_CLIENT_SECRET` не нужен вовсе: у public-клиента
секрета нет. Пропустите переменную, которую выбранный вариант читает, — запуск упадёт ещё на импорте
`config.js`: `new URL(undefined)` бросает `ERR_INVALID_URL`, ни один маршрут обслужен не будет.

Файл `.env` тоже подойдёт, если он вам привычнее: Node.js 20.6 и новее читает его командой
`node --env-file=.env server.js`.

Переключение между вариантами — облако, Docker, служба ОС — это правка одного
`VERIQA_ISSUER`.

<Warning>
  В облаке `issuer` производен от slug проекта. Смена slug меняет issuer и **ломает существующие
  интеграции** — это отдельное действие владельца в Dangerous zone, а не рутинная правка настроек.
</Warning>

## 3. Выполните discovery

Клиент собирается один раз при старте процесса: библиотека сама читает
`/.well-known/openid-configuration` и запоминает эндпоинты.

<CodeGroup>
  ```js config.js — public-клиент (PKCE) theme={null}
  import * as client from 'openid-client'

  const issuer = new URL(process.env.VERIQA_ISSUER)

  // Четвёртый аргумент — способ аутентификации клиента.
  // У public-клиента её нет: на token-эндпоинт уходит только client_id.
  export const config = await client.discovery(
    issuer,
    process.env.VERIQA_CLIENT_ID,
    undefined,
    client.None(),
  )
  ```

  ```js config.js — confidential-клиент theme={null}
  import * as client from 'openid-client'

  const issuer = new URL(process.env.VERIQA_ISSUER)

  export const config = await client.discovery(
    issuer,
    process.env.VERIQA_CLIENT_ID,
    process.env.VERIQA_CLIENT_SECRET,
  )
  ```
</CodeGroup>

<Note>
  Первый аргумент — объект `URL`, а не строка. Если discovery отдаёт `http`-адреса эндпоинтов при
  доступе по `https`, дело не в клиенте: self-hosted сервер стоит за прокси без доверенных
  заголовков — см. [шаг 6 self-hosted quickstart](/docs/ru/quickstart/self-hosted#6-поставьте-за-reverse-proxy).
</Note>

## 4. Начните вход

PKCE обязателен, `state` — защита от CSRF. Оба значения нужно **сохранить в сессии** до возврата
пользователя: они понадобятся в callback.

```js login.js theme={null}
import * as client from 'openid-client'
import { config } from './config.js'

export async function startLogin(req, res) {
  const code_verifier = client.randomPKCECodeVerifier()
  const code_challenge = await client.calculatePKCECodeChallenge(code_verifier)
  const state = client.randomState()

  // Сохраните оба значения в сессии пользователя — в callback они обязательны
  req.session.code_verifier = code_verifier
  req.session.state = state

  const parameters = {
    redirect_uri: process.env.APP_REDIRECT_URI,
    scope: 'openid profile channel',
    code_challenge,
    code_challenge_method: 'S256',
    state,
  }

  res.redirect(client.buildAuthorizationUrl(config, parameters).href)
}
```

Дальше работает Veriqa: пользователь видит страницу входа с QR и списком каналов, подтверждает
вход в мессенджере на телефоне, после чего браузер возвращается на ваш `redirect_uri` с кодом.

## 5. Обработайте возврат

```js callback.js theme={null}
import * as client from 'openid-client'
import { config } from './config.js'

export async function handleCallback(req, res) {
  const currentUrl = new URL(req.url, process.env.APP_REDIRECT_URI)

  const tokens = await client.authorizationCodeGrant(config, currentUrl, {
    pkceCodeVerifier: req.session.code_verifier,
    expectedState: req.session.state,
  })

  const claims = tokens.claims()          // разобранный id_token
  req.session.user = {
    sub: claims.sub,
    name: claims.name,
    channel_type: claims.channel_type,    // только при scope channel (шаг 7)
  }

  res.redirect('/')
}
```

`authorizationCodeGrant` сам обменивает код на токены и валидирует ответ; сверка `state` — через
`expectedState`, повторно проверять его руками не нужно. Метод `claims()` возвращает разобранный
`id_token` либо `undefined`, если сервер его не выдал.

`/` — то, куда callback в итоге приводит браузер: этот маршрут добавляет
[шаг 6](#6-соберите-сервер).

## 6. Соберите сервер

Три модуля выше — обработчики, а не приложение: кто-то должен дать им `req.session`, повесить их на
маршруты и слушать порт. Это тот самый файл, который вы и запускаете.

```js server.js theme={null}
import express from 'express'
import session from 'express-session'

import { startLogin } from './login.js'
import { handleCallback } from './callback.js'

const app = express()

// express-session и ставит на место req.session — PKCE-code_verifier и state живут
// там, пока пользователь подтверждает вход на телефоне. Хранилище по умолчанию держит
// сессии в памяти одного процесса: для прохода по этой странице достаточно, для
// продакшена — нет.
app.use(session({
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: false,
}))

app.get('/login', startLogin)
app.get('/callback', handleCallback)

app.get('/', (req, res) => {
  if (req.session.user === undefined) {
    res.redirect('/login')
    return
  }

  res.json(req.session.user)
})

app.listen(3000, '127.0.0.1')
```

Пути маршрутов здесь — контракт страницы: на `/callback` указывает `APP_REDIRECT_URI`, и его же вы
зарегистрировали в [шаге 2](#2-заведите-клиента-на-стороне-veriqa), а `/` показывает результат входа —
пока в сессии пусто, он отправляет на `/login`, а после завершённого входа отдаёт сохранённые claims
как JSON, так что итог виден без шаблонов.

## 7. Прочитайте claims

Базовый набор Veriqa кладёт в токены сама:

| Claim | Значение | Где |
| - | - | - |
| `sub` | Стабильный идентификатор субъекта | оба токена |
| `auth_time` | Момент завершения подтверждения (Unix) | только `id_token` |
| `amr` | Тип канала, которым подтверждён вход (`telegram`, `whatsapp`, `max`, `email`) | оба токена; в `id_token` — массивом строк (`["telegram"]`), даже при одном методе |
| `name`, `email`, `phone_number`, … | Из resolved identity — по запрошенным scopes (`profile`, `email`, `phone`) | оба токена |
| `picture` | Аватар пользователя — только со scope `avatar`, разрешённым и в `AllowedScopes` ([подробнее](/docs/ru/concepts/oidc-explainer#resolved-identity-и-claims)) | только `access_token` — читать из userinfo |

**Claims канала выдаются только по scope `channel`:**

| Claim | Значение |
| - | - |
| `channel_type` | Тип канала, к которому привязан пользователь |
| `channel_user_id` | Идентификатор пользователя внутри этого канала |

Без запрошенного scope `channel` эти два claim не попадают **ни в один** токен — и, следовательно,
не появляются в ответе userinfo, который зеркалит access\_token. Именно по ним приложение различает
привязанные каналы, поэтому для сценария привязки аккаунта scope обязателен.

```js theme={null}
const userinfo = await client.fetchUserInfo(config, tokens.access_token, claims.sub)
// userinfo.channel_type / userinfo.channel_user_id — присутствуют, только если запрошен scope channel
```

Третий аргумент — ожидаемый `sub`: библиотека сверяет его с ответом и не даст подменить субъекта.

## 8. Ограничьте вход конкретным каналом

Стандартный OIDC-параметр `acr_values` формата `channel:{тип}` сужает выбор на странице входа:

```js theme={null}
const parameters = {
  redirect_uri: process.env.APP_REDIRECT_URI,
  scope: 'openid profile',
  acr_values: 'channel:telegram',
  code_challenge,
  code_challenge_method: 'S256',
  state,
}
```

Несколько значений через пробел (`channel:telegram channel:whatsapp`) — пользователь выбирает из
перечисленных. Без параметра доступны все включённые на сервере каналы.

## 9. Контрольная точка

Запустите приложение из каталога с `server.js`, в той оболочке, где экспортированы переменные
[шага 2](#2-заведите-клиента-на-стороне-veriqa):

```bash theme={null}
node server.js
```

Оно поднимается на `http://127.0.0.1:3000` — том адресе, из которого собран `APP_REDIRECT_URI`.

<Steps>
  <Step title="Проверьте discovery">
    `curl $VERIQA_ISSUER/.well-known/openid-configuration` возвращает метаданные с `https`-URL.
  </Step>

  <Step title="Запустите вход">
    Откройте `http://127.0.0.1:3000/login` — браузер должен уйти на страницу Veriqa с QR и каналами.
  </Step>

  <Step title="Подтвердите на телефоне">
    Подтвердите запрос в доверенном канале; браузер вернётся на `/callback`, а тот перенаправит его
    на `/`.
  </Step>

  <Step title="Проверьте claims">
    `/` отдаёт сессию как JSON: `sub` заполнен, а `channel_type` несёт канал, которым подтверждён
    вход, — его не будет, если scope `channel` не запрашивался.
  </Step>
</Steps>

## Дальше

<CardGroup cols={2}>
  <Card title="Сценарии интеграции" icon="route" href="/docs/ru/guides/scenarios">
    Привязка канала, повторное подтверждение и подтверждение действия — на уровне HTTP.
  </Card>

  <Card title="Быстрый старт: self-hosted" icon="server" href="/docs/ru/quickstart/self-hosted">
    Развернуть свой issuer в Docker.
  </Card>

  <Card title="Установка службой ОС" icon="server" href="/docs/ru/quickstart/os-service">
    Тот же issuer без Docker — systemd или служба Windows, из архива.
  </Card>

  <Card title="OIDC + Veriqa: разбор" icon="key" href="/docs/ru/concepts/oidc-explainer">
    Где Veriqa встаёт в стандартный поток.
  </Card>

  <Card title="Hardening к продакшну" icon="shield-check" href="/docs/ru/guides/hardening">
    Что проверить перед выпуском.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.