【C#】コンソールアプリでTwitchAPIのOAuth認証アクセストークンを取得する

先日、C#のWPFでTwitch配信のコメント読み上げ＆ビューワーアプリを作ったという記事を書きました。
https://qiita.com/mojomojopon/items/27b16cc17538549f25c5

今回はこのビューワーを作るにあたって行った、C#上からTwitchAPIのOAuth認証アクセストークンを取得する方法について書いていきます。
記事で紹介する項目としては
 1. Twitch Developerでアプリの登録申請を行う
 2. C#コンソールアプリからOAuth認証Urlを開き、ローカルサーバーを立ち上げてリダイレクトでアクセストークンを受け取る

といった形です。
ローカルサーバーを立ち上げてリダイレクトでアクセストークンを受け取るというくだりは、URLリダイレクトでトークン受け渡しをするほかのOAuth認証サービスでも使いまわせるんじゃないかと思います（試してはいない）
あくまで私が行ったやり方ですのでベストプラクティスというものではありませんので、そこはご了承ください。

## アプリ登録申請
OAuth認証を行うためにはまずはアプリの登録申請が必要です。
https://dev.twitch.tv/ でアプリの登録申請を行います。
お持ちのTwitchアカウントでログインし、認証後右上のConsoleを開いてアプリケーション登録画面を開いてください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/107527/f84392c6-bc63-2dc0-898f-468b43746e43.png)

こんな感じの画面になるので名前にアプリの名前を入れてください。
リダイレクトURLは今回はひとまず「 http://localhost:15555 」としてください（後ほど説明します）
カテゴリーは今回はC#のコンソールアプリで使用するので「Application Integration」で良いと思います。

登録したら、コンソールからアプリの管理画面を開き、クライアントIDを確認してください。
後ほどC#のコンソールアプリ内で必要となります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/107527/fb13d36a-cf92-7c60-6eb6-84862f6ae945.png)

以上でアプリの登録申請は完了です。

## C#のコンソールアプリでトークンを受け取る
次にOAuth認証をしてアクセストークンを受け取るのですが、アプリ申請登録の際に入力したURLにユーザーが認証後リダイレクトされてアクセストークンが渡されるという流れになります。
C#のコンソールアプリ側でアクセストークンを受け取るには、ローカルでウェブサーバーを立ち上げる必要があります。
そこで、今回はHttpListenerを使ってサーバーを立ち上げてパラメータを受け取ります。
Visual Studioでコンソールアプリケーションプロジェクトを新規作成し、Programクラスを以下のようにします。

```
    internal class Program
    {
        static void Main(string[] args)
        {
            // Twitch Developer Consoleに表示されるClientIDを入力
            var clientId = "";
            var redirectUrl = "http://localhost:" + SimpleWebServer.Port;

            SimpleWebServer server = new SimpleWebServer();
            server.OnTokenReceived += (str) =>
            {
                Console.WriteLine("トークン取得:" + str);
            };
            server.Start();

            var oauthUrl = string.Format("https://id.twitch.tv/oauth2/authorize?client_id={0}&redirect_uri={1}&response_type=token&scope=chat:read%20chat:edit", clientId, redirectUrl);
            Console.WriteLine("URL:" + oauthUrl);
            ProcessStartInfo pi = new ProcessStartInfo()
            {
                FileName = oauthUrl,
                UseShellExecute = true,
            };
            Process.Start(pi);

            Console.WriteLine("トークン取得まで待機");
            Console.ReadLine();

            server.Close();
        }

        /// <summary>
        /// ブラウザからのリダイレクトでアクセストークンの取得を行うためのサーバーの起動・管理および取得したアクセストークンの通知を行うクラス
        /// </summary>
        private class SimpleWebServer
        {
            public static readonly int Port = 15555;

            private HttpListener _listener = new HttpListener();
            public event Action<string> OnTokenReceived;

            public void Start()
            {
                _listener.Prefixes.Add("http://localhost:" + Port.ToString() + "/");
                _listener.Start();

                WaitForRequest();
            }

            public void Close()
            {
                _listener.Stop();
                _listener.Close();
            }

            private async void WaitForRequest()
            {
                HttpListenerContext context = null;
                try
                {
                    context = await _listener.GetContextAsync();
                }
                catch (Exception ex)
                {
                    // エラー時の処理
                }

                if (context != null)
                {

                    var response = context.Response;
                    response.StatusCode = (int)HttpStatusCode.OK;
                    response.ContentType = "text/html";

                    byte[] buffer = Encoding.UTF8.GetBytes(CreateResponseHtml());
                    response.OutputStream.Write(buffer, 0, buffer.Length);
                    response.OutputStream.Close();

                    GetAccessToken(context);

                    WaitForRequest();
                }
            }

            private string CreateResponseHtml()
            {
                return $@"
                <!DOCTYPE html><html><head></head><body></body>
                  <script>
                    var hash = location.hash;
                    if(hash.length > 0) 
                    {{
                      var params = hash.split('&');     
                      if(params.length > 0 && params[0].match(/#access_token/)) 
                      {{
                        var token = params[0].split('=')[1];
                        window.location.replace(`http://localhost:{Port}/?access_token=` + token);
                      }}
                    }}
                  </script>
                </html>
            ";
            }

            private void GetAccessToken(HttpListenerContext context)
            {
                var request = context.Request;

                if (!string.IsNullOrEmpty(request.QueryString["access_token"]))
                {
                    OnTokenReceived?.Invoke(request.QueryString["access_token"]);
                }
            }
        }
    }
```

以下、コードの処理についてひとつずつ、解説していきます。

## ローカルサーバーを立ち上げる
SimpleWebServerクラスは、アプリ登録申請で入力したURL「 http://localhost:15555/ 」へのリダイレクトでトークンを受け取るための処理を行います。
処理の流れを解説します。

## サーバー部分

```
public void Start()
{
    // プレフィックスの登録
    _listener.Prefixes.Add("http://localhost:" + Port.ToString() + "/");
    _listener.Start();

    // リクエストを受け付ける
    WaitForRequest();
}
```
はじめにプレフィックスの登録を行います。今回は「 http://localhost:15555/ 」を登録してるので、こちらがリクエストを受け付けるURLとなります。
その後、WaitForRequest()でリクエスト受付を開始します。

```
HttpListenerContext context = null;
try
{
    context = await _listener.GetContextAsync();
}
catch (Exception ex)
{
    // エラー時の処理
}
```

HttpListener.GetContextAsync()でリクエストとレスポンスを含むコンテキストを取得するのですが、1回以上取得を行った後にHttpListener.Close()を行うと必ずエラーが出てしまうためやむを得ずtry&catchで対処しています。

```
var response = context.Response;
response.StatusCode = (int)HttpStatusCode.OK;
response.ContentType = "text/html";

// レスポンスHtmlを作成
byte[] buffer = Encoding.UTF8.GetBytes(CreateResponseHtml());
response.OutputStream.Write(buffer, 0, buffer.Length);
response.OutputStream.Close();
```

リクエスト時に返却するHtmlを作成する処理です。
CreateResponseHtml()で返却するHtmlを作成しています。

```
private string CreateResponseHtml()
{
    return $@"
    <!DOCTYPE html><html><head></head><body></body>
      <script>
        var hash = location.hash;
        if(hash.length > 0) 
        {{
          var params = hash.split('&');     
          if(params.length > 0 && params[0].match(/#access_token/)) 
          {{
            var token = params[0].split('=')[1];
            window.location.replace(`http://localhost:{Port}/?access_token=` + token);
          }}
        }}
      </script>
    </html>
    ";
}
```
リクエストしたブラウザ上で表示されるHtmlを作成する処理です。
javascriptでlocation.hashを取得して、パラメータを取得してから再度URLリダイレクトを行う処理を含んでます。
なんでこんな事をしているかというと、ブラウザが取得したFragment Part（URLの#から始まるパラメータ部分）はClientサイドでの利用のみと定められており、C#のHttpListenerクラスで直接受け取る事が不可能だったためです。
制約の裏をかいてるみたいな若干気が引ける処理ですが、今回はブラウザ上で取得したFragmentパラメータをクエリパラメータを含むURLに書き換えて再度リダイレクトすることでHttpListenerでのパラメータ受け取りのための対処としています。

```
private void GetAccessToken(HttpListenerContext context)
{
    var request = context.Request;

    if (!string.IsNullOrEmpty(request.QueryString["access_token"]))
    {
        OnTokenReceived?.Invoke(request.QueryString["access_token"]);
    }
}
```

リクエストにaccess_tokenのクエリが含まれてないかチェックして、含まれていたらイベントで通知する処理です。
サーバー部分の処理は以上です。

## Main

```
            // Twitch Developer Consoleに表示されるClientIDを入力
            var clientId = "";
            var redirectUrl = "http://localhost:" + SimpleWebServer.Port;

            // リダイレクトを受け取るローカルサーバーを立ち上げる
            SimpleWebServer server = new SimpleWebServer();
            // トークン取得時の処理を登録
            server.OnTokenReceived += (str) =>
            {
                Console.WriteLine("トークン取得:" + str);
            };
            server.Start();
```
clientIdには先ほどアプリ申請画面で取得したClient Idを入力してください。
OAuth認証のための認証Url作成時に必要です。


```
// TwitchAPIのOAuth認証URLをブラウザで開くための処理
var oauthUrl = string.Format("https://id.twitch.tv/oauth2/authorize?client_id={0}&redirect_uri={1}&response_type=token&scope=chat:read%20chat:edit", clientId, redirectUrl);
ProcessStartInfo pi = new ProcessStartInfo()
{
    FileName = oauthUrl,
    UseShellExecute = true,
};
Process.Start(pi);
```
認証Urlをブラウザで開く処理です。URLのscope以下のパラメータで権限を得たいscopeを指定できます。
今回はchat:read（ライブチャットのルームメッセージを表示）とchat:edit（ライブチャットとルームメッセージを送信）を指定してみました。

## 動かす
以上のコードをビルドして立ち上げる事で、コンソール起動と同時に認証サイトがブラウザ上で表示され、許可を押すことでアプリ申請で指定したローカルホストにリダイレクトされ、コンソール上に取得したアクセストークンが表示されます。
TwitchAPIを利用する際にはこちらのトークンが必要となります。
後日、TwitchLibというライブラリを利用して、実際にトークンを利用してAPIにアクセスする方法についても記事にしようと思います。

## ソースコード
https://github.com/mojopon/TwitchOAuthPractice

## 参考
https://qiita.com/pasta04/items/2ff86692d20891b65905