﻿# 使用 appsecret_proof 增強從伺服器呼叫 Facebook Graph API 的安全性

我上周在 **精通 OAuth 2.0 授權框架** 課堂上示範 Facebook 的 OAuth 2.0 如何進行實作，結果過程中不小心啟用一個**進階設定**中的**需要應用程式密鑰**(Require app secret)選項，結果導致我在拿到 Access Token 之後竟然無法成功呼叫任何 [Graph API](https://developers.facebook.com/docs/graph-api/)。在我詳細研究之後，發現這個選項確實可以很好的增強 Access Token 使用的安全性，這篇文章我將來說明這個選項的來龍去脈，讓大家知道這個選項的確切用法。

先給大家看一下 `Meta for Developers` > `設定` > `進階` > `安全性` > `需要應用程式密鑰` 選項的所在位置：

![Meta for Developers > 設定 > 進階 > 安全性 > 需要應用程式密鑰](https://stwillblogassets.blob.core.windows.net/files/images/external/user-images.githubusercontent.com/8bb932405ea03b718896-177196863-70974519-59b8-455f-84b3-3142a380e2ff.png)

![Meta for Developers > Settings > Advanced > Security > Require app secret](https://stwillblogassets.blob.core.windows.net/files/images/external/user-images.githubusercontent.com/ad8bfe41f293208bd035-177196877-28638aef-603f-4f4f-824f-59576f05d7a2.png)

> 這裡我一定要抱怨一下，在 [Meta for Developers](https://developers.facebook.com/) 上面設定「應用程式」的時候，很多選項無論你是在**中文版**或**英文版**都很難理解，這個後台應該是我見過的所有 OAuth Provider 之中最不友善的。這個選項只有一段非常不清楚的描述，沒有背景知識，也沒有參考連結，真的會讓開發人員非常苦惱！重點是 Meta for Developers 網站並沒有很方便的「中英切換」方式，這很容易導致開發人看不懂又查不到資料的情況！

我後來找到了 [Securing Graph API Requests](https://developers.facebook.com/docs/graph-api/securing-requests%20/) 這份文件，這才得知所謂的 `需要應用程式密鑰`(Require app secret) 真正的含意為何！

### 為什麼需要「需要應用程式密鑰」選項？

我們都知道 OAuth 2.0 是一種「授權流程」的規範，而這整個「授權流程」的最終結果，就是要取得一個 Access Token，好讓你拿這個 Access Token 來存取受保護的資源。

若你經營了一個有許多人使用的服務網站，這也意味著你的網站儲存了所有客戶授權與你的 Access Token，若有駭客透過方法獲取網站中所有客戶的 Access Token，這將會導致駭客可以在任何時間、任何地點，使用客戶的 Access Token 來存取客戶的個人資料，我想這種事情並沒有任何人想遇到，但想到「資訊安全」就不得不想到「風險控管」方法，我們永遠要未雨綢繆，對駭客入侵之後的事件進行防範與預備。

因此 Facebook 特別設定了一個「進階」的「安全性」功能，讓你可以透過啟用「需要應用程式密鑰」選項(Require app secret)，來限制 Access Token 的使用方式，讓你不僅僅是在 HTTP Header 中加入一個 `Authorization: Bearer {access_token}` 就能成功呼叫 Graph API，你還要在每次呼叫 Graph API 時都加上 `appsecret_proof` 參數在 Payload 中才行，此舉將可大幅提昇 Access Token 的安全性！

這裡的 `appsecret_proof` 參數，對英文直譯，其意思便是**應用程式密鑰**(app secret)的**驗證**，接下來我將提及 `appsecret_proof` 參數的產生方式！

### 如何產生 `appsecret_proof` 參數

我們每個 Facebook 的應用程式，都會產生一組 **應用程式編號** (App ID) 與 **應用程式密鑰** (App secret)，這兩個參數便是 OAuth 2.0 流程中的 `client_id` 與 `client_secret`。

產生 `appsecret_proof` 參數會需要使用到加密演算法中的 [HMAC](https://en.wikipedia.org/wiki/HMAC) (hash-based message authentication code) 來驗證 Access Token 的真實性，你會需要利用 SHA256 的 Hash 雜湊演算法，搭配一個**應用程式密鑰**(App secret)對 **Access Token** 進行 HMAC 計算，得出一個 Hash 雜湊值。這個雜湊值就是 `appsecret_proof` 參數的確切內容。

以下是我在 [LINQPad 7](https://www.linqpad.net/LINQPad7.aspx) 的 C# 程式碼片段 ([HMACSHA256.linq](http://share.linqpad.net/qcmdb9.linq))：

```csharp
void Main()
{
    var accessToken = "EBAQ7ZADnvnM4BAGgvGFQalWbyz3tK6DhVIXTXP1c4Cuwg1DYQkZBfk2SljTna4yxCX4ZB2O9KAZBgZB3tLWVEpNHWho2z1Oc1w2BN3Fz4D4IEXY62ZCXeR43buhWh4lU6EyVfoYecV1rSACAQD53rym6LZBiZCtO1sl5ceiE4yuwg9BwTydca9VH9vDJPPsZBUFDQi2jIHqZ7ItZBUSgdvkfZCtlOnOxgAtsuyqKR9vVJD2ToC1mErVcUg1";
    var appSecret = "00fed430c8ed7e584042bfe0ed52d523";

    HMACSHA256(accessToken, appSecret).Dump();
    // c800ac5544633e21671cbef352252099941dcd0eab32e2b69a60e2da89dd4890
}

// [C#] 在 C# 使用 HMAC - SHA256 加密
// https://marcus116.blogspot.com/2019/06/how-to-create-hmacsha256-cryptographyin-csharp.html

string HMACSHA256(string message, string key)
{
    var encoding = new System.Text.UTF8Encoding();
    byte[] keyByte = encoding.GetBytes(key);
    byte[] messageBytes = encoding.GetBytes(message);

    // HMACSHA256 建構函式
    // https://learn.microsoft.com/zh-tw/dotnet/api/system.security.cryptography.hmacsha256.-ctor
    using (var hmacSHA256 = new HMACSHA256(keyByte))
    {
        byte[] hashMessage = hmacSHA256.ComputeHash(messageBytes);
        return BitConverter.ToString(hashMessage).Replace("-", "").ToLower();
    }
}
```

假設我們呼叫 `/me?fields=id,name` 這個端點：

```
GET /me?fields=id,name HTTP/1.1
Host: graph.facebook.com
Authorization: Bearer EBAQ7ZADnvnM4BAGgvGFQalWbyz3tK6DhVIXTXP1c4Cuwg1DYQkZBfk2SljTna4yxCX4ZB2O9KAZBgZB3tLWVEpNHWho2z1Oc1w2BN3Fz4D4IEXY62ZCXeR43buhWh4lU6EyVfoYecV1rSACAQD53rym6LZBiZCtO1sl5ceiE4yuwg9BwTydca9VH9vDJPPsZBUFDQi2jIHqZ7ItZBUSgdvkfZCtlOnOxgAtsuyqKR9vVJD2ToC1mErVcUg1
```

你會得到這個錯誤訊息：

```json
{
    "error": {
        "message": "API calls from the server require an appsecret_proof argument",
        "type": "GraphMethodException",
        "code": 100,
        "fbtrace_id": "Ax8kq3_1ZaWovwhVwRda0Js"
    }
}
```

當我們透過 `HMACSHA256(accessToken, appSecret)` 算出一個雜湊值，你就可以把 `appsecret_proof` 參數加入到 Query String 之中，並發出 Graph API 呼叫：

```
GET /me?fields=id,name&appsecret_proof=c800ac5544633e21671cbef352252099941dcd0eab32e2b69a60e2da89dd4890 HTTP/1.1
Host: graph.facebook.com
Authorization: Bearer EBAQ7ZADnvnM4BAGgvGFQalWbyz3tK6DhVIXTXP1c4Cuwg1DYQkZBfk2SljTna4yxCX4ZB2O9KAZBgZB3tLWVEpNHWho2z1Oc1w2BN3Fz4D4IEXY62ZCXeR43buhWh4lU6EyVfoYecV1rSACAQD53rym6LZBiZCtO1sl5ceiE4yuwg9BwTydca9VH9vDJPPsZBUFDQi2jIHqZ7ItZBUSgdvkfZCtlOnOxgAtsuyqKR9vVJD2ToC1mErVcUg1
```

接著你就可以獲得預期的結果：

```json
{
    "id": "XXXXXXX2196190487",
    "name": "Will 保哥"
}
```

### 如何有效防止 Access Token 被駭客利用

我們可以想像一種情境，駭客透過某種手法取得客戶資料庫，無論透過社交工程(Social engineering)、[APT](https://en.wikipedia.org/wiki/Advanced_persistent_threat) ([進階持續性威脅](https://zh.wikipedia.org/wiki/%E9%AB%98%E7%BA%A7%E9%95%BF%E6%9C%9F%E5%A8%81%E8%83%81))、從 USB 取得資料庫備份檔 (別認為不可能) 或找到系統漏洞([SQL Injection](https://zh.m.wikipedia.org/zh-tw/SQL%E6%B3%A8%E5%85%A5))等方法，從中取得所有客戶曾經授權過的 Access Token，但駭客只要拿不到**應用程式密鑰**(app secret)，那麼他肯定無法產生 `appsecret_proof` 參數值，因此你只要使用 [Key management](https://en.wikipedia.org/wiki/Key_management) 之類的管理技巧 (例如 [Azure Key Vault](https://azure.microsoft.com/zh-tw/services/key-vault/) 等等)，好好保護**應用程式密鑰**(app secret)的存取，不要寫在資料庫中，也不要寫在程式碼中，甚至不要寫在應用程式組態中，如此一來就可以大幅提昇系統的安全性！ 👍

### 相關連結

-   [Securing Graph API Requests](https://developers.facebook.com/docs/graph-api/securing-requests%20/)
-   [Graph API](https://developers.facebook.com/docs/graph-api/)
    -   強制英文版: [https://developers.facebook.com/docs/graph-api/?locale=en\_US](https://developers.facebook.com/docs/graph-api/?locale=en_US)
    -   強制中文版: [https://developers.facebook.com/docs/graph-api/?locale=zh\_TW](https://developers.facebook.com/docs/graph-api/?locale=zh_TW)
