[料理佳餚] 在 ASP.NET Core 整合 Twitter 做為網站的第三方登入

第三方登入系列文章的第六篇,「在 ASP.NET Core 整合 Twitter 做第三方登入」,應該也是最後一篇了,最近這一段時間討論度最高的社群平台應該就是 Twitter 了,不知道一龍這樣搞下去,Twitter 的未來會怎麼樣?不過在台灣來講,使用 Twitter 的人比例是比較小的,若是我們的應用程式的主要服務對象是在美國或是日本,Twitter 的第三方登入可能就有其必要性,整個整合的過程,就記錄在這篇文章當中。

其他的第三方登入解決方案可以參考下面的連結:

建立 App

第一步我們先在 Twitter 上建立 App,在建立 App 之前先要建立 Project,我們到 Twitter 的 developer portal’s App page,點擊「Create Project」,一路按照步驟填入「Project name」、「Use case」、「Project description」,Project 就能建立起來。

Project 建立完成後,會自動跳到建立 App 的畫面,填入「App name」後按下「Next」,看到 API KeyAPI Key SecretBearer Token 都產生出來之後,App 就建立成功了。

接下來,我們要取得 Client IDClient Secret,我們切換到應用程式的設定頁面,點擊「Set up」。

在開啟的設定畫面中,將必填欄位填一填,其中「App permissions」就按照我們的需要去選擇,「Type of App」請選擇 Confidential client,「Callback URI / Redirect URL」填入規劃好的 Callback URL,「Website URL」填入應用程式的相關網頁,然後按下「Save」。

跳出來的視窗就會顯示我們的 Client ID 及 Client Secret,複製下來備用。

準備 Redirect URL

接下來,我們要來撰寫 Redirect URL 的程式了,使用者授權成功後,Twitter 會將 code 回傳到我們的 Web Api,我們再拿這個 code 發送 POST 去跟 Twitter 交換 Access Token,再拿 Access Token 去跟 Twitter 拿使用者授權的資料,下面是交換 Access Token 時,所需的參數:

  1. Access Token URL:https://api.twitter.com/2/oauth2/token
  2. User Info Url:https://api.twitter.com/2/users/me?user.fields=id,name,profile_image_url
  3. client_id:即 Client ID
  4. client_secret:即 Client Secret
  5. code:即我們拿到的 code
  6. redirect_uri:即 Callback URI / Redirect URL
  7. code_verifier:一個隨機字串,必須搭配授權網址中的 code_challengecode_challenge_method 一起使用,在 OAuth 2.0 PKCE(Proof Key for Code Exchange) 模式中扮演答案的角色。

關於 OAuth 2.0 PKCE 模式的運作是這樣的,我們會在授權網址中將 code_challenge 及 code_challenge_method 送給 Twitter,而 code_challenge 的值是依據 code_challenge_method 所指定的方式,將 code_verifier 進行轉換得來的,而 code_challenge_method 有兩種選擇,分別是 plainS256,詳細的轉換邏輯可以參考 RFC 7636 - 4.2 Client Creates the Code Challenge

我們在跟 Twitter 交換 Access Token 的時候,需要將 code_verifier 送給 Twitter,Twitter 會將收到 code_verifier 再依據 code_challenge_method 指定的方式轉換過後,與 code_challenge 進行比對,比對相同才能通過驗證。

程式原始碼如下:

[HttpGet("callback")]
public async Task<IActionResult> Callback()
{
    if (!this.Request.Query.TryGetValue("code", out var code))
    {
        return this.StatusCode(400);
    }

    var oauthToken = await this.ExchangeAccessToken(code);

    if (oauthToken == null)
    {
        return this.StatusCode(400);
    }

    var userInfo = await this.GetUserInfo(oauthToken);

    // TODO: Save AccessToken and UserProfile

    // TODO: User Login

    return this.Redirect("/");
}

private async Task<string> ExchangeAccessToken(string code)
{
    var client = this.httpClientFactory.CreateClient();

    var request = new HttpRequestMessage(HttpMethod.Post, "AccessTokenUrl");

    // 將 Client ID 及 Client Secret 以 Basic Authentication 的方式塞進 Header
    var authorization = Convert.ToBase64String(
        Encoding.UTF8.GetBytes($"{ClientId}:{ClientSecret}"));

    request.Headers.Authorization = AuthenticationHeaderValue.Parse($"Basic {authorization}");

    request.Content = new FormUrlEncodedContent(
        new Dictionary<string, string>
        {
            ["grant_type"] = "authorization_code",
            ["code"] = code,
            ["redirect_uri"] = "RedirectURI",
            ["code_verifier"] = "CodeVerifier"
        });

    var response = await client.SendAsync(request);

    if (response.StatusCode != HttpStatusCode.OK) return null;

    var content = await response.Content.ReadAsStringAsync();

    var result = JsonNode.Parse(content);

    return result["access_token"].GetValue<string>();
}

private async Task<string> GetUserInfo(string accessToken)
{
    var client = this.httpClientFactory.CreateClient();

    var request = new HttpRequestMessage(HttpMethod.Get, "UserInfoUrl");

    request.Headers.Authorization = AuthenticationHeaderValue.Parse($"Bearer {accessToken}");

    var response = await client.SendAsync(request);

    var result = await response.Content.ReadAsStringAsync();

    return result;
}

授權網址

再來要兜出授權網址給使用者登入用,Twitter 的授權網址為 https://twitter.com/i/oauth2/authorize,所需要的參數如下:

  1. client_id:即 Client ID
  2. redirect_uri:即 Callback URI / Redirect URL
  3. state:隨機產生的一段唯一的字串,主要是可以用來避免 CSRF(Cross Site Request Forgery)
  4. scope:想請使用者授權給我們的資料範圍,可參考 OAuth 2.0 Authorization Code Flow with PKCE | Docs | Twitter Developer Platform
  5. code_challenge:請參考上面關於 OAuth 2.0 PKCE 模式的說明
  6. code_challenge_method:請參考上面關於 OAuth 2.0 PKCE 模式的說明,我選用 S256。

在這邊我們可能會遇到 code_verifier 及 code_challenge 如何產生的問題?這個部分我們除了根據 RFC 7636 所制定的邏輯自行撰寫之外,也可以利用網路上現成的 Online PKCE Generator Tool 來產生。

所以我根據我所取得資料,兜出了下面這個授權網址:

https://twitter.com/i/oauth2/authorize?client_id=ClientID&redirect_uri=http%3A%2F%2Flocalhost%3A5101%2Ftwitter%2Flogin%2Fcallback&state=12345abcde&response_type=code&scope=tweet.read%20users.read&code_challenge=6vJcU7lefkN6SspmdjY4UFO2LxdGfuDsThyaWwcQJY4&code_challenge_method=S256

登入的流程跑起來大概像下面這張圖:

使用者點擊授權網址,透過 Twitter 的介面授權成功之後,我們拿到了使用者授權的資料,就算是整合成功了。

Twitter 目前正處於風雨飄搖的狀態,你我都正在見證著,祝福能夠有個好的結果,以上,整合 Twitter 第三方登入就分享給大家,希望對大家能夠有一點幫助。

參考資料

相關資源

C# 指南
ASP.NET 教學
ASP.NET MVC 指引
Azure SQL Database 教學
SQL Server 教學
Xamarin.Forms 教學