【Blazor入門】Blazor初心者がログインからチャット機能まで付けてデプロイしてみた ～その3～

##前回
[【Blazor入門】Blazor初心者がログインからチャット機能まで付けてデプロイしてみた ～その1～](https://qiita.com/OXamarin/items/08a829d0ea77e2554231)
[【Blazor入門】Blazor初心者がログインからチャット機能まで付けてデプロイしてみた ～その2～](https://qiita.com/OXamarin/items/e34de662ad8958dcdab4)
の続きです。

前回までは、チャットページで`SignalR`を使用してブラウザ間で文字のやり取りが見れる所までやりました。
今回はログイン機能の見直しをしたいと思います。

##現在のログイン画面の問題点
前にもちらっと書きましたが、今のままだとユーザーIDとパスワードに何かしらの文字さえ入れておけば認証が通る仕組みです。
なので、事前に登録したユーザーID・パスワードを使用してログイン出来るようにしてみましょう。

##どのようにログイン情報を保持するか
ぱっと思いつく方法だと、自前のDBにユーザー情報を登録しておき、ログインボタン押下時にユーザーIDとパスワードでレコードが抽出できるか、というのが思い浮かびました。

ただそれだと面白くないというか、この記事を見ている人がパッと試しにくくなりますしそれよりもよい方法があるのではという漠然とした考えがあったので調べてみました。

###IDaaS
イマドキの流行りはこの`IDaaS`を使用しているみたいですね。

>`IDaaS(Identity as a Service)`の略です。読み方は「アイディーアース」または「アイダース」と呼びます。`SSO(シングルサインオン)`等のID認証をクラウド経由で提供するサービスです。最近はWebサービスやスマートフォン以外の機器が増えてきました。それぞれ、ID認証する必要が出てきており複雑化してきています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172223/5275cea6-374e-28c7-ec3a-141bd08ef886.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172223/968f1b6f-6559-d131-f880-f86a3f962826.png)
出典：[認証サービスiDaaSのFirebaseとAuth0 機能比較](https://www.threenext.com/auth-platform/)

簡単に纏めると、
昔はDBにIDとパスワードを保存してたけどサービスが多くなりすぎてそれを制御する開発者側が辛い、消費者側も覚えとくの辛い。
んじゃ一個どれか覚えておけば全部アクセスできるようにすればいいじゃん！ということを叶えてくれる仕組みなわけですね。

具体例を挙げると、
何かしらのサービスにログインする場合（ex.ニコニコ動画 とか）に、
ツイッター、グーグル、フェイスブック、アップル、それかユーザーIDとパスワード、どれか持ってればログインできるよ？って聞かれた方が使う側としてもログインが楽だよねって事です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172223/707c8e89-05eb-3a06-bb7a-f0de2d3bebb1.png)

じゃあどういう`IDaaS`があるんでしょうか。

####Amazon Cognito

[Amazon Cognito(コグニート)](https://aws.amazon.com/jp/cognito/)という`IdP(Identity Provider)`が`AWS`で提供されているということを知りました。
しかもなんと、5万回の認証まで無料(!!!)だそうです。これはよい。

でも、`Twitter`・`Instagram`などの認証には対応していないようです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172223/c757fea5-ced7-528b-c3d4-2037f2c52a60.png)

####Auth0
[Auth0](https://auth0.com/jp/)というのもあるようです。

- OIDC/OAuth2を利用した認証・認可が可能
- 画面はAuth0側で自由に作り込みが可能（アプリ内に画面を自分で作り込んで持つことも可能）
- ソーシャル連携が可能（ボタンをオン・オフすると、標準画面にソーシャルログインボタンが出現します）
- MFA(Multi-Factor Authentication : 多要素認証)が可能
- パイプライン・HOOK機能で、サインアップ・サインイン等、特定のアクションにlambda的なロジックの挟み込むことが出来る

出典：[Auth0 導入編](https://future-architect.github.io/articles/20200122/)

結構、高機能っぽいですね。
特に、画面が作れる所やダッシュボードからサンプルコードを落とせる所が凄くよいなと思いました。

しかし、最低でも月$15…。（開発アカウントだけなら無料）

####Firebase Authentication

最後に、[Firebase Authentication](https://firebase.google.com/docs/auth?hl=ja)を紹介。


>`Firebase Authentication`には、バックエンドサービス、使いやすいSDK、アプリでのユーザー認証に使用できる UI ライブラリが用意されています。Firebase Authentication では、パスワード、電話番号、一般的なフェデレーション ID プロバイダ（Google、Facebook、Twitter）などを使用した認証を行うことができます。

出典：[Firebase - ドキュメント](https://firebase.google.com/docs/auth?hl=ja)

こちらも機能が充実してそうですね。
調べたんですがイマイチ料金体系がよくわかりませんでした。
恐らく、`Firebase Authentication`単体でのサービス提供はしていなさそうです。

###選ばれたのはCognitoでした
機能面で言えば、恐らくは`Auth0`の方がよさげですが今回はお財布に優しい`Cognito`で実装してみたいと思います。


####Cognitoの実装前の準備
まずは [チュートリアル: ユーザープールの作成](https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/tutorial-create-user-pool.html) に沿って、ユーザープールの作成をしてください。
この時に生成される`PoolID`はメモしておいてください。

次に、ドメイン名を適当に登録する必要があるらしいです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172223/bd48b2ac-250c-44e2-5200-3b7c2e03d894.png)

ユーザーを作成する際に面倒なので、ポリシーからパスワードの強度を最大まで下げておくと楽です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172223/10f3beef-ec6b-48d7-2852-ddf52943fd0d.png)

そしてこれが最重要かつ壮大な罠。
アプリクライアント作成時に**「クライアントシークレットを生成」のチェックを外して**登録してください。シークレットを生成すると認証が上手くいきません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172223/d65198ed-8171-7488-8f5e-0e88a3710f36.png)

これはやらなくてもよいですが、ユーザーの作成をしておくと後々の説明が頭に入ってきやすくなるとおもいます。このユーザーは`Cognito`の認証を許可されたユーザーとなります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172223/1d3bf339-52a1-98fd-4fb7-708622c30441.png)

ここまで出来たら準備完了です。

####Cognitoの実装
ブログや`Qiita`で実装している記事がちらほらあったので余裕じゃん！と思ったら案外上手くいきませんでした。半ば心折れそうになってましたが無事に実装出来たので安心してください。

一番参考になったのは以下の動画です。
[AWS Cognito C# example](https://www.youtube.com/watch?v=ZMK-bADOxsg)

`NuGet`から以下二つのライブラリをインストールしてください。

- Amazon.AspNetCore.Identity.Cognito
- Amazon.Extensions.CognitoAuthentication

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172223/1414609a-656e-a4d2-82ac-cee2f838668e.png)

`Index.razor.cs`を以下の様にします。これで認証が通るようになります。

```C#:Index.razor.cs
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Amazon;
using Amazon.CognitoIdentityProvider;
using Amazon.CognitoIdentityProvider.Model;
using Amazon.Extensions.CognitoAuthentication;
using Amazon.Runtime;
using LoginTest.Models;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;

namespace LoginTest.Pages
{
    public partial class Index
    {
        #region 定数
        private const string PoolID = "us-east-1_hogehoge";
        private const string ClientID = "hogehogehogehoge";
        private RegionEndpoint Region = RegionEndpoint.USEast1;
        #endregion

        #region フィールド
        private string _loginErrorMessage;
        #endregion

        #region プロパティ
        /// <summary>
        /// Inject属性を指定することで、NavigationManagerのサービスの依存関係を挿入します。
        /// </summary>
        [Inject]
        public NavigationManager Navigation { get; set; }
        /// <summary>
        /// ログイン情報を保持
        /// </summary>
        public LoginData LoginData { get; set; }
        #endregion

        #region コンストラクタ
        public Index()
        {
            //Index.razorから参照するのでインスタンス生成をしておかないとエラーとなる
            LoginData = new LoginData();
        }
        #endregion

        #region メソッド
        /// <summary>
        /// Validate処理成功時に処理
        /// </summary>
        /// <param name="context"></param>
        public async Task OnValidSubmit(EditContext context)
        {
            Console.WriteLine($"OnValidSubmit()");

            var errorMessage = await SignInUserAsync();
            if (string.IsNullOrEmpty(errorMessage))
            {
                Navigation.NavigateTo("Chat", false);
            }
            else
            {
                _loginErrorMessage = errorMessage;
            }
        }
        /// <summary>
        /// Validate処理失敗時に処理
        /// </summary>
        /// <param name="context"></param>
        public void OnInvalidSubmit(EditContext context)
        {
            Console.WriteLine($"OnInvalidSubmit()");
        }
        /// <summary>
        /// Cognitoで使用できるユーザーを作成する
        /// </summary>
        /// <returns></returns>
        public async Task<string> SignUpUserAsync()
        {
            //Regionが「USEast1」なのは作成したPoolIDのプレフィックスと同じにしている為
            using var provider = new AmazonCognitoIdentityProviderClient(new AnonymousAWSCredentials(), Region);
            var signUpRequest = new SignUpRequest
            {
                ClientId = ClientID,
                Username = LoginData.UserID,
                Password = LoginData.Password,
                
                UserAttributes = new List<AttributeType>
                {
                    new AttributeType{Name = "email", Value="hogehoge@gmail.com"},
                },
            };

            try
            {
                var result = await provider.SignUpAsync(signUpRequest).ConfigureAwait(false);
                return string.Empty;
            }
            catch (Exception e)
            {
                return e.Message;
            }
        }
        /// <summary>
        /// Cognitoで登録したユーザかを判別する
        /// </summary>
        /// <returns></returns>
        private async Task<string> SignInUserAsync()
        {
            using var provider = new AmazonCognitoIdentityProviderClient(new AnonymousAWSCredentials(), RegionEndpoint.USEast1);

            var userPool = new CognitoUserPool(PoolID, ClientID, provider);
            var cognitoUser = new CognitoUser(LoginData.UserID, ClientID, userPool, provider);
            var authRequest = new InitiateSrpAuthRequest
            {
                Password = LoginData.Password,
            };

            try
            {
                var authResponse = await cognitoUser.StartWithSrpAuthAsync(authRequest).ConfigureAwait(false);
                var userRequest = new GetUserRequest
                {
                    AccessToken = authResponse.AuthenticationResult.AccessToken,
                };

                await provider.GetUserAsync(userRequest).ConfigureAwait(false);
                return string.Empty;
            }
            catch (Exception e)
            {
                return e.Message;
            }
        }
        #endregion
    }
}
```

最初にログイン画面を作った記事から追加しているのは`SignUpUserAsyncメソッド`・`SignInUserAsyncメソッド`です。

`SignUpUserAsyncメソッド`に関しては、先ほどの手順で示した`Cognito`へのユーザー追加をコード上から行っているだけです。ただ一つ注意点として、`SignUpUserAsyncメソッド`からユーザーを作成した場合、ユーザーの確認をしてやる必要があります。これをしないと認証が通りませんでした。

コード上からアカウントのステータスを更新できる方法も恐らくあるんでしょうけど、ぱっとは分かりませんでした。知っていれば教えて欲しいです。
![キャプチャ.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172223/ea7af63f-1ecf-9e92-8a05-118ca28a1e1f.jpeg)


また、`SignInUserAsyncメソッド`では`Cognito`に登録したユーザーが存在するかを取得しています。いずれのメソッドも、エラーメッセージを返すようにしており、問題がなければ空文字が返ってきます。

#####Cognitoでハマった所
①謎のRegion
`AmazonCognitoIdentityProviderClientクラス`の第二引数に`Region`という謎のパラメーターを渡さないといけません。よくわかりませんが、発行された`PoolID`の接頭辞に付いているものと同じ`RegionEndpoint`を渡せば良いようです。

②謎のエラー
認証時に、以下のエラーメッセージが返ってきていました。
>「Unable to verify secret hash for client」
>クライアントの秘密ハッシュを確認できません

これを読んで、確かにシークレットハッシュ渡してないなと思って渡すようにしたんですが、それでも解消されませんでした。調べてみたところ[
AWS Cognito User Pools のサインアップ時に NotAuthorizedException が出る (JavaScript)](https://qiita.com/noobar/items/6615501b035e47792227)という記事の中で言及されていますが、上記の手順で示した通り「クライアントシークレットを生成」のチェックを外す必要があったようです。

この記事が無ければ、無限にハマるところでした。

③パスワードのエラー
認証時に「validation errors detected: Value at 'password' failed to satisfy constraint: Member must have length greater than or equal to 6; Value at 'password' failed to satisfy constraint: Member must satisfy regular expression pattern: ^[\\S]+.*[\\S]+$"}」というエラーが出て何事かと思ったんですが、ユーザー登録時のパスワードは6桁以上にしようね、と怒られていました。

これも上記で設定した通りのポリシーに沿ってユーザー登録時にエラーを出してくれているみたいです。すごい。

あとはログイン認証失敗時のエラーを出す場所を追加で用意してあげます。
なんか他にいい書き方があれば教えてください。

```HTML:Index.razor
@if (!string.IsNullOrEmpty(_loginErrorMessage))
{
    <div class="form-group">
        <p style="color: red; font-weight: bold;">@_loginErrorMessage</p>
    </div>
}                                                          
```

これで実行してみます。

最初はパスワードをわざと間違え、エラーを表示させる。
次に、`Cognito`に登録しているユーザーID・パスワードであれば認証が通り、晴れてチャット画面へ遷移できることが確認できました。
![Counter.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172223/b0974a69-3d6b-b278-5903-7b9e6b874f0c.gif)


しかし、これでもまだ不完全です。
それはどこでしょうか？次回の記事をお楽しみに！

##まとめ
実装量としては全然大したことありませんでしたが、ドキュメントが少なさ過ぎてかなりてこずってしまいました。`Cognito`を使ってみたことがなかったので一つ使える知識が増えたのかなと思います。


**参考にさせて頂いたページ**

- [認証サービスiDaaSのFirebaseとAuth0 機能比較](https://www.threenext.com/auth-platform/)
- [Amazon Cognito(コグニート)](https://aws.amazon.com/jp/cognito/)
- [Auth0](https://auth0.com/jp/)
- [Firebase Authentication](https://firebase.google.com/docs/auth?hl=ja)
- [AWS Cognito C# example](https://www.youtube.com/watch?v=ZMK-bADOxsg)
- [C#でCognitoを利用し、ユーザー登録→認証を実装する。](https://qiita.com/boxboxbax/items/3a71e539ae301c21b3b2)
- [Windowsフォームアプリの認証にCognitoを使ってみた](https://qiita.com/afukuma/items/53d96cf83b553f4a5b56)
- [Amazon CognitoAuthentication 拡張ライブラリの例](https://docs.aws.amazon.com/ja_jp/sdk-for-net/v3/developer-guide/cognito-authentication-extension.html)
- [プログラミングせずにCognitoで新規ユーザー登録＆サインインを試してみる](https://dev.classmethod.jp/articles/sign-up-and-sign-in-by-cognito-with-awscli/)
- [AWS Cognito User Pools のサインアップ時に NotAuthorizedException が出る (JavaScript)](https://qiita.com/noobar/items/6615501b035e47792227)
- [UnityでCognito UserPoolsを使ってサインアップ・サインインを実現する](https://dev.classmethod.jp/articles/unity-cognito-authentication/)
- [チュートリアル: ユーザープールの作成](https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/tutorial-create-user-pool.html)