<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Authentication on kenji.blog</title><link>http://kenji.blog/ru/tags/authentication/</link><description>Recent content in Authentication on kenji.blog</description><generator>Hugo -- gohugo.io</generator><language>ru</language><copyright>kenjinote</copyright><lastBuildDate>Sun, 13 Sep 2026 12:00:00 +0900</lastBuildDate><atom:link href="http://kenji.blog/ru/tags/authentication/index.xml" rel="self" type="application/rss+xml"/><item><title>Архитектура OAuth 2.0 в схемах: учимся на примере интеграции Slack App</title><link>http://kenji.blog/ru/p/oauth2-architecture-slack-app-integration/</link><pubDate>Sun, 13 Sep 2026 12:00:00 +0900</pubDate><guid>http://kenji.blog/ru/p/oauth2-architecture-slack-app-integration/</guid><description>&lt;img src="http://kenji.blog/p/oauth2-architecture-slack-app-integration/img/eyecatch.jpg" alt="Featured image of post Архитектура OAuth 2.0 в схемах: учимся на примере интеграции Slack App" />&lt;h1 id="введение-зачем-изучать-oauth-20">Введение: Зачем изучать OAuth 2.0?
&lt;/h1>&lt;p>В современных веб-приложениях совместная работа нескольких сервисов стала уже привычным делом. Например, такие функции, как «Вход через Google», «Отправка уведомления в Slack при обновлении задачи в Trello» или «Автоматическое добавление ссылки на встречу Zoom в Google Календарь». За всем этим стоит фреймворк авторизации &lt;strong>OAuth 2.0 (Open Authorization 2.0)&lt;/strong>.&lt;/p>
&lt;p>В прошлом при обмене данными между различными сервисами использовались такие методы, как «Базовая аутентификация» или «Обмен паролями», когда пользователь передавал свой логин и пароль стороннему сервису напрямую, что было крайне опасно. Этот метод давал стороннему сервису полный доступ к правам пользователя, создавая фатальные риски для безопасности.&lt;/p>
&lt;p>OAuth 2.0 был создан как стандартный протокол (RFC 6749) для того, чтобы избежать подобного «обмена паролями», позволяя передавать сторонним приложениям только «определенные права (области доступа / scopes)» и только на «ограниченное время».&lt;/p>
&lt;p>В этой статье мы подробно и на практике разберем механизм OAuth 2.0 на примере создания приложения (Slack App) для &lt;strong>Slack (Slack API)&lt;/strong>, который стал де-факто стандартом среди корпоративных мессенджеров. Это исчерпывающее руководство объемом более 10 000 символов, включающее примеры кода на Node.js (Express), диаграммы последовательностей, иллюстрирующие поток протокола, а также глубокое погружение в математические и криптографические аспекты таких важных понятий безопасности, как параметр &lt;code>state&lt;/code> и PKCE.&lt;/p>
&lt;hr>
&lt;h1 id="1-базовые-концепции-oauth-20-4-роли-roles">1. Базовые концепции OAuth 2.0: 4 роли (Roles)
&lt;/h1>&lt;p>Первый шаг к пониманию OAuth 2.0 — это точное понимание его действующих лиц (Roles). Согласно RFC 6749, определены следующие 4 роли:&lt;/p>
&lt;pre class="mermaid">
graph TD
RO[&amp;#34;Resource Owner (Пользователь)&amp;#34;] -- &amp;#34;Предоставляет авторизацию&amp;#34; --&amp;gt; C[&amp;#34;Client (Slack App)&amp;#34;]
C -- &amp;#34;Запрашивает авторизацию&amp;#34; --&amp;gt; AS[&amp;#34;Authorization Server (Сервер авторизации Slack)&amp;#34;]
AS -- &amp;#34;Выдает Access Token&amp;#34; --&amp;gt; C
C -- &amp;#34;Доступ с помощью токена&amp;#34; --&amp;gt; RS[&amp;#34;Resource Server (Сервер Slack API)&amp;#34;]
RO -- &amp;#34;Вход по логину/паролю&amp;#34; --&amp;gt; AS
&lt;/pre>
&lt;ol>
&lt;li>&lt;strong>Resource Owner (Владелец ресурса)&lt;/strong>
&lt;ul>
&lt;li>Сущность, имеющая право предоставлять доступ к ресурсу. Обычно это «конечный пользователь (человек)». В нашем примере это «вы сами, как участник рабочего пространства Slack с правом публиковать сообщения в каналах».&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>&lt;strong>Client (Клиент)&lt;/strong>
&lt;ul>
&lt;li>Приложение, которое пытается получить доступ к серверу ресурсов с разрешения владельца ресурса. В нашем примере это «приложение на Node.js, которое вы разрабатываете (Slack App)». Хотя оно и называется «клиентом», в контексте OAuth даже серверные веб-приложения называются «клиентами».&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>&lt;strong>Authorization Server (Сервер авторизации)&lt;/strong>
&lt;ul>
&lt;li>Сервер, который аутентифицирует владельца ресурса, получает его согласие и выдает клиенту маркер доступа (Access Token). В нашем примере это инфраструктура аутентификации Slack, предоставляющая &lt;code>slack.com/oauth/v2/authorize&lt;/code>.&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>&lt;strong>Resource Server (Сервер ресурсов)&lt;/strong>
&lt;ul>
&lt;li>Сервер, на котором размещены защищенные ресурсы и который принимает запросы на доступ к ним с использованием токена доступа. В нашем примере это конечные точки &lt;code>slack.com/api/&lt;/code>, предоставляющие такие API, как &lt;code>chat.postMessage&lt;/code>.&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ol>
&lt;p>Поток OAuth в двух словах — это &lt;strong>«последовательность действий, при которой Клиент получает согласие Владельца ресурса, получает маркер доступа от Сервера авторизации и с его помощью получает или изменяет данные на Сервере ресурсов»&lt;/strong>.&lt;/p>
&lt;hr>
&lt;h1 id="2-полный-разбор-authorization-code-grant-предоставление-кода-авторизации">2. Полный разбор Authorization Code Grant (Предоставление кода авторизации)
&lt;/h1>&lt;p>В OAuth 2.0 существует несколько потоков (типов предоставления / grant types), но самым рекомендуемым и широко используемым для сред, где можно безопасно хранить секретный ключ (Client Secret) — например, в серверных веб-приложениях — является &lt;strong>Authorization Code Grant (Предоставление кода авторизации)&lt;/strong>.&lt;/p>
&lt;p>Главной особенностью Authorization Code Grant является четкое разделение &lt;strong>front-channel (связь через браузер)&lt;/strong> и &lt;strong>back-channel (прямая связь между серверами)&lt;/strong>. Через front-channel передается только временный «код авторизации (Authorization Code)», а окончательное получение «маркера доступа (Access Token)» происходит через back-channel. Это значительно снижает риск утечки токена через историю браузера или HTTP-реферер.&lt;/p>
&lt;p>Следующая диаграмма последовательности показывает весь процесс Authorization Code Grant для Slack App.&lt;/p>
&lt;pre class="mermaid">
sequenceDiagram
autonumber
participant U as &amp;#34;Resource Owner (Web Browser)&amp;#34;
participant C as &amp;#34;Client (Node.js App)&amp;#34;
participant AS as &amp;#34;Authorization Server (Slack Auth)&amp;#34;
participant RS as &amp;#34;Resource Server (Slack API)&amp;#34;
U-&amp;gt;&amp;gt;C: &amp;#34;Нажатие кнопки установки приложения (GET /slack/install)&amp;#34;
Note over C: &amp;#34;Генерация параметра state&amp;#34;
C--&amp;gt;&amp;gt;U: &amp;#34;Редирект: 302 Found (Location: Slack Auth URL)&amp;#34;
U-&amp;gt;&amp;gt;AS: &amp;#34;GET /oauth/v2/authorize?client_id=...&amp;amp;scope=...&amp;amp;state=...&amp;#34;
AS--&amp;gt;&amp;gt;U: &amp;#34;Экран входа Slack и экран согласия (Consent Screen)&amp;#34;
U-&amp;gt;&amp;gt;AS: &amp;#34;Разрешить доступ (Allow)&amp;#34;
Note over AS: &amp;#34;Генерация кода авторизации (code)&amp;#34;
AS--&amp;gt;&amp;gt;U: &amp;#34;Редирект: 302 Found (Location: Client Callback URL?code=...&amp;amp;state=...)&amp;#34;
U-&amp;gt;&amp;gt;C: &amp;#34;GET /slack/oauth_redirect?code=...&amp;amp;state=...&amp;#34;
Note over C: &amp;#34;Проверка параметра state (защита от CSRF)&amp;#34;
C-&amp;gt;&amp;gt;AS: &amp;#34;POST /api/oauth.v2.access (code, client_id, client_secret)&amp;#34;
Note over C,AS: &amp;#34;Back-channel связь (без участия браузера)&amp;#34;
AS--&amp;gt;&amp;gt;C: &amp;#34;200 OK (JSON: выдача access_token и т.д.)&amp;#34;
C-&amp;gt;&amp;gt;RS: &amp;#34;POST /api/chat.postMessage (Authorization: Bearer &amp;lt;access_token&amp;gt;)&amp;#34;
RS--&amp;gt;&amp;gt;C: &amp;#34;200 OK (Сообщение успешно отправлено)&amp;#34;
&lt;/pre>
&lt;p>Давайте разберем этот поток шаг за шагом с помощью конкретной реализации на Node.js (Express).&lt;/p>
&lt;hr>
&lt;h1 id="3-подготовка-к-реализации-настройки-в-slack-developer-console">3. Подготовка к реализации: Настройки в Slack Developer Console
&lt;/h1>&lt;p>Перед написанием кода необходимо зарегистрировать в системе Slack информацию о том, что «существует новый клиент».&lt;/p>
&lt;ol>
&lt;li>Перейдите в &lt;a class="link" href="https://api.slack.com/apps" target="_blank" rel="noopener"
>Slack API: Applications&lt;/a> и нажмите «Create New App».&lt;/li>
&lt;li>Выберите «From scratch», укажите имя приложения (например, &lt;code>My First OAuth App&lt;/code>) и рабочее пространство для установки.&lt;/li>
&lt;li>На появившемся экране «Basic Information» получите следующие две важные учетные данные:
&lt;ul>
&lt;li>&lt;strong>Client ID&lt;/strong>: Идентификатор, который уникально идентифицирует ваше приложение в публичном доступе. Нет проблем, если он будет включен в запросы, проходящие через браузер (front-channel).&lt;/li>
&lt;li>&lt;strong>Client Secret&lt;/strong>: Секретная строка, известная только вашему приложению. &lt;strong>Ни в коем случае не показывайте её на стороне браузера и не коммитьте в GitHub.&lt;/strong>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>Перейдите на вкладку «OAuth &amp;amp; Permissions» и добавьте URL обратного вызова в «Redirect URLs». Для локальной разработки укажите следующее:
&lt;ul>
&lt;li>&lt;code>http://localhost:3000/slack/oauth_redirect&lt;/code>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ol>
&lt;p>На этом подготовка завершена. Переходим к реализации сервера.&lt;/p>
&lt;hr>
&lt;h1 id="4-шаг-реализации-1-slackinstall-и-параметр-state-для-защиты-от-csrf">4. Шаг реализации 1: &lt;code>/slack/install&lt;/code> и параметр &lt;code>state&lt;/code> для защиты от CSRF
&lt;/h1>&lt;p>Создадим первую конечную точку для того, чтобы пользователь мог начать использовать приложение (установить его в рабочее пространство). Главная задача здесь — перенаправить пользователя на сервер авторизации Slack. Однако с точки зрения безопасности критически важным является &lt;strong>генерация и сохранение параметра &lt;code>state&lt;/code>&lt;/strong>.&lt;/p>
&lt;h2 id="зачем-нужен-параметр-state-предотвращение-csrf-атак">Зачем нужен параметр state (Предотвращение CSRF-атак)
&lt;/h2>&lt;p>Если параметр &lt;code>state&lt;/code> отсутствует, злоумышленник может начать процесс авторизации под своим аккаунтом Slack и подкинуть жертве URL обратного вызова, содержащий полученный «код авторизации» (например, &lt;code>http://localhost:3000/slack/oauth_redirect?code=ATTACKER_CODE&lt;/code>). Если браузер жертвы выполнит этот запрос, в сессии жертвы будет привязан аккаунт Slack злоумышленника, что может привести к утечке информации или нежелательным действиям (Login CSRF).&lt;/p>
&lt;p>Чтобы предотвратить это, используется &lt;code>state&lt;/code> — непредсказуемая случайная строка, позволяющая убедиться, что браузер, инициировавший запрос, и браузер, получивший обратный вызов, — это один и тот же браузер.&lt;/p>
&lt;h2 id="энтропия-state-математический-контекст">Энтропия state (Математический контекст)
&lt;/h2>&lt;p>Для генерации безопасного &lt;code>state&lt;/code> необходимо случайное число с достаточной «энтропией (количеством информации)». Энтропия $E$ зависит от количества возможных вариантов строки $N$ и выражается следующей формулой:&lt;/p>
$$
E = \log_2(N) \quad (\text{единицы измерения: биты})
$$&lt;p>Например, если сгенерировать 16-байтное криптографически стойкое псевдослучайное число (CSPRNG) и преобразовать его в шестнадцатеричную (Hex) строку, количество возможных состояний будет $2^{128}$.&lt;/p>
$$
E = \log_2(2^{128}) = 128 \text{ бит}
$$&lt;p>С энтропией в 128 бит найти коллизию путем полного перебора (brute-force) в современной информатике практически невозможно (астрономически малая вероятность). Обычно в качестве требования безопасности рекомендуется использовать &lt;code>state&lt;/code> с энтропией не менее 128 бит.&lt;/p>
&lt;h2 id="реализация-на-nodejs">Реализация на Node.js
&lt;/h2>&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;span class="lnt">34
&lt;/span>&lt;span class="lnt">35
&lt;/span>&lt;span class="lnt">36
&lt;/span>&lt;span class="lnt">37
&lt;/span>&lt;span class="lnt">38
&lt;/span>&lt;span class="lnt">39
&lt;/span>&lt;span class="lnt">40
&lt;/span>&lt;span class="lnt">41
&lt;/span>&lt;span class="lnt">42
&lt;/span>&lt;span class="lnt">43
&lt;/span>&lt;span class="lnt">44
&lt;/span>&lt;span class="lnt">45
&lt;/span>&lt;span class="lnt">46
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-javascript" data-lang="javascript">&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// app.js (фрагмент)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kr">const&lt;/span> &lt;span class="nx">express&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">require&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;express&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kr">const&lt;/span> &lt;span class="nx">crypto&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">require&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;crypto&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kr">const&lt;/span> &lt;span class="nx">session&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">require&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;express-session&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kr">const&lt;/span> &lt;span class="nx">dotenv&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">require&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;dotenv&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">dotenv&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">config&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kr">const&lt;/span> &lt;span class="nx">app&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">express&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// Настройка промежуточного ПО для сессий (для сохранения state)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="nx">app&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">use&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">session&lt;/span>&lt;span class="p">({&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">secret&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">process&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">env&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">SESSION_SECRET&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">resave&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="kc">false&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">saveUninitialized&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="kc">true&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">cookie&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">secure&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="kc">false&lt;/span> &lt;span class="p">}&lt;/span> &lt;span class="c1">// В производственной среде установите в true
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="p">}));&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kr">const&lt;/span> &lt;span class="nx">SLACK_CLIENT_ID&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">process&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">env&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">SLACK_CLIENT_ID&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kr">const&lt;/span> &lt;span class="nx">SLACK_AUTHORIZE_URL&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s1">&amp;#39;https://slack.com/oauth/v2/authorize&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">app&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;/slack/install&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">req&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">res&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// Генерация надежного 16-байтного случайного числа и преобразование в hex строку (Энтропия: 128 бит)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="kr">const&lt;/span> &lt;span class="nx">state&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">crypto&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">randomBytes&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">16&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">toString&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;hex&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// Сохранение в сессии для проверки во время обратного вызова
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="nx">req&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">session&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">oauth_state&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">state&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// Список запрашиваемых областей доступа (через запятую)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="c1">// chat:write = право на отправку сообщений в канал
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="c1">// channels:read = право на получение информации о публичных каналах
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="kr">const&lt;/span> &lt;span class="nx">scope&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s1">&amp;#39;chat:write,channels:read&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// URL-параметры для формирования запроса к серверу авторизации Slack
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="kr">const&lt;/span> &lt;span class="nx">params&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">URLSearchParams&lt;/span>&lt;span class="p">({&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">client_id&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">SLACK_CLIENT_ID&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">scope&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">scope&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">state&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">state&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">redirect_uri&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;http://localhost:3000/slack/oauth_redirect&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">const&lt;/span> &lt;span class="nx">authUrl&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="sb">`&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nx">SLACK_AUTHORIZE_URL&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="sb">?&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nx">params&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">toString&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="sb">`&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// Редирект пользователя на экран авторизации Slack (302 Found)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="nx">res&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">redirect&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">authUrl&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>При обращении к этой конечной точке HTTP-ответ будет выглядеть следующим образом:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-http" data-lang="http">&lt;span class="line">&lt;span class="cl">&lt;span class="kr">HTTP&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="m">1.1&lt;/span> &lt;span class="m">302&lt;/span> &lt;span class="ne">Found&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">Location&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="l">https://slack.com/oauth/v2/authorize?client_id=123.456&amp;amp;scope=chat%3Awrite%2Cchannels%3Aread&amp;amp;state=a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6&amp;amp;redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fslack%2Foauth_redirect&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">Set-Cookie&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="l">connect.sid=...; Path=/; HttpOnly&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Браузер пользователя немедленно перейдет по указанному &lt;code>Location&lt;/code>, и появится экран Slack (Consent Screen) со знакомым сообщением «My First OAuth App запрашивает доступ к вашему рабочему пространству».&lt;/p>
&lt;hr>
&lt;h1 id="5-шаг-реализации-2-получение-обратного-вызова-и-обмен-на-маркер-доступа">5. Шаг реализации 2: Получение обратного вызова и обмен на маркер доступа
&lt;/h1>&lt;p>Когда пользователь нажимает «Разрешить (Allow)» на экране Slack, сервер Slack перенаправляет браузер пользователя на настроенный &lt;code>redirect_uri&lt;/code>. При этом к URL добавляются параметры запроса &lt;code>code&lt;/code> (код авторизации) и &lt;code>state&lt;/code>, который мы отправили ранее.&lt;/p>
&lt;p>В бэкенде выполняются следующие действия:&lt;/p>
&lt;ol>
&lt;li>Проверка того, что полученный &lt;code>state&lt;/code> полностью совпадает со &lt;code>state&lt;/code>, сохраненным в сессии.&lt;/li>
&lt;li>При совпадении использование полученного &lt;code>code&lt;/code>, вашего &lt;code>client_id&lt;/code> и секретной информации &lt;code>client_secret&lt;/code> для связи с Slack API через back-channel с запросом на получение токена доступа.&lt;/li>
&lt;/ol>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;span class="lnt">34
&lt;/span>&lt;span class="lnt">35
&lt;/span>&lt;span class="lnt">36
&lt;/span>&lt;span class="lnt">37
&lt;/span>&lt;span class="lnt">38
&lt;/span>&lt;span class="lnt">39
&lt;/span>&lt;span class="lnt">40
&lt;/span>&lt;span class="lnt">41
&lt;/span>&lt;span class="lnt">42
&lt;/span>&lt;span class="lnt">43
&lt;/span>&lt;span class="lnt">44
&lt;/span>&lt;span class="lnt">45
&lt;/span>&lt;span class="lnt">46
&lt;/span>&lt;span class="lnt">47
&lt;/span>&lt;span class="lnt">48
&lt;/span>&lt;span class="lnt">49
&lt;/span>&lt;span class="lnt">50
&lt;/span>&lt;span class="lnt">51
&lt;/span>&lt;span class="lnt">52
&lt;/span>&lt;span class="lnt">53
&lt;/span>&lt;span class="lnt">54
&lt;/span>&lt;span class="lnt">55
&lt;/span>&lt;span class="lnt">56
&lt;/span>&lt;span class="lnt">57
&lt;/span>&lt;span class="lnt">58
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-javascript" data-lang="javascript">&lt;span class="line">&lt;span class="cl">&lt;span class="kr">const&lt;/span> &lt;span class="nx">axios&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">require&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;axios&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kr">const&lt;/span> &lt;span class="nx">SLACK_CLIENT_SECRET&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">process&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">env&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">SLACK_CLIENT_SECRET&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kr">const&lt;/span> &lt;span class="nx">SLACK_ACCESS_TOKEN_URL&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s1">&amp;#39;https://slack.com/api/oauth.v2.access&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">app&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;/slack/oauth_redirect&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kr">async&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">req&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">res&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">const&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">code&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">state&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">error&lt;/span> &lt;span class="p">}&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">req&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">query&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// Обработка случая, если пользователь отклонил авторизацию
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">error&lt;/span> &lt;span class="o">===&lt;/span> &lt;span class="s1">&amp;#39;access_denied&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">res&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">status&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">403&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">send&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;Доступ отклонен.&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// 1. Проверка state (защита от CSRF)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="kr">const&lt;/span> &lt;span class="nx">savedState&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">req&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">session&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">oauth_state&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="o">!&lt;/span>&lt;span class="nx">state&lt;/span> &lt;span class="o">||&lt;/span> &lt;span class="nx">state&lt;/span> &lt;span class="o">!==&lt;/span> &lt;span class="nx">savedState&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">res&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">status&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">400&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">send&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;Invalid State Parameter (CSRF Attack Detected)&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// Удаляем использованный state (защита от атак повторного воспроизведения)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="k">delete&lt;/span> &lt;span class="nx">req&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">session&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">oauth_state&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">try&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// 2. Обмен кода авторизации на токен доступа (Back-channel связь)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="kr">const&lt;/span> &lt;span class="nx">tokenResponse&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kr">await&lt;/span> &lt;span class="nx">axios&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">post&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">SLACK_ACCESS_TOKEN_URL&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">URLSearchParams&lt;/span>&lt;span class="p">({&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">client_id&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">SLACK_CLIENT_ID&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">client_secret&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">SLACK_CLIENT_SECRET&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">code&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">code&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">redirect_uri&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;http://localhost:3000/slack/oauth_redirect&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}).&lt;/span>&lt;span class="nx">toString&lt;/span>&lt;span class="p">(),&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">headers&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;Content-Type&amp;#39;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;application/x-www-form-urlencoded&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">const&lt;/span> &lt;span class="nx">data&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">tokenResponse&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">data&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="o">!&lt;/span>&lt;span class="nx">data&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">ok&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">console&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">error&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;Token Exchange Error:&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">data&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">error&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">res&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">status&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">500&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">send&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="sb">`Ошибка Slack API: &lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nx">data&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">error&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="sb">`&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// Успех! Получен токен доступа
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="kr">const&lt;/span> &lt;span class="nx">accessToken&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">data&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">access_token&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">const&lt;/span> &lt;span class="nx">teamName&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">data&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">team&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">name&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">const&lt;/span> &lt;span class="nx">botUserId&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">data&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">bot_user_id&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">console&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">log&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="sb">`Успешно установлено в &lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nx">teamName&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="sb">. Access Token: &lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nx">accessToken&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="sb">`&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// В реальности здесь токен следует зашифровать и сохранить в базу данных
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="c1">// saveToDatabase(data.team.id, encrypt(accessToken));
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">res&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">send&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="sb">`Установка завершена! Рабочее пространство: &lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nx">teamName&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="sb">`&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span> &lt;span class="k">catch&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">err&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">console&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">error&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;Network Error:&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">err&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">res&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">status&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">500&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">send&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;Произошла ошибка сети.&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>В качестве ответа на этот запрос &lt;code>/api/oauth.v2.access&lt;/code>, Slack возвращает следующий JSON:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;ok&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">true&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;app_id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;A12345678&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;authed_user&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;U12345678&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;scope&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;chat:write,channels:read&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;token_type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;bot&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;access_token&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;&amp;lt;YOUR_BOT_TOKEN_HERE&amp;gt;&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;bot_user_id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;B12345678&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;team&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;T12345678&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;My Workspace&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;enterprise&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">null&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Строка, начинающаяся с &lt;code>xoxb-&lt;/code>, — это &lt;strong>Bot Access Token&lt;/strong> в Slack. В дальнейшем, когда приложение будет отправлять запросы к Slack API (Resource Server), оно должно будет указывать &lt;code>Authorization: Bearer xoxb-...&lt;/code> в HTTP-заголовке для аутентификации и подтверждения прав.&lt;/p>
&lt;hr>
&lt;h1 id="6-области-действия-токенов-scopes-и-принцип-наименьших-привилегий">6. Области действия токенов (Scopes) и принцип наименьших привилегий
&lt;/h1>&lt;p>Одним из важнейших понятий в OAuth 2.0 является «Область действия (Scope)». Scope обозначает набор прав, привязанных к токену доступа.&lt;/p>
&lt;p>В Slack права доступа классифицированы очень детально и в основном делятся на &lt;strong>Bot Token Scopes&lt;/strong> и &lt;strong>User Token Scopes&lt;/strong>.&lt;/p>
&lt;ul>
&lt;li>&lt;code>chat:write&lt;/code> (Bot): Право отправлять сообщения в каналы от лица самого приложения (бота).&lt;/li>
&lt;li>&lt;code>chat:write&lt;/code> (User): Право отправлять сообщения от имени пользователя, установившего приложение (с его именем и аватаркой).&lt;/li>
&lt;li>&lt;code>channels:read&lt;/code>: Право на получение списка каналов.&lt;/li>
&lt;li>&lt;code>channels:history&lt;/code>: Право на чтение истории прошлых сообщений в канале.&lt;/li>
&lt;/ul>
&lt;p>В соответствии с важнейшим принципом безопасности — «Принципом наименьших привилегий (Principle of Least Privilege)» — железным правилом является &lt;strong>запрашивать только те права, которые абсолютно необходимы для работы приложения&lt;/strong>. Например, если приложение предназначено «только для отправки уведомлений», оно должно запрашивать только &lt;code>chat:write&lt;/code>, и не должно запрашивать &lt;code>channels:history&lt;/code> (право читать всю прошлую переписку). Это делается для минимизации ущерба в случае взлома приложения и утечки токена.&lt;/p>
&lt;hr>
&lt;h1 id="7-более-продвинутая-безопасность-pkce-proof-key-for-code-exchange">7. Более продвинутая безопасность: PKCE (Proof Key for Code Exchange)
&lt;/h1>&lt;p>В последнее время все шире используется стандарт &lt;strong>PKCE (Proof Key for Code Exchange, RFC 7636, произносится как &amp;ldquo;пикси&amp;rdquo;)&lt;/strong> как механизм дополнительного усиления безопасности OAuth 2.0.&lt;/p>
&lt;p>Изначально PKCE был разработан для «публичных клиентов», таких как нативные приложения (iOS/Android) или SPA (Single Page Application), которые не могут безопасно хранить &lt;code>client_secret&lt;/code>. Однако сегодня в рекомендациях по безопасности (черновик OAuth 2.1) строго рекомендуется использовать PKCE даже для серверных «конфиденциальных клиентов».&lt;/p>
&lt;h2 id="механизм-pkce-и-его-математическая-основа">Механизм PKCE и его математическая основа
&lt;/h2>&lt;p>PKCE криптографически доказывает, что «тот, кто инициировал запрос на авторизацию» и «тот, кто запрашивает обмен токена», — это одно и то же лицо.&lt;/p>
&lt;ol>
&lt;li>Клиент генерирует случайную строку &lt;strong>&lt;code>code_verifier&lt;/code>&lt;/strong> (от 43 до 128 символов).&lt;/li>
&lt;li>Эта строка хешируется с помощью &lt;strong>SHA-256&lt;/strong> и кодируется в BASE64URL. Результат называется &lt;strong>&lt;code>code_challenge&lt;/code>&lt;/strong>.&lt;/li>
&lt;/ol>
&lt;p>Математически это можно выразить так:&lt;/p>
$$
\text{code\_challenge} = \text{BASE64URL-ENCODE}( \text{SHA256}( \text{ASCII}(\text{code\_verifier}) ) )
$$&lt;ol start="3">
&lt;li>При выполнении &lt;code>/slack/install&lt;/code>, клиент отправляет на сервер авторизации (Slack) &lt;code>code_challenge&lt;/code> и &lt;code>code_challenge_method=S256&lt;/code> вместе с параметром &lt;code>state&lt;/code> (Slack временно сохраняет это).&lt;/li>
&lt;li>После обратного вызова, во время обмена токена (&lt;code>/api/oauth.v2.access&lt;/code>), клиент отправляет исходный &lt;strong>&lt;code>code_verifier&lt;/code>&lt;/strong> (до хеширования).&lt;/li>
&lt;li>Сервер авторизации (Slack) самостоятельно хеширует полученный &lt;code>code_verifier&lt;/code> с помощью SHA-256 и проверяет, полностью ли он совпадает с &lt;code>code_challenge&lt;/code>, сохраненным на шаге 3.&lt;/li>
&lt;/ol>
&lt;pre class="mermaid">
sequenceDiagram
participant C as &amp;#34;Client&amp;#34;
participant AS as &amp;#34;Authorization Server&amp;#34;
Note over C: &amp;#34;code_verifier = Случайная строка&amp;#34;&amp;lt;br/&amp;gt;&amp;#34;code_challenge = SHA256(code_verifier)&amp;#34;
C-&amp;gt;&amp;gt;AS: &amp;#34;Запрос авторизации (отправка code_challenge)&amp;#34;
Note over AS: &amp;#34;Сохранение code_challenge&amp;#34;
AS--&amp;gt;&amp;gt;C: &amp;#34;Выдача кода авторизации (code)&amp;#34;
C-&amp;gt;&amp;gt;AS: &amp;#34;Запрос токена (отправка code + code_verifier)&amp;#34;
Note over AS: &amp;#34;SHA256(полученного verifier) == сохраненный challenge?&amp;#34;
AS--&amp;gt;&amp;gt;C: &amp;#34;Проверка успешна: выдача access token&amp;#34;
&lt;/pre>
&lt;p>Благодаря этому механизму, даже если «код авторизации (code)» будет украден вредоносным приложением или в результате перехвата связи, злоумышленник не сможет получить маркер доступа, поскольку он не знает исходного &lt;code>code_verifier&lt;/code> (из-за необратимой природы хеш-функции SHA-256 невозможно получить verifier из challenge).&lt;/p>
&lt;p>В настоящее время поддержка PKCE продвигается в некоторых новых потоках Slack API, а также в других современных API SaaS (Auth0, Okta, X/Twitter API v2 и т.д.), и разработчикам следует активно внедрять эту технологию.&lt;/p>
&lt;hr>
&lt;h1 id="8-безопасное-управление-и-использование-токенов-доступа">8. Безопасное управление и использование токенов доступа
&lt;/h1>&lt;p>Наконец, рассмотрим лучшие практики по сохранению полученных токенов доступа.&lt;/p>
&lt;h2 id="1-обязательное-шифрование-при-сохранении-в-базу-данных">1. Обязательное шифрование при сохранении в базу данных
&lt;/h2>&lt;p>Маркер доступа (&lt;code>xoxb-...&lt;/code>) — это буквальный «ключ» к рабочему пространству Slack. Его нельзя хранить в базе данных (MySQL, PostgreSQL, MongoDB и т.д.) в виде открытого (plain) текста. В случае утечки базы данных, например, из-за SQL-инъекции, произойдет катастрофа: аккаунты Slack всех клиентов будут скомпрометированы.&lt;/p>
&lt;p>Обязательно шифруйте токены на уровне приложения с использованием сильных алгоритмов симметричного шифрования, таких как &lt;strong>AES-256-GCM&lt;/strong>, перед их сохранением в БД. Главный ключ для шифрования/дешифрования должен строго управляться с использованием безопасных сервисов управления ключами, таких как AWS KMS (Key Management Service) или GCP Cloud KMS.&lt;/p>
&lt;h2 id="2-ротация-токенов-token-rotation">2. Ротация токенов (Token Rotation)
&lt;/h2>&lt;p>Использование долгоживущих токенов связано с рисками. В современных реализациях OAuth рекомендуется внедрять механизм перевыпуска нового токена доступа каждые несколько часов (Token Rotation) с использованием «токена обновления (Refresh Token)». В Slack API также можно включить ротацию токенов в настройках.&lt;/p>
&lt;hr>
&lt;h1 id="заключение">Заключение
&lt;/h1>&lt;p>В этой статье мы подробно разобрали поток Authorization Code Grant в OAuth 2.0 с примерами реализации на Node.js для интеграции со Slack App.&lt;/p>
&lt;ol>
&lt;li>Понимание &lt;strong>4 ролей (RO, Client, AS, RS)&lt;/strong> проясняет архитектуру всей системы.&lt;/li>
&lt;li>&lt;strong>Authorization Code Grant&lt;/strong> обеспечивает безопасность за счет грамотного использования каналов связи (front-channel / back-channel) между браузером и сервером.&lt;/li>
&lt;li>Понимание криптографических механизмов, стоящих за защитой от CSRF с помощью параметра &lt;strong>&lt;code>state&lt;/code>&lt;/strong> и предотвращением перехвата кода авторизации с помощью &lt;strong>PKCE&lt;/strong>, является кратчайшим путем к безопасной реализации.&lt;/li>
&lt;li>Проектирование областей доступа (scopes) на основе &lt;strong>принципа наименьших привилегий&lt;/strong> и шифрование при сохранении в БД — абсолютно необходимые элементы для эксплуатации.&lt;/li>
&lt;/ol>
&lt;p>OAuth 2.0 — очень глубокая тема, и одни только спецификации RFC имеют огромный объем. Однако, изучая его на практике на примере реальной платформы (Slack), вы сможете почувствовать его продуманную архитектуру и надежные механизмы безопасности. Надеемся, что знания, полученные из этой статьи, окажутся полезными в вашей будущей разработке приложений и интеграции API.&lt;/p></description></item></channel></rss>