ゼロからBlazor（プロジェクト作成～ログイン機能実装まで）

# Blazorとは
Blazorとは、一言で説明すると「C#ですべてを作る」ことを考えたフレームワークです。

昨今のJavaScriptライブラリの進化やフレームワークの機能の充実には目を見張るものがあります。
クライアントサイドでもダイナミックな処理が可能となった反面、非同期処理によるリアルタイム更新が当然のように求められ開発者に掛かる負担は大きくなっています。
MVCモデルを前提としたフレームワークが出てきた頃から、「クライアントサイドとサーバサイドを分離することでデザイナーとプログラマーの協業がしやすく云々」という説明をよく目にしますが、「いやデザイナーなんて社内にいねーから」というほとんで一人でやらなきゃいけない（当然JavaScriptも使いこなさなきゃいけない）技術者＠零細開発会社への福音になるかもしれないので、個人的には今後の主流になってほしいと願っています。

この記事では、Blazorのプロジェクト作成から、.NET Identity をカスタマイズした認証機能作成までの流れを見ていきたいと思います。
# 環境
- Windows 10
- .NET Core SDK 3.1.201
- Visual Studio 2019 16.5.4
- SQL Server 2019（とプロジェクト用のデータベース）

# Blazorプロジェクトの作成
#### 1. テンプレートの選択
Blazorアプリケーションは、ASP.NET Coreにプロジェクトテンプレートとして用意されています。
Visual Studioの「新しいプロジェクトの作成」で、プロジェクトテンプレートに「Blazor」アプリを選択します。
![image2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234496/2abf0a79-295a-de24-2beb-3486765e33d0.png)
#### 2. プロジェクト名の入力
プロジェクト名を入力して作成を押します
![image1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234496/ab92aac3-659b-62b7-b79d-669bb6605f25.png)
#### 3. 認証設定の変更
Blazor サーバーアプリを選択し、認証なしになっているので変更を選択します
![image3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234496/705d189f-3957-2926-683f-38abace1dc91.png)
個別のユーザーアカウント→アプリ内のストア ユーザーアカウントが選択されていることを確認し、OKをクリックしプロジェクトを作成して下さい
![image4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234496/de30147a-dde8-3bd3-a673-275ec9047df3.png)
これでプロジェクトが作成されます。
# プロジェクト構成の確認と実行
プロジェクトの構成はこんな感じになっていると思います。
![image5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234496/18f7ec0a-8062-7a96-4ff0-115afe935cc0.png)

| フォルダ名 | 役割 |
|:-----------|:------------|
| Areas       |認証ありにした場合、Identityフォルダが作成される|
| Data| Dbコンテキストがある|
| Pages| Razorのページファイルがある|
| Shared| レイアウトに関する共用ファイルがある|
| wwwroot| bootstrapやファビコンなどの静的ファイル置き場|

| ファイル名 | 役割 |
|:-----------|:------------|
| _Imports.razor|パッケージのインポートを行っている|
| App.razor| アプリ全体のレイアウトに使用するRazorファイルを決定している|
| appsettings.json| 接続文字列やログなどのアプリの設定情報|
| Program.cs| おなじみの起動プログラム|
| Startup.cs| アプリ起動時の初期化処理|

念のため、依存関係→パッケージからEntityFramework、Identity、SQLServer、Toolsなどのパッケージがインストール済みか確認してください。
もしなければここまでの手順をやりなおすか、NuGetパッケージマネージャーからインストールしてください。
![image6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234496/8054fda9-7c19-f23f-fe5f-aeac69485355.png)
#### サンプルプロジェクトの実行
F5をクリック、もしくは「IIS Express」をクリックしてサンプルプロジェクトを実行してみます。
![image7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234496/ca487c44-7e97-5dc2-93bd-b093fa072846.png)
サンプルでは「Home」、「Counter」、「Fetch data」の３つのページが用意されています。
各ページについて詳述はしませんが、（実行時に少し時間がかかるものの）ページ切り替えやボタン操作が非同期でサクサク動くことがわかると思います。
![image8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234496/97a75000-c0a9-8603-9f9f-37a6fc98555f.png)
ページ右上にあるRegister をクリックしてみてください。
ここでアカウントを登録することができます。Login と組み合わせてログインユーザー用の機能やページを設定することができます。
![image10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234496/047e347b-d2cd-9c9a-b740-378d976dfcd7.png)
Register とLogin の画面はデフォルトのテンプレートには存在しませんが、プロジェクト作成時に認証ありを選択したおかげで、追加されています。
これらは .NET Core の **Identity Core** という機能を利用して作成されています。

---------------------------------------
#### 補足 .NET の認証機能について
少し歴史的な説明をすると、.NET Core 以前の .NETでは Membership Provider という名前で認証機能は提供されていました。これがあることにより、ログインコントロールと連携して、ほぼノンコーディングで認証/認可処理が可能になりましたが、SQL Server以外のDBでは使いにくかったりカスタマイズがしにいといった課題がありました。その後、認証機能は.NET Idendity を経て .NET Idendity Core に進化し、かつての課題を解決しつつ多くの機能が追加されていきました。
登録とログイン周りだけでも二要素認証、パスワードリセット、パスワード忘れの処理が用意されていて、パスワードのハッシュ変換は当然のことながら、SNS認証、SQL Server以外への対応などなども盛り込まれています。

正直これらの機能を自力で実装するのは要件定義も実装もテストも困難。情報漏洩は企業イメージの失墜のみならず金銭問題に発展するリスクも抱えるため、認証にかける作業を省略できるのはありがたいの一言に尽きます（何かあったらマイクロソフトのバグですと言い訳できます、できません）。ただデフォルトでこれほど機能モリモリだと、作成されるDBも多く、よくわからないファイルも大量作成されますので、最低限使いこなすための知識は必要です。Identity の機能は多岐にわたるため、ここでは登録とログインのカスタマイズに絞って説明したいと思います。

---------------------------------------

# 認証画面のカスタマイズ
#### 1. スキャフォールディング
Areaフォルダ内にあるIdentityフォルダを右クリックし、追加→新規スキャフォールディングアイテムをクリックします
![image11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234496/f1317332-87b3-4895-471a-b47778d0b45f.png)
追加アイテムとして「ID」を選択し、追加をクリック（パッケージのインストールなどで少し待たされます）
![image12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234496/f73b549b-5d47-ca4c-a930-515c93027c5f.png)
#### 2. Login と Register をオーバーライド
オーバーライドするファイルを聞かれるので、「Account\Login」と「Account\Register」にチェック<br>データコンテキストクラスの欄で「＋」ボタンクリックし、コンテキストの型の名前を指定（ここではIdentityContext）
![image13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234496/accef02b-c7d0-fa3c-b63b-1b41e92d5231.png)
Areasフォルダ配下を確認すると、コンテキストファイルやRegister/Login ページが追加され、編集できるようになりました。
**スキャフォールディング前**
![image14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234496/6efd5428-561a-642f-b132-38ee94d8d41b.png)
**スキャフォールディング後**
![image15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234496/abe8536e-27d5-accc-26e4-bb404c26b2e4.png)
#### 3. Identity の統一
この状態でF5を押しビルドすると、Programファイルでエラーが発生します。
原因はもともとあったスタートアップファイルと、新たに追加された「IdentityHostingStartup.cs」でそれぞれIdentity をサービスに追加していてバッティングしているからです。
![image16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234496/645b421c-b909-678f-34b2-aab57b427d3e.png)
まずそれを解決するためにStartup.cs の方を消します。

```C#:Startup.cs
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddDbContext<ApplicationDbContext>(options =>
                options.UseSqlServer(
                    Configuration.GetConnectionString("DefaultConnection")));
            //services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
            //    .AddEntityFrameworkStores<ApplicationDbContext>();
            services.AddRazorPages();
            services.AddServerSideBlazor();
            services.AddScoped<AuthenticationStateProvider, RevalidatingIdentityAuthenticationStateProvider<IdentityUser>>();
            services.AddSingleton<WeatherForecastService>();
        }
```

IdentityHostingStartup の接続情報をDefaultConnectionに修正しておきます

```C#:IdentityHostingStartup.cs
        public void Configure(IWebHostBuilder builder)						
        {						
            builder.ConfigureServices((context, services) => {						
                services.AddDbContext<IdentityContext>(options =>						
                    options.UseSqlServer(						
                        context.Configuration.GetConnectionString("DefaultConnection")));						
						
                services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)						
                    .AddEntityFrameworkStores<IdentityContext>();						
            });						
```
再度F5を押しビルドし、エラーが起きないことを確認してください。

#### 4. データベースの設定
次に、自前のデータベースにユーザー情報を保存するために、DB接続情報を設定しているappsettings.json ファイルをいじります。
ここではスキャフォールディング時に追加された接続情報は削除し、DefaultConnectionに統一しています。

```json:appsettings.json
{				
  "ConnectionStrings": {				
    "DefaultConnection": "Data Source=（ローカルコンピューター名）;Initial Catalog=BLAZORAPP;Integrated Security=True;Pooling=False"				
  },				
  "Logging": {				
    "LogLevel": {				
      "Default": "Information",				
      "Microsoft": "Warning",				
      "Microsoft.Hosting.Lifetime": "Information"				
    }				
  },				
  "AllowedHosts": "*"				
}				
```

もしまだBlazorアプリ用のDBを用意していなかったらここで新規データベースを作成して、接続文字列を取得して下さい。
Visual Studio の表示→サーバーエクスプローラー→データ接続を右クリック→新しいSQL Serverデータベースの作成→サーバーを指定し、新しいデータベース名（ここではBLAZORAPP）を入力しOKをクリック
![コメント 2020-05-08 003801.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234496/2a3b4229-d603-3032-9fc3-529434ccd36b.png)
追加された接続情報を選択しF4キーもしくは右クリック→プロパティを押すとプロパティが表示されるので、接続情報をコピーして前述のappsettings.json ファイルに貼り付けます
![コメント 2020-05-08 003948.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234496/001cc676-b647-26ee-dfc1-4156f7c164db.png)
ではこの状態でサンプルページを実行し、Register から登録作業を行えるようになっているのでしょうか。
ここでは試しませんが個人情報を入力して登録しようとすると、「SqlException: オブジェクト名 'AspNetUsers' が無効です。」というエラーが発生します。
コードのプロパティとDBが結びついていないためです。

そのための認証に関するテーブル群をデータベースに作成しましょう。
EntityFrameworkに慣れている方には説明不要ですが、SQL Server Management Studio を開いたり、 SQLのコマンドを書いたりは一切しません。
具体的には、マイグレーションファイルの作成と、データベースの更新作業をパッケージマネージャーコンソールというVisual Studio 内のツールを使って行います。

---------------------------------------
#### 補足 Entity Framework について
ASP.NET Core でデータベースアクセスを行う場合、必ず利用することになるのが「Entity Framework Core」というフレームワークです。
Entity Framework は「ORM」と呼ばれる技術のためのフレームワークです。
一言でいうならばSQL を全く書かずにDBを利用できる機能です。
ORMの考え方が出てきた時は、「こんなの使わないで真面目にSQL覚えればいいのに」と思ったものですが、Visual Studio のインテリセンスにサポートされながら初めてLINQを書いた時は、あまりの便利さに泣きそうになりました。
マイグレーションやデータベースの更新はこの「Entity Framework Core」の機能により実行されます。

---------------------------------------

#### 5. マイグレーションファイルの作成と更新
Visual Studio で「ツール」→「NuGetパッケージマネージャ」→「パッケージマネージャーコンソール」の順に選択します。
画面下部にコマンドを実行できるウィンドウが現れるので、以下のコマンドを入力してEnterを押し実行してください。

```
Add-Migration Initial -Context IdentityContext
```
![image22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234496/05b22464-21f7-3135-3238-d7dc8fe246dc.png)
解説：
データベースとの差分情報を記述したファイル（マイグレーションファイル）を作成。
「Initial」とした箇所はファイルを識別するための名前で好きな名前を指定できる。コンテキストファイルが複数ある場合はコンテキスト名を指定する必要がある。

```
Update-Database -Context IdentityContext
```
![image23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234496/618a73a1-b4fa-8cf9-a341-91c73e78c222.png)
解説：
マイグレーションファイルを元にデータベースを更新

コンソールに以下のように表示されれば成功です。
DBにテーブルが追加され、無事にユーザーの登録ができるようになりました。
![image24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234496/856e99a8-97ac-5471-c101-609a3062fd1c.png)

あとは画面に表示されている英語を日本語に変えたりレイアウトを変えて終わりにしてもいいのですが、登録をしていて気になることがあります。

* 登録にEmailじゃなくて名前じゃダメ？
* メール使った2要素認証いらない。登録後即ホーム画面行かせてほしい。
* パスワードの要件チェック（バリデーション）多くないか？（6文字以上、大文字必須、数字必須、記号必須）

#### 6. 登録・ログイン画面をカスタマイズ
というわけで、こんなの使い勝手悪いのでカスタマイズです。
ここでは以下の変更を加えつつ、ページ制御も同時に実装したいと思います。

**ログイン・登録画面**

- Emailではなく名前で。（バリデーションなし）
- 名前もパスワードも半角英数字1文字以上ならOK（名前の重複は不可）
- 登録後のメール確認はしない

**ページ制御**

* ログインしていないと見れないページを作る
* 許可されていないページに行った場合、ログインページにリダイレクトさせる


まず、ページ制御で使うRedirectToLogin.razor をページフォルダに作成します。
Shared フォルダ上で右クリック 追加→新しい項目を選択
![image19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234496/c0cf36cd-6fe7-7580-506b-17eebb12f835.png)
Razorコンポーネントを選択し追加（ファイル名はRedirectToLogin.razor とします）
![image20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234496/c48a2151-1cfb-6507-8eff-3e8f50c48d4c.png)
RedirectToLogin.razor ファイルが追加されたら、以下の内容に書き換えてください。
未ログインユーザーの遷移先をログイン画面に設定しています。

```c#:RedirectToLogin.razor
@inject NavigationManager NavigationManager

@code{
    [Parameter]
    public string ReturnUrl { get; set; }
    protected override void OnInitialized()
    {
        ReturnUrl = "~/" + ReturnUrl;
        NavigationManager.NavigateTo($"Identity/Account/Login?returnUrl={ ReturnUrl}", forceLoad: true);
    }
}
```

App.razor コンポーネントを書き換えます。
書き換えているのは、`<NotAuthorized>`タグの部分です。ここでRedirectToLogin.razor コンポーネントにURLを引数として渡しています。
また `@using`で、先ほどのRedirectToLogin.Razor を追加した名前空間を指定し、`@inject` でNavigationManager をDI（オブジェクトの注入）します

```html:App.razor
@using Blazorアプリ名.Shared
@inject NavigationManager NavigationManager

<CascadingAuthenticationState>
    <Router AppAssembly="@typeof(Program).Assembly">
        <Found Context="routeData">
            <AuthorizeRouteView RouteData="@routeData"
                                DefaultLayout="@typeof(MainLayout)">
                <NotAuthorized>
                    @{
                        var returnUrl = NavigationManager.ToBaseRelativePath(NavigationManager.Uri);
                        <RedirectToLogin ReturnUrl="@returnUrl" />
                    }
                </NotAuthorized>
                <Authorizing>
                    Wait...
                </Authorizing>
            </AuthorizeRouteView>
        </Found>
        <NotFound>
            <LayoutView Layout="@typeof(MainLayout)">
                <p>Sorry, there's nothing at this address.</p>
            </LayoutView>
        </NotFound>
    </Router>
</CascadingAuthenticationState>
```
`<NotAuthorized>`タグを追加しています。ちなみに特定のHTMLタグを`<AuthorizeView></AuthorizeView>`で囲むと、ログイン前の状態では非表示にすることができます

ここで、試しにCounter.razor コンポーネントのトップに @attribute [Authorize] を加え、プログラムを起動し、Counterページにアクセスしてみてください。ログインページにリダイレクトされればOKです。

```html:Counter.razor
@attribute [Authorize]
@page "/counter"

<h1>Counter</h1>

<p>Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    private int currentCount = 0;

    private void IncrementCount()
    {
        currentCount++;
    }
}
```
登録画面とログイン画面の処理を変更するために、以下の4ファイルに手を加えます。
![コメント 2020-05-08 010705.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234496/150d50af-4a32-ad08-8560-5b671b858bec.png)

修正点のポイントは以下です。
1. プロパティのEmail を Name に変更
2. パスワードとメールのバリデーションを削除
（パスワードのチェックはライブラリ内部で行っているため、無理やり接尾辞「_Blazor」をくっつけて突破）
3. メール認証の箇所を削除

```C#:Login.cshtml.cs								
using System;								
using System.Collections.Generic;								
using System.ComponentModel.DataAnnotations;								
using System.Linq;								
using System.Text.Encodings.Web;								
using System.Threading.Tasks;								
using Microsoft.AspNetCore.Authorization;								
using Microsoft.AspNetCore.Authentication;								
using Microsoft.AspNetCore.Identity;								
using Microsoft.AspNetCore.Identity.UI.Services;								
using Microsoft.AspNetCore.Mvc;								
using Microsoft.AspNetCore.Mvc.RazorPages;								
using Microsoft.Extensions.Logging;								
								
namespace HelloBlazorGoodByeJavaScript.Areas.Identity.Pages.Account								
{								
    [AllowAnonymous]								
    public class LoginModel : PageModel								
    {								
        private readonly UserManager<IdentityUser> _userManager;								
        private readonly SignInManager<IdentityUser> _signInManager;								
        private readonly ILogger<LoginModel> _logger;								
								
        public LoginModel(SignInManager<IdentityUser> signInManager,								
            ILogger<LoginModel> logger,								
            UserManager<IdentityUser> userManager)								
        {								
            _userManager = userManager;								
            _signInManager = signInManager;								
            _logger = logger;								
        }								
								
        [BindProperty]								
        public InputModel Input { get; set; }								
								
        public IList<AuthenticationScheme> ExternalLogins { get; set; }								
								
        public string ReturnUrl { get; set; }								
								
        [TempData]								
        public string ErrorMessage { get; set; }								
								
        public class InputModel								
        {								
            [Required]								
            public string Name { get; set; }								
								
            [Required]								
            [DataType(DataType.Password)]								
            public string Password { get; set; }								
								
            [Display(Name = "Remember me?")]								
            public bool RememberMe { get; set; }								
        }								
								
        public async Task OnGetAsync(string returnUrl = null)								
        {								
            if (!string.IsNullOrEmpty(ErrorMessage))								
            {								
                ModelState.AddModelError(string.Empty, ErrorMessage);								
            }								
								
            returnUrl = returnUrl ?? Url.Content("~/");								
								
            // Clear the existing external cookie to ensure a clean login process								
            await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);								
								
            ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();								
								
            ReturnUrl = returnUrl;								
        }								
								
        public async Task<IActionResult> OnPostAsync(string returnUrl = null)								
        {								
            returnUrl = returnUrl ?? Url.Content("~/");								
								
            if (ModelState.IsValid)								
            {								
                // This doesn't count login failures towards account lockout								
                // To enable password failures to trigger account lockout, set lockoutOnFailure: true								
                Input.Password += "_Blazor";								
                var result = await _signInManager.PasswordSignInAsync(Input.Name, Input.Password, Input.RememberMe, lockoutOnFailure: false);								
                if (result.Succeeded)								
                {								
                    _logger.LogInformation("User logged in.");								
                    return LocalRedirect(returnUrl);								
                }								
                if (result.RequiresTwoFactor)								
                {								
                    return RedirectToPage("./LoginWith2fa", new { ReturnUrl = returnUrl, RememberMe = Input.RememberMe });								
                }								
                if (result.IsLockedOut)								
                {								
                    _logger.LogWarning("User account locked out.");								
                    return RedirectToPage("./Lockout");								
                }								
                else								
                {								
                    ModelState.AddModelError(string.Empty, "Invalid login attempt.");								
                    return Page();								
                }								
            }								
								
            // If we got this far, something failed, redisplay form								
            return Page();								
        }								
    }								
}								
```
```C#:Register.cshtml.cs					
using System;					
using System.Collections.Generic;					
using System.ComponentModel.DataAnnotations;					
using System.Linq;					
using System.Text;					
using System.Text.Encodings.Web;					
using System.Threading.Tasks;					
using Microsoft.AspNetCore.Authentication;					
using Microsoft.AspNetCore.Authorization;					
using Microsoft.AspNetCore.Identity;					
using Microsoft.AspNetCore.Identity.UI.Services;					
using Microsoft.AspNetCore.Mvc;					
using Microsoft.AspNetCore.Mvc.RazorPages;					
using Microsoft.AspNetCore.WebUtilities;					
using Microsoft.Extensions.Logging;					
					
namespace HelloBlazorGoodByeJavaScript.Areas.Identity.Pages.Account					
{					
    [AllowAnonymous]					
    public class RegisterModel : PageModel					
    {					
        private readonly SignInManager<IdentityUser> _signInManager;					
        private readonly UserManager<IdentityUser> _userManager;					
        private readonly ILogger<RegisterModel> _logger;					
        private readonly IEmailSender _emailSender;					
					
        public RegisterModel(					
            UserManager<IdentityUser> userManager,					
            SignInManager<IdentityUser> signInManager,					
            ILogger<RegisterModel> logger,					
            IEmailSender emailSender)					
        {					
            _userManager = userManager;					
            _signInManager = signInManager;					
            _logger = logger;					
            _emailSender = emailSender;					
        }					
					
        [BindProperty]					
        public InputModel Input { get; set; }					
					
        public string ReturnUrl { get; set; }					
					
        public IList<AuthenticationScheme> ExternalLogins { get; set; }					
					
        public class InputModel					
        {					
            [Required]					
            //[EmailAddress]					
            //[Display(Name = "Name")]					
            public string Name { get; set; }					
					
            [Required]					
            //[StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]					
            [DataType(DataType.Password)]					
            [Display(Name = "Password")]					
            public string Password { get; set; }					
					
            [DataType(DataType.Password)]					
            [Display(Name = "Confirm password")]					
            [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]					
            public string ConfirmPassword { get; set; }					
        }					
					
        public async Task OnGetAsync(string returnUrl = null)					
        {					
            ReturnUrl = returnUrl;					
            ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();					
        }					
					
        public async Task<IActionResult> OnPostAsync(string returnUrl = null)					
        {					
            returnUrl = returnUrl ?? Url.Content("~/");					
            ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();					
            if (ModelState.IsValid)					
            {					
                var user = new IdentityUser { UserName = Input.Name, EmailConfirmed = true };					
                Input.Password += "_Blazor";					
                var result = await _userManager.CreateAsync(user, Input.Password);					
                if (result.Succeeded)					
                {					
                    _logger.LogInformation("User created a new account with password.");					
					
                    //var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);					
                    //code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));					
                    //var callbackUrl = Url.Page(					
                    //    "/Account/ConfirmEmail",					
                    //    pageHandler: null,					
                    //    values: new { area = "Identity", userId = user.Id, code = code },					
                    //    protocol: Request.Scheme);					
					
                    //await _emailSender.SendEmailAsync(Input.Email, "Confirm your email",					
                    //    $"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");					
					
                    //if (_userManager.Options.SignIn.RequireConfirmedAccount)					
                    //{					
                    //    return RedirectToPage("RegisterConfirmation", new { email = Input.Email });					
                    //}					
                    //else					
                    //{					
                    await _signInManager.SignInAsync(user, isPersistent: false);					
                    return LocalRedirect(returnUrl);					
                    //}					
                }					
                foreach (var error in result.Errors)					
                {					
                    ModelState.AddModelError(string.Empty, error.Description);					
                }					
            }					
					
            // If we got this far, something failed, redisplay form					
            return Page();					
        }					
    }					
}					
```
```html:Login.cshtml
@page					
@model LoginModel					
					
@{					
    ViewData["Title"] = "Log in";					
}					
					
<h1>@ViewData["Title"]</h1>					
<div class="row">					
    <div class="col-md-4">					
        <section>					
            <form id="account" method="post">					
                <h4>Use a local account to log in.</h4>					
                <hr />					
                <div asp-validation-summary="All" class="text-danger"></div>					
                <div class="form-group">					
                    <label asp-for="Input.Name"></label>					
                    <input asp-for="Input.Name" class="form-control" />					
                    @*<span asp-validation-for="Input.Email" class="text-danger"></span>*@					
                </div>					
                <div class="form-group">					
                    <label asp-for="Input.Password"></label>					
                    <input asp-for="Input.Password" class="form-control" />					
                    @*<span asp-validation-for="Input.Password" class="text-danger"></span>*@					
                </div>					
                <div class="form-group">					
                    <div class="checkbox">					
                        <label asp-for="Input.RememberMe">					
                            <input asp-for="Input.RememberMe" />					
                            @Html.DisplayNameFor(m => m.Input.RememberMe)					
                        </label>					
                    </div>					
                </div>					
                <div class="form-group">					
                    <button type="submit" class="btn btn-primary">Log in</button>					
                </div>					
                @*<div class="form-group">					
                    <p>					
                        <a id="forgot-password" asp-page="./ForgotPassword">Forgot your password?</a>					
                    </p>					
                    <p>					
                        <a asp-page="./Register" asp-route-returnUrl="@Model.ReturnUrl">Register as a new user</a>					
                    </p>					
                    <p>					
                        <a id="resend-confirmation" asp-page="./ResendEmailConfirmation">Resend email confirmation</a>					
                    </p>					
                </div>*@					
            </form>					
        </section>					
    </div>					
    @*<div class="col-md-6 col-md-offset-2">					
        <section>					
            <h4>Use another service to log in.</h4>					
            <hr />					
            @{					
                if ((Model.ExternalLogins?.Count ?? 0) == 0)					
                {					
                    <div>					
                        <p>					
                            There are no external authentication services configured. See <a href="https://go.microsoft.com/fwlink/?LinkID=532715">this article</a>					
                            for details on setting up this ASP.NET application to support logging in via external services.					
                        </p>					
                    </div>					
                }					
                else					
                {					
                    <form id="external-account" asp-page="./ExternalLogin" asp-route-returnUrl="@Model.ReturnUrl" method="post" class="form-horizontal">					
                        <div>					
                            <p>					
                                @foreach (var provider in Model.ExternalLogins)					
                                {					
                                    <button type="submit" class="btn btn-primary" name="provider" value="@provider.Name" title="Log in using your @provider.DisplayName account">@provider.DisplayName</button>					
                                }					
                            </p>					
                        </div>					
                    </form>					
                }					
            }					
        </section>					
    </div>*@					
</div>					
					
@section Scripts {					
    <partial name="_ValidationScriptsPartial" />					
}					
```
```html:Register.cshtml				
@page				
@model RegisterModel				
@{				
    ViewData["Title"] = "Register";				
}				
				
<h1>@ViewData["Title"]</h1>				
				
<div class="row">				
    <div class="col-md-4">				
        <form asp-route-returnUrl="@Model.ReturnUrl" method="post">				
            <h4>Create a new account.</h4>				
            <hr />				
            <div asp-validation-summary="All" class="text-danger"></div>				
            <div class="form-group">				
                <label asp-for="Input.Name"></label>				
                <input asp-for="Input.Name" class="form-control" />				
                @*<span asp-validation-for="Input.Name" class="text-danger"></span>*@				
            </div>				
            <div class="form-group">				
                <label asp-for="Input.Password"></label>				
                <input asp-for="Input.Password" class="form-control" />				
                @*<span asp-validation-for="Input.Password" class="text-danger"></span>*@				
            </div>				
            <div class="form-group">				
                <label asp-for="Input.ConfirmPassword"></label>				
                <input asp-for="Input.ConfirmPassword" class="form-control" />				
                <span asp-validation-for="Input.ConfirmPassword" class="text-danger"></span>				
            </div>				
            <button type="submit" class="btn btn-primary">Register</button>				
        </form>				
    </div>				
    @*<div class="col-md-6 col-md-offset-2">				
        <section>				
            <h4>Use another service to register.</h4>				
            <hr />				
            @{				
                if ((Model.ExternalLogins?.Count ?? 0) == 0)				
                {				
                    <div>				
                        <p>				
                            There are no external authentication services configured. See <a href="https://go.microsoft.com/fwlink/?LinkID=532715">this article</a>				
                            for details on setting up this ASP.NET application to support logging in via external services.				
                        </p>				
                    </div>				
                }				
                else				
                {				
                    <form id="external-account" asp-page="./ExternalLogin" asp-route-returnUrl="@Model.ReturnUrl" method="post" class="form-horizontal">				
                        <div>				
                            <p>				
                                @foreach (var provider in Model.ExternalLogins)				
                                {				
                                    <button type="submit" class="btn btn-primary" name="provider" value="@provider.Name" title="Log in using your @provider.DisplayName account">@provider.DisplayName</button>				
                                }				
                            </p>				
                        </div>				
                    </form>				
                }				
            }				
        </section>				
    </div>*@				
</div>				
				
@section Scripts {				
    <partial name="_ValidationScriptsPartial" />				
}				
```
ではあらためて登録してみます。EmailだったところがNameに変わっているので名前を入力し、パスワードは単純に「1234」を入れています。
![image25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234496/a9bc0419-b631-655e-338b-f706cc0d2c4b.png)
ページの右上に登録時に入力した名前が表示され、また先ほど見れなかったCounterページにアクセスできていれば成功です。
ログアウトやログインもできるようになっているはずです。
![image26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234496/68e89984-6d4f-5148-5883-7933b592d18d.png)
ログイン画面
![image27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234496/6d7d642d-dd67-80a3-0ae0-a0a247c51c34.png)
本格的なカスタマイズにはC#以外にRazor記法や タグヘルパーを駆使する必要がありますが、ひとまずBlazorアプリのプロジェクト作成からログイン機能実装までを行うことができました。


次回はDBの参照、登録、削除といったCRUD処理を追加する予定です。コンテキストファイルが複数あるので、少しだけ工夫が必要です。