ASP.NET CORE MVC + CentOS7 + httpd(Apache)でTwitter経由でoAuthログインしたい

こんにちわ。所属がサークル名になってるけど、FJ-WAN内にアカウントを持っている人です。

冬コミに向けて、[こんなサービス](https://cloud.こぁ.jp)作ったんですけど。
その時に掲題の件でクソほどハマったので、技術メモしときます。何かの参考になれば。

#何がしたかったの？
TwitterのOAuthのAPIを叩いて、コールバックで戻って来たアカウントをDBに登録したかった。
つまり、右側のリンクをクリックすると、Twitterの認証に飛び、ユーザーが利用認証すると、自分のページにまた戻って来る、という仕組み。よくあるアレです。

ここを
![image.png](https://qiita-image-store.s3.amazonaws.com/0/158353/9d5c7eb7-1f98-245e-298f-40e327d1c345.png)

こうして
![image.png](https://qiita-image-store.s3.amazonaws.com/0/158353/b81e24e7-fa96-b86e-2e94-8856a015fbd8.png)

こうじゃ！
![image.png](https://qiita-image-store.s3.amazonaws.com/0/158353/41a244c7-31b1-8b4a-3824-79e22202177e.png)

#環境・要件
環境と要件について書いておきます。
##開発環境
| 項目 | 設定内容 |
|:-:|:-:|
| OS | Windows10 Pro 1803 |
| SDK | Visual Studio 2017 CE（会社のサブスクリプション使うと怒られるからね！！） |
| .Net Framework | .Net Core 2.0 |

##本番環境
| 項目 | 設定内容 |
|:-:|:-:|
| OS | CentOS 7.3(チェリーブロッサムインターネットのVPS) |
| SSL | TLSV1.2(証明書はLet's Encrypt先生を利用) |
| .Net Framework | .Net Core 2.0 |
| Webサーバー | Apache(httpd リバースプロキシ) + Kestrel(ASP.NET CORE動作用) |
| DBサーバー | MySQL(わざわざMariaDBは殺した) |

##要件

- MVCだよ。
- SSLは必須（今日日https付いてないとかアカン）。
- TwitterのoAuthを使ってログイン。サーバーにはユーザーシークレットやらパスワードやらは残さない。
- FirefoxとCromeで見れればいい。IEはﾀﾋんでくれ。

#ハマったこと
ハマった内容は大まかに二つあります。
__１．Twitterの認証に飛ぶのに、Buttonを使う例はクソほどあったが、リンクを使う例が箸にも棒にも掛からぬ__

__２．いざ、oAuth（と書いてナムサンと読む）！！とリクエストを飛ばしても403で返って来る__

割と心が折れそうになりましたが、なんとか解決しましたと。
１，２と順番に書いていきます。

##さてどうやってTwitterの認証に飛ばそうか
Formのボタンを使う例は[公式を含めて](https://docs.microsoft.com/ja-jp/aspnet/core/security/authentication/social/twitter-logins?view=aspnetcore-2.2)クソほどある訳ですが。
僕のググり方が未熟なのか、どうもFormなんて面倒くさい事をせず、サクっとリンクで飛ばすやり方がわかりません。
ｇｇｒｋｓとか言われそうですけど。いや、ググってますねんで。ホンマ。

で、__１日くらい潰れたんですが。__もう埒が明かないので、ソースサンプル見て自分で実装した。
結構案ずるより産むが易しでした。

```RootController.cs
        [HttpGet("~/TwitterLogin")]
        public IActionResult TwitterLogin()
        {
            try
            {
                ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12 | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls;
                return Challenge(new AuthenticationProperties { RedirectUri = "https://cloud.xn--k8j9a.jp/Twitter" }, "Twitter");
            }
            catch
            {
                return Redirect("~/InternalServerError");
            }
        }

```

こんなコントロールメソッドを用意してあげます。
__デバッグの時はRedirectUriを自分のローカルデバッグ環境にしてあげてね。__

で、VIEWにはこんな感じでよきにはらかう。

```_Layout.cshtml
<p class="header_text">
  <span class="header_text_twitter"><i class="fab fa-twitter"></i> @Html.Raw(ViewBag.AccessToken != null ? string.Format("<a href=\"https://twitter.com/{0}\" target=\"_blank\">{1}</a>&nbsp;でログイン中", ViewBag.TwitterID, ViewBag.TwitterDisplayName) : "<a href=\"/TwitterLogin\">Twitter経由でログインしてください</a>")</span>
</p>
```

cshtmlの中でゴリゴリ三項演算子使うと見づらいよね。ごめんね。
大事なところはここだけです。

```.cshtml
<a href="/TwitterLogin">Twitter経由でログインしてください</a>
```

リンクでさっき作ったコントローラーにGetアクセスしてるだけです。簡単ね。難しく考えすぎたネ。

##いざ、oAuth（ナムサン）！！……あれ？
ほいじゃぁまぁ、と。デバッグで動かしてリンククリックしてみます。
あれ、画面真っ白なんやが……という現象に遭遇します。
困った時はログを見ます。

```/var/log/messages
dotnet-example: Connection id "0HLITC18U3J6L", Request id "0HLITC18U3J6L:00000001": An unhandled exception was thrown by the application.
dotnet-example: System.ArgumentException: The 'ConsumerKey' option must be provided.
dotnet-example: Parameter name: ConsumerKey
dotnet-example: at Microsoft.AspNetCore.Authentication.Twitter.TwitterOptions.Validate()
dotnet-example: at Microsoft.AspNetCore.Authentication.RemoteAuthenticationOptions.Validate(String scheme)
dotnet-example: at Microsoft.AspNetCore.Authentication.AuthenticationHandler`1.<InitializeAsync>d__42.MoveNext()
```
__OOPS!!!__
ConsumerKeyが見当たらないとな。ユーザーシークレットに書くだけじゃダメなんかい！！

ということで、appsettings.jsonあたりに書いてやります。

```appsettings.json
{
  "Logging": {
    "IncludeScopes": false,
    "LogLevel": {
      "Default": "Warning"
    }
  },
  "Authentication:Twitter:ConsumerKey": "イヤーーーーン",
  "Authentication:Twitter:ConsumerSecret": "見ちゃらめぇ"
}
```

ConsumerKeyとConsumerSecretは自分の環境に置き換えて書いてあげてね。ボクのも見られたら困るので、適当な文字列に置き換えてあります。

気を取り直して、もう一度チャレンジです。
……やっぱり画面白いんやが。
まぁ、もう一度ログ見ましょうな。

```
dotnet-example: #033[41m#033[30mfail#033[39m#033[22m#033[49m: Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware[0]
dotnet-example: An unhandled exception has occurred: Response status code does not indicate success: 403 (Forbidden).
dotnet-example: System.Net.Http.HttpRequestException: Response status code does not indicate success: 403 (Forbidden).
dotnet-example: at System.Net.Http.HttpResponseMessage.EnsureSuccessStatusCode()
dotnet-example: at Microsoft.AspNetCore.Authentication.Twitter.TwitterHandler.<ObtainRequestTokenAsync>d__12.MoveNext()
```

__OOOOOOOOOOOOOOOPS!!!!__
アイエエエエエエ！？ナンデ！？403(Forbidden)ナンデ！？
寝食を忘れてググりにググった。これで__また１日潰れた。__
で、ようやく見つけた[こちらの記事](https://wallstudio.hateblo.jp/entry/2018/06/30/044839)のここに、大事な事が書いてあった。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/158353/d8ed16f3-b258-c121-6501-380ccea78613.png)

>httpsを受け取ってhttpに投げ直す時にヘッダーが全部死にます．どういうことかというと，nginxがhttpsで受けた際には，"プロトコル=https;クライアント=133.5.6.44;リモートホスト:www.wallstudio.work;" みたいになっているのですが，Kestrelが受け取るのは"プロトコル=http;クライアント=127.0.0.1;リモートホスト:localhost;" みたいなことになってしまいます．性的なHTMLを返すだけのような場合には大して問題になりませんが，ユーザー認証を使っていて特にTwitterはCallbackURLが違うと403エラーで弾かれてしまいます．プログラムで正しいCallbackURLを生成できるように，ヘッダーを正しく転送してあげる必要があるのです．（地雷その2）

えっちな誤字には目を背けつつ。
なるほど、

```.cs
Challenge(new AuthenticationProperties { RedirectUri = "https://cloud.xn--k8j9a.jp/Twitter" }, "Twitter");
```
でTwitterAPIを叩くときに、Kestrelくんは自分のURI（有体に言うと、http://localhost:5000 ）でCallBackのURIを投げてしまうと。
うーんこの。__やはりMS不能与（アタエランネーワ）__。

これ、どうにかして、Kestrelくんが認識している自分のURIをリバースプロキシ元のサーバールート（つまりhttps://cloud.こぁ.jp ）にしてやらんといけないということです。

これ、__Webサーバーがnginxの人はそのまま丸コピでイケるんちゃうの__。
しかし、__わちきはApacheなのである！！！__
参考にはなるが、丸コピはできぬのだ！！！！！つらい！！！！
__ググってもApacheの場合のいいやり方が載っていない！！クソァ！！__
でもここまできて諦めるのは。なんとかしたい。するしかない。

__「閣下。今からApacheからnginxに乗り換えましょう。そうすればやり直しで工数は倍になりますが万事うまくいきます」__
__「んな事は理解ってんだよバーーーーカ！！時間が無ぇんだよ！！ファイッ嫌いだ！！」__
（総統閣下画像略）

__その試行錯誤で更に２日くらい潰れました__が。その過程はまあ要らんと思うので、結果だけ書きます。

```RootController.cs
        [HttpGet("~/TwitterLogin")]
        public IActionResult TwitterLogin()
        {
            try
            {
                ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12 | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls;
                // 追加はじまり
                Request.Scheme = "https";
                Request.Host = new HostString("cloud.xn--k8j9a.jp");
                // 追加おわり
                return Challenge(new AuthenticationProperties { RedirectUri = "https://cloud.xn--k8j9a.jp/Twitter" }, "Twitter");
            }
            catch
            {
                return Redirect("~/InternalServerError");
            }
        }
```
ここ。ここに注目。

```.cs
Request.Scheme = "https";
Request.Host = new HostString("cloud.xn--k8j9a.jp");
```

オレオレ偽装です。なんてこった。ウカツ。実際ダサイ。
ま、まぁ、__実現できれば方法は何だっていいのだ。__
（なんかもっとスマートな方法があったら教えてくださいオナシャス）

こうすると、Kestrelくんは無事自分のプロトコルが__https__でかつ、自分のホストアドレスが__cloud.こぁ.jp__だと思い込んでくれます。

これで、ようやくTwitterの認証が通って、~/Twitterのコールバックコントローラーに飛んできてくれるようになりましたとさ。めでたしめでたし。

#タネ~~明石~~明かし
Twitterのアプリの設定画面のコールバックアドレスとKestrelくんが送り付けるURLが合っていないと、403になるようです。

__RedirectUriにURLを設定しないと、/signin-twitterめがけてリダイレクトが飛ぶよん__などと公式ドキュメントにはありましたが。
これマジウソッパチ。__結局RedirectUriに設定しても、/signin-twitterには飛ぶ__んですよ。一応。
__その応答の中に更にRedirectUriに対するリダイレクトが仕込まれていて、最終的にRedirectUriに設定したところに飛ぶ__、というのが正しい動き。

なので、Twitterの設定のベストプラクティスはこうなります。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/158353/3a1d6853-1381-5be7-de10-287a81f9b718.png)
signin-twitterは必ず入れること。でないと403になりますよと。ご注意。

#大変参考になった記事
- [ASP.NET Core MVC via nginxでTwitterアプリを作るまでの地雷原(うｐﾊｼ氏)](https://wallstudio.hateblo.jp/entry/2018/06/30/044839)
- [ASP.NET Core での twitter 外部ログインのセットアップ(MS公式ドキュメント)](https://docs.microsoft.com/ja-jp/aspnet/core/security/authentication/social/twitter-logins?view=aspnetcore-2.2)