<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>OAuth2.0 on kenji.blog</title><link>http://kenji.blog/zh-tw/tags/oauth2.0/</link><description>Recent content in OAuth2.0 on kenji.blog</description><generator>Hugo -- gohugo.io</generator><language>zh-tw</language><copyright>kenjinote</copyright><lastBuildDate>Sun, 13 Sep 2026 12:00:00 +0900</lastBuildDate><atom:link href="http://kenji.blog/zh-tw/tags/oauth2.0/index.xml" rel="self" type="application/rss+xml"/><item><title>圖解 OAuth 2.0 的運作機制：從 Slack App 整合實作中學習</title><link>http://kenji.blog/zh-tw/p/oauth2-architecture-slack-app-integration/</link><pubDate>Sun, 13 Sep 2026 12:00:00 +0900</pubDate><guid>http://kenji.blog/zh-tw/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>在現代的 Web 應用程式中，多個服務協同運作已成為理所當然的景象。例如，「使用 Google 帳號登入」、「當 Trello 任務更新時發送通知到 Slack」、「將 Zoom 的會議連結自動加入 Google 日曆」等功能。在這些功能背後活躍的，正是稱為 &lt;strong>OAuth 2.0 (Open Authorization 2.0)&lt;/strong> 的授權框架。&lt;/p>
&lt;p>過去，在不同服務之間交換資料時，使用者會將自己的帳號密碼直接交給合作服務，這種稱為「基本驗證（Basic Authentication）」或「密碼共享」的手法非常危險。然而，這種方法會讓合作服務掌握使用者的所有權限，伴隨著安全性上致命的風險。&lt;/p>
&lt;p>OAuth 2.0 的誕生正是為了避免這種「密碼共享」的情況，同時作為一種標準協定（RFC 6749），將「僅限特定權限（Scope）」在「有限時間內」委派給第三方應用程式。&lt;/p>
&lt;p>本文將透過作為商業通訊工具事實標準的 &lt;strong>Slack (Slack API)&lt;/strong> 之應用程式（Slack App）實作，極為詳細且實用地解說 OAuth 2.0 的運作機制。這是一篇超過一萬字的決定版解說，包含了使用 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 的第一步，是準確掌握登場人物（角色）。在 RFC 6749 中，定義了以下 4 個角色：&lt;/p>
&lt;pre class="mermaid">
graph TD
RO[&amp;#34;Resource Owner (資源擁有者 / 使用者)&amp;#34;] -- 給予授權 --&amp;gt; C[&amp;#34;Client (Slack App)&amp;#34;]
C -- 請求授權 --&amp;gt; AS[&amp;#34;Authorization Server (Slack 授權伺服器)&amp;#34;]
AS -- 發行存取權杖 (Access Token) --&amp;gt; C
C -- 使用權杖進行存取 --&amp;gt; RS[&amp;#34;Resource Server (Slack API 伺服器)&amp;#34;]
RO -- 使用 ID / 密碼登入 --&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 的語境中，即使是在伺服器端運行的 Web 應用程式也被稱為「客戶端」。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>&lt;strong>Authorization Server（授權伺服器）&lt;/strong>
&lt;ul>
&lt;li>驗證資源擁有者，並在取得資源擁有者的授權後，向客戶端發行存取權杖（Access Token）的伺服器。在這次的範例中，是提供 &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>前台通道（Front-channel，透過瀏覽器的通訊）&lt;/strong> 與 &lt;strong>後台通道（Back-channel，伺服器間的直接通訊）&lt;/strong>。在前台通道中僅傳遞暫時的「授權碼（Authorization Code）」，最終的「存取權杖」則是在後台通道取得，藉此大幅降低了權杖洩漏到瀏覽器歷史紀錄或推薦連結（Referer）的風險。&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」畫面中，取得以下兩個重要的憑證（Credentials）：
&lt;ul>
&lt;li>&lt;strong>Client ID&lt;/strong>: 公開且唯一識別你的應用程式的 ID。將其包含在透過瀏覽器發送的請求（前台通道）中也沒有問題。&lt;/li>
&lt;li>&lt;strong>Client Secret&lt;/strong>: 只有你的應用程式才知道的機密字串。&lt;strong>絕對不可以暴露在瀏覽器端，也不可以提交（Commit）到 GitHub 等地方。&lt;/strong>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>移動到「OAuth &amp;amp; Permissions」畫面，在「Redirect URLs」中註冊回呼（Callback）網址。這次假設是本地開發，請設定如下：
&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 帳號開始授權流程，並誘使受害者點擊包含取得的「授權碼」的回呼網址（例如：&lt;code>http://localhost:3000/slack/oauth_redirect?code=ATTACKER_CODE&lt;/code>）。當受害者的瀏覽器執行此網址時，就會在受害者的連線階段（Session）中完成與攻擊者 Slack 帳號的綁定，導致資料外洩或非預期的操作（登入 CSRF）。&lt;/p>
&lt;p>為了防範這種情況，&lt;code>state&lt;/code> 被設計為一個無法預測的隨機字串，用來驗證「發起請求的瀏覽器」與「接收回呼的瀏覽器」是同一個。&lt;/p>
&lt;h2 id="state-的熵entropy--數學背景">state 的熵（Entropy / 數學背景）
&lt;/h2>&lt;p>為了產生安全的 &lt;code>state&lt;/code>，需要具有足夠「熵（資訊量）」的亂數。熵 $E$ 取決於產生的字串種類數 $N$，並以下列公式表示：&lt;/p>
$$
E = \log_2(N) \quad (\text{單位: bits})
$$&lt;p>例如，若產生 16 位元組的密碼學偽亂數（CSPRNG），並將其轉換為十六進位（Hex）字串，所能表現的狀態數量為 $2^{128}$。&lt;/p>
$$
E = \log_2(2^{128}) = 128 \text{ bits}
$$&lt;p>只要擁有 128 位元的熵，在現代的計算機科學中，要透過暴力破解（Brute-force）攻擊找到碰撞，實際上是不可能的（天文數字般的機率）。通常在安全要求上，會建議使用至少擁有 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">// Session 中介軟體設定（為了保存 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 位元組的強大亂數，並轉換為十六進位字串 (熵: 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">// 保存到 session 中，以便在回呼時進行驗證
&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 的查詢參數（Query Parameter）中會附加上 &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> 是否與保存在 session 中的 &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（防範重放攻擊 / Replay Attack）
&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;pixy&amp;rdquo;）&lt;/strong> 已被標準化並廣泛使用。&lt;/p>
&lt;p>最初，PKCE 是為了無法安全保存 &lt;code>client_secret&lt;/code> 的「公開客戶端（Public Client）」，如原生應用程式（iOS/Android）或 SPA（Single Page Application）而設計的。然而，在目前的安全性最佳實踐（OAuth 2.1 草案）中，即使是伺服器端的「機密客戶端（Confidential Client）」，也強烈建議使用 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 工作區的「備用鑰匙」。絕對不能以明文（Plain Text）的形式保存在資料庫（MySQL、PostgreSQL、MongoDB 等）中。萬一因為 SQL 注入（SQL Injection）等攻擊導致資料庫外洩，將引發所有客戶的 Slack 帳號被盜用的慘劇。&lt;/p>
&lt;p>請務必在應用程式層使用 &lt;strong>AES-256-GCM&lt;/strong> 等強大的對稱金鑰加密技術進行加密後，再存入資料庫。用於加密/解密的主要金鑰（Master Key），應利用 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>本文透過 Slack App 整合的具體 Node.js 實作程式碼，詳細解說了 OAuth 2.0 的授權碼授權流程。&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> 的範圍設計，以及在存入資料庫時進行加密，是營運上絕對不可或缺的要素。&lt;/li>
&lt;/ol>
&lt;p>OAuth 2.0 是一門非常深奧的學問，光是 RFC 就有龐大的規格，但像這樣以實際的平台（Slack）為目標，一邊動手實作一邊學習，應該就能體會到其精練的設計理念與堅固的安全機制。希望本文的知識能在未來的應用程式開發或 API 整合實作中對您有所幫助。&lt;/p></description></item></channel></rss>