<?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/tags/authentication/</link><description>Recent content in Authentication on kenji.blog</description><generator>Hugo -- gohugo.io</generator><language>ja</language><copyright>kenjinote</copyright><lastBuildDate>Sun, 13 Sep 2026 12:00:00 +0900</lastBuildDate><atom:link href="http://kenji.blog/tags/authentication/index.xml" rel="self" type="application/rss+xml"/><item><title>OAuth2.0の仕組みを図解：Slack App連携の実装から学ぶ</title><link>http://kenji.blog/p/oauth2-architecture-slack-app-integration/</link><pubDate>Sun, 13 Sep 2026 12:00:00 +0900</pubDate><guid>http://kenji.blog/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 OAuth2.0の仕組みを図解：Slack App連携の実装から学ぶ" />&lt;h1 id="はじめになぜoauth-20を学ぶのか">はじめに：なぜOAuth 2.0を学ぶのか？
&lt;/h1>&lt;p>現代のWebアプリケーションにおいて、複数のサービスが連携して動作することはもはや当たり前の光景となりました。例えば、「Googleアカウントでログインする」「Trelloのタスクが更新されたらSlackに通知を送る」「ZoomのミーティングリンクをGoogleカレンダーに自動追加する」といった機能です。これらすべての裏側で活躍しているのが &lt;strong>OAuth 2.0 (Open Authorization 2.0)&lt;/strong> という認可フレームワークです。&lt;/p>
&lt;p>かつて、異なるサービス間でデータをやり取りする際には、ユーザーが自分のIDとパスワードを連携先のサービスに直接渡す「ベーシック認証」や「パスワード共有」という非常に危険な手法が用いられていました。しかし、この方法では連携先サービスがユーザーの全権限を握ることになり、セキュリティ上の致命的なリスクを伴います。&lt;/p>
&lt;p>OAuth 2.0は、このような「パスワードの共有」を回避しつつ、「特定の権限（スコープ）のみ」を「限られた時間だけ」サードパーティアプリケーションに委譲するための標準プロトコル（RFC 6749）として誕生しました。&lt;/p>
&lt;p>本記事では、このOAuth 2.0の仕組みを、ビジネスコミュニケーションツールとしてデファクトスタンダードとなっている &lt;strong>Slack (Slack API)&lt;/strong> を対象としたアプリケーション（Slack App）の実装を通して、極めて詳細かつ実践的に解説していきます。Node.js (Express) を用いたコード例、プロトコルのフローを図解するシーケンス図、そしてセキュリティ上の重要な概念である &lt;code>state&lt;/code> パラメータや PKCE の数学的・暗号学的背景にまで踏み込んだ、1万文字を超える決定版の解説です。&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を理解するための第一歩は、登場人物（Role）を正確に把握することです。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;アクセストークン発行&amp;#34; --&amp;gt; C
C -- &amp;#34;トークンを使ってアクセス&amp;#34; --&amp;gt; RS[&amp;#34;Resource Server (Slack APIサーバー)&amp;#34;]
RO -- &amp;#34;ID/パスワードでログイン&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）」です。「クライアント」という名前ですが、サーバーサイドで動くWebアプリケーションであってもOAuthの文脈では「クライアント」と呼ばれます。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>&lt;strong>Authorization Server（認可サーバー）&lt;/strong>
&lt;ul>
&lt;li>リソースオーナーを認証し、リソースオーナーから認可を得た上で、クライアントに対してアクセストークンを発行するサーバーです。今回の例では、&lt;code>slack.com/oauth/v2/authorize&lt;/code> を提供するSlackの認証基盤です。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>&lt;strong>Resource Server（リソースサーバー）&lt;/strong>
&lt;ul>
&lt;li>保護されたリソースをホストしており、アクセストークンを用いてリソースへのアクセス要求を受け付け、応答するサーバーです。今回の例では、&lt;code>chat.postMessage&lt;/code> などのAPIを提供する &lt;code>slack.com/api/&lt;/code> のエンドポイントです。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ol>
&lt;p>OAuthのフローとは、一言で言えば &lt;strong>「Clientが、Resource Ownerの同意を得て、Authorization Serverからアクセストークンを受け取り、それを使ってResource Serverからデータを取得・操作する」&lt;/strong> 一連の手順のことです。&lt;/p>
&lt;hr>
&lt;h1 id="2-認可コードグラントauthorization-code-grantの完全解剖">2. 認可コードグラント（Authorization Code Grant）の完全解剖
&lt;/h1>&lt;p>OAuth 2.0には複数のフロー（グラントタイプ）が存在しますが、Webアプリケーションのようなサーバーサイドで秘密鍵（Client Secret）を安全に保持できる環境において最も推奨され、最も広く使われているのが &lt;strong>認可コードグラント（Authorization Code Grant）&lt;/strong> です。&lt;/p>
&lt;p>認可コードグラントの最大の特徴は、&lt;strong>フロントチャネル（ブラウザを経由する通信）&lt;/strong> と &lt;strong>バックチャネル（サーバー間の直接通信）&lt;/strong> を明確に分離している点です。フロントチャネルでは一時的な「認可コード（Authorization Code）」のみを受け渡し、最終的な「アクセストークン」の取得はバックチャネルで行うことで、トークンがブラウザの履歴やリファラに漏洩するリスクを劇的に低減しています。&lt;/p>
&lt;p>以下のシーケンス図は、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;バックチャネル通信（ブラウザを経由しない）&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」にて、以下の重要な2つのクレデンシャル（資格情報）を取得します。
&lt;ul>
&lt;li>&lt;strong>Client ID&lt;/strong>: あなたのアプリを公開的に一意に識別するID。ブラウザを経由するリクエスト（フロントチャネル）に含めても問題ありません。&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」画面に移動し、「Redirect URLs」にコールバック先のURLを登録します。今回はローカル開発を想定し、以下を設定します。
&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-実装ステップ1slackinstall-と-csrf対策の-state-パラメータ">4. 実装ステップ1：&lt;code>/slack/install&lt;/code> と CSRF対策の &lt;code>state&lt;/code> パラメータ
&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アカウントとの紐付けが完了してしまい、情報漏洩や意図しない操作の原因となります（ログイン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{単位: bits})
$$&lt;p>たとえば、16バイトの暗号論的疑似乱数（CSPRNG）を生成し、それを16進数（Hex）文字列に変換した場合、表現できる状態の数は $2^{128}$ となります。&lt;/p>
$$
E = \log_2(2^{128}) = 128 \text{ bits}
$$&lt;p>128ビットのエントロピーがあれば、現代の計算機科学においてブルートフォース（総当たり）攻撃で衝突を見つけることは事実上不可能（天文学的な確率）です。通常、セキュリティ要件として少なくとも128ビット以上のエントロピーを持つ &lt;code>state&lt;/code> が推奨されます。&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バイトの強力な乱数を生成し、16進数文字列に変換 (エントロピー: 128 bits)
&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">// Slackの認可サーバー構築するURLパラメータ
&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>ユーザーがSlackの画面で「許可する (Allow)」をクリックすると、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にバックチャネルで通信し、アクセストークンを要求する。&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. 認可コードをアクセストークンに交換（バックチャネル通信）
&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 Error: &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">`Successfully installed to &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> から始まる文字列が、Slackにおける&lt;strong>Botアクセストークン&lt;/strong>です。以降、アプリケーションがSlack API（Resource Server）にリクエストを送る際は、HTTPヘッダーに &lt;code>Authorization: Bearer xoxb-...&lt;/code> と付与することで、認証と権限の証明が行われます。&lt;/p>
&lt;hr>
&lt;h1 id="6-トークンスコープと最小権限の原則-principle-of-least-privilege">6. トークンスコープと最小権限の原則 (Principle of Least Privilege)
&lt;/h1>&lt;p>OAuth 2.0において最も重要な概念の一つが「スコープ（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>昨今、OAuth 2.0のセキュリティをさらに強化する仕組みとして &lt;strong>PKCE (Proof Key for Code Exchange, RFC 7636, &amp;ldquo;ピクシー&amp;quot;と発音)&lt;/strong> が標準化され、広く利用されるようになっています。&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> 実行時に、&lt;code>state&lt;/code> に加えて &lt;code>code_challenge&lt;/code> と &lt;code>code_challenge_method=S256&lt;/code> を認可サーバー（Slack）に送信します（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ハッシュ化し、ステップ3で保存しておいた &lt;code>code_challenge&lt;/code> と完全一致するか検証します。&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;検証成功: アクセストークン発行&amp;#34;
&lt;/pre>
&lt;p>この仕組みにより、仮に悪意のあるアプリや通信経路の盗聴によって「認可コード（code）」が盗まれたとしても、攻撃者は元の &lt;code>code_verifier&lt;/code> を知らないため（不可逆なハッシュ関数SHA-256の性質上、challengeからverifierを逆算することは不可能）、アクセストークンを入手することができません。&lt;/p>
&lt;p>現在、Slack APIの一部の新しいフローや、他のモダンなSaaS API（Auth0, Okta, X/Twitter API v2など）ではPKCEのサポートが進んでおり、開発者は積極的に採用すべき技術となっています。&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など）に平文（プレーンテキスト）で保存してはいけません。万が一SQLインジェクションなどでデータベースが流出した場合、全顧客のSlackが乗っ取られる大惨事となります。&lt;/p>
&lt;p>必ずアプリケーションレイヤーで &lt;strong>AES-256-GCM&lt;/strong> などの強力な対称鍵暗号を用いて暗号化してからDBに保存してください。暗号化/復号のためのマスターキーは、AWS KMS（Key Management Service）や GCP Cloud KMS などのセキュアな鍵管理サービスを利用して厳格に管理します。&lt;/p>
&lt;h2 id="2-トークンローテーションtoken-rotation">2. トークンローテーション（Token Rotation）
&lt;/h2>&lt;p>長期有効なトークンを使い続けることはリスクを伴います。最新のOAuth実装では「リフレッシュトークン（Refresh Token）」を利用し、数時間ごとに新しいアクセストークンを発行し直す仕組み（Token Rotation）を取り入れることが推奨されます。Slack APIでもオプション設定でトークンローテーションを有効化することが可能です。&lt;/p>
&lt;hr>
&lt;h1 id="まとめ">まとめ
&lt;/h1>&lt;p>本記事では、OAuth 2.0の認可コードグラントフローについて、Slack App連携の具体的なNode.js実装コードを交えながら詳細に解説しました。&lt;/p>
&lt;ol>
&lt;li>&lt;strong>4つの役割（RO, Client, AS, RS）&lt;/strong> を意識することで、システム全体のアーキテクチャが明確になります。&lt;/li>
&lt;li>&lt;strong>認可コードグラント&lt;/strong> は、ブラウザとサーバー間の通信経路（フロント/バックチャネル）を巧みに使い分けることで安全性を担保しています。&lt;/li>
&lt;li>&lt;strong>&lt;code>state&lt;/code> パラメータ&lt;/strong> によるCSRF防御や、&lt;strong>PKCE&lt;/strong> による認可コードインターセプト攻撃の防止など、背景にある暗号学的なメカニズムを理解することがセキュアな実装への近道です。&lt;/li>
&lt;li>&lt;strong>最小権限の原則&lt;/strong> に基づくスコープ設計と、DB保存時の暗号化は運用上絶対に欠かせない要素です。&lt;/li>
&lt;/ol>
&lt;p>OAuth 2.0は非常に奥が深く、RFCだけでも膨大な仕様が存在しますが、このように実際のプラットフォーム（Slack）をターゲットにして手を動かしながら学ぶことで、その洗練された設計思想と堅牢なセキュリティの仕組みを実感できるはずです。今後のアプリケーション開発やAPI連携の実装において、本記事の知識が役立てば幸いです。&lt;/p></description></item></channel></rss>