こんにちわ。所属がサークル名になってるけど、FJ-WAN内にアカウントを持っている人です。
冬コミに向けて、こんなサービス作ったんですけど。
その時に掲題の件でクソほどハマったので、技術メモしときます。何かの参考になれば。
#何がしたかったの?
TwitterのOAuthのAPIを叩いて、コールバックで戻って来たアカウントをDBに登録したかった。
つまり、右側のリンクをクリックすると、Twitterの認証に飛び、ユーザーが利用認証すると、自分のページにまた戻って来る、という仕組み。よくあるアレです。
#環境・要件
環境と要件について書いておきます。
##開発環境
項目 | 設定内容 |
---|---|
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はタヒんでくれ。
#ハマったこと
ハマった内容は大まかに二つあります。
1.Twitterの認証に飛ぶのに、Buttonを使う例はクソほどあったが、リンクを使う例が箸にも棒にも掛からぬ
2.いざ、oAuth(と書いてナムサンと読む)!!とリクエストを飛ばしても403で返って来る
割と心が折れそうになりましたが、なんとか解決しましたと。
1,2と順番に書いていきます。
##さてどうやってTwitterの認証に飛ばそうか
Formのボタンを使う例は公式を含めてクソほどある訳ですが。
僕のググり方が未熟なのか、どうもFormなんて面倒くさい事をせず、サクっとリンクで飛ばすやり方がわかりません。
ggrksとか言われそうですけど。いや、ググってますねんで。ホンマ。
で、__1日くらい潰れたんですが。__もう埒が明かないので、ソースサンプル見て自分で実装した。
結構案ずるより産むが易しでした。
[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にはこんな感じでよきにはらかう。
<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> でログイン中", ViewBag.TwitterID, ViewBag.TwitterDisplayName) : "<a href=\"/TwitterLogin\">Twitter経由でログインしてください</a>")</span>
</p>
cshtmlの中でゴリゴリ三項演算子使うと見づらいよね。ごめんね。
大事なところはここだけです。
<a href="/TwitterLogin">Twitter経由でログインしてください</a>
リンクでさっき作ったコントローラーにGetアクセスしてるだけです。簡単ね。難しく考えすぎたネ。
##いざ、oAuth(ナムサン)!!……あれ?
ほいじゃぁまぁ、と。デバッグで動かしてリンククリックしてみます。
あれ、画面真っ白なんやが……という現象に遭遇します。
困った時はログを見ます。
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あたりに書いてやります。
{
"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)ナンデ!?
寝食を忘れてググりにググった。これで__また1日潰れた。__
で、ようやく見つけたこちらの記事のここに、大事な事が書いてあった。
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)
えっちな誤字には目を背けつつ。
なるほど、
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に乗り換えましょう。そうすればやり直しで工数は倍になりますが万事うまくいきます」
「んな事は理解ってんだよバーーーーカ!!時間が無ぇんだよ!!ファイッ嫌いだ!!」
(総統閣下画像略)
__その試行錯誤で更に2日くらい潰れました__が。その過程はまあ要らんと思うので、結果だけ書きます。
[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");
}
}
ここ。ここに注目。
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の設定のベストプラクティスはこうなります。
signin-twitterは必ず入れること。でないと403になりますよと。ご注意。
#大変参考になった記事