使用 appsecret_proof 增強從伺服器呼叫 Facebook Graph API 的安全性 | The Will Will Web

The Will Will Web

記載著 Will 在網路世界的學習心得與技術分享

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

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

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

Meta for Developers > 設定 > 進階 > 安全性 > 需要應用程式密鑰

Meta for Developers > Settings > Advanced > Security > Require app secret

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

我後來找到了 Securing Graph API Requests 這份文件,這才得知所謂的 需要應用程式密鑰(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_idclient_secret

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

以下是我在 LINQPad 7 的 C# 程式碼片段 (HMACSHA256.linq):

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://docs.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

你會得到這個錯誤訊息:

{
    "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

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

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

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

我們可以想像一種情境,駭客透過某種手法取得客戶資料庫,無論透過社交工程(Social engineering)、APT (進階持續性威脅)、從 USB 取得資料庫備份檔 (別認為不可能) 或找到系統漏洞(SQL Injection)等方法,從中取得所有客戶曾經授權過的 Access Token,但駭客只要拿不到應用程式密鑰(app secret),那麼他肯定無法產生 appsecret_proof 參數值,因此你只要使用 Key management 之類的管理技巧 (例如 Azure Key Vault 等等),好好保護應用程式密鑰(app secret)的存取,不要寫在資料庫中,也不要寫在程式碼中,甚至不要寫在應用程式組態中,如此一來就可以大幅提昇系統的安全性! 👍

相關連結