ASP.NET Core 2.0 Authentication

# ASP.NET Core 2.0 Authentication
ASP.NET Core は認証周りもフレームワークがサポートしていてかなり便利なのですが、サンプルで提示されている以外のことをやろうとするとドキュメント不足に陥りました。調べた結果をまとめます。


## 前提知識
* ASP.NET Core MVC/Web API のチュートリアルくらいは分かる


## 本題
### 認証ハンドラーの登録
ASP.NET Core 2.0 では認証のための実装はサービスとしてすでに用意されています。そちらの基本部分の登録は `AddAuthentication` メソッドで行います。

```csharp
internal sealed class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddAuthentication();
    }

    // ...
}
```

ただし、登録されているのはフレームワークで、例えば Cookie にトークンを埋め込むなどの実装は認証ハンドラーを登録する必要があります。

注) **認証ハンドラー** が正式な用語かはわかりません。ただ、これらは `IAuthenticationHandler` インターフェイスを実装しているため、ここでは認証ハンドラーと呼びます。

この認証ハンドラーは標準でいくつか用意されています。以下はそれを抜き出した表です。

| 実装名 | 目的
| ---- | ----
| CookieAuthenticationHandler | Cookie でトークンを管理する
| JwtBearerHandler | HTTP Authorization ヘッダーの Bearer でトークンを管理する
| OAuthHandler | OAuth ログインを管理する
| OpenIdConnectHandler | Open ID Connect ログインを管理する
| MicrosoftAccountHandler | Microsoft Account ログインを管理する

大雑把に分類すると、トークンをブラウザーとやり取りする `Cookie` と `JwtBearer`、外部認証からトークンを受け取るための `OAuth` や `OpenIdConnect` や `MicrosoftAccount` の二つに別れます。(厳密には `JwtBearer` はトークンのブラウザーへ発行ができません)

比較的分かりやすい `JwtBearerHandler` を使って説明します。

`AddAuthentication` メソッドの戻り値は `AuthenticationBuilder` オブジェクトとなっていて、ここで認証処理用のハンドラーを登録します。

```csharp
internal sealed class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddAuthentication()
            .AddJwtBearer();
    }

    // ...
}
```

登録した認証ハンドラーは `HttpContext` オブジェクトから呼び出せるようになります。認証されていない状態 (HTTP Code 401) を返す `ChallengeAsync` メソッドを呼び出します。(認証ハンドラーとして `JwtBearer` を明示的に指定しています)

```csharp
internal sealed class Startup
{
    // ...

    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        app.Run(context => context.ChallengeAsync(JwtBearerDefaults.AuthenticationScheme));
    }
}
```

実行して、ウェブアプリにアクセスすると、`HTTP/1.1 401 Unauthorized` が返るようになります。

```
$ curl -v http://localhost:5000
* Rebuilt URL to: http://localhost:5000/
*   Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 5000 (#0)
> GET / HTTP/1.1
> Host: localhost:5000
> User-Agent: curl/7.47.0
> Accept: */*
> 
< HTTP/1.1 401 Unauthorized
< Date: Thu, 18 Jan 2018 03:45:24 GMT
< Server: Kestrel
< Content-Length: 0
< WWW-Authenticate: Bearer
< 
* Connection #0 to host localhost left intact
```

せっかくですので、`CookieAuthenticationHandler` を使ってみます。

```csharp
internal sealed class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddAuthentication()
            .AddJwtBearer()
            .AddCookie(); // CookieAuthenticationHandler も登録
    }

    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        // CookieAuthenticationHandler.ChallengeAsync の呼び出し
        app.Run(context => context.ChallengeAsync(CookieAuthenticationDefaults.AuthenticationScheme));
    }
}
```

同じように実行してアクセスすると、`HTTP/1.1 302 Found` が返ります。

```
$ curl -v http://localhost:5000
* Rebuilt URL to: http://localhost:5000/
*   Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 5000 (#0)
> GET / HTTP/1.1
> Host: localhost:5000
> User-Agent: curl/7.47.0
> Accept: */*
> 
< HTTP/1.1 302 Found
< Date: Thu, 18 Jan 2018 03:57:19 GMT
< Server: Kestrel
< Content-Length: 0
< Location: http://localhost:5000/Account/Login?ReturnUrl=%2F
< 
* Connection #0 to host localhost left intact
```

`Location` ヘッダーでログインページにジャンプするようになっています。

`ChallengeAsync` 以外にも認証ハンドラーは以下のメソッドが実装されています。

| メソッド名 | 動作
| ---- | ----
| AuthenticateAsync | HTTP リクエストヘッダー内のトークンから認証情報を取り出します
| ChallengeAsync | 認証が必要なことを示すレスポンスを返します
| ForbidAsync | 権限が足りないことを示すレスポンスを返します


### デフォルトスキームの登録
`HttpContext.ChallengeAsync` メソッドを呼び出すたびにいちいちスキーム名を指定していたら大変面倒です。そこで、デフォルトスキームというものを登録できるようになっています。

以下のように、`AddAuthentication` メソッドでデフォルトスキームを登録します。

```csharp
internal sealed class Startup
{
    // ReSharper disable once UnusedMember.Global
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddAuthentication(options =>
                {
                    // ChallengeAsync 呼び出し時に JWT を使うように
                    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
                })
            .AddJwtBearer()
            .AddCookie();
    }

    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        // デフォルトの Challenge スキームを呼び出します
        app.Run(context => context.ChallengeAsync());
    }
}
```

`AuthenticateAsync` や `ForbidAsync` にもデフォルトスキームが用意されています。あまり意味はないと思いますが、`ChallengeAsync` はクッキーを `ForbidAsync` には JWT Bearer トークンをと使い分けることもできます。

```csharp
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultForbidScheme = JwtBearerDefaults.AuthenticationScheme;
```

全て共通のデフォルトスキームにすることもできます。以下の一行だけで、すべてのメソッドのデフォルトスキーマを設定できます。


```csharp
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
```


### SignInAsync
少し脱線して、`SignInAsync` メソッドを説明します。メソッドの名前からは分かりにくいですが、認証情報をブラウザーへ返す役割があります。

ただ、すべての認証ハンドラーがこれを実装しているかというとそうではなく、標準では `CookieAuthenticationHadleer` のみのようです。認証ハンドラーが `IAuthenticationSignInHandler` インターフェイスを実装しているかどうかで区別がつきます。

```csharp
internal sealed class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddAuthentication(options =>
            {
                options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
            })
            .AddCookie();
    }

    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        app.Run(context =>
        {
            // 認証情報を作る
            var identity = new ClaimsIdentity();
            identity.AddClaim(new Claim(ClaimTypes.Name, "taro"));
            var principal = new ClaimsPrincipal(identity);

            // 認証情報を登録する
            return context.SignInAsync(principal);
        });
    }
}
```

実行してアクセスすると、以下のように Cookie にトークンが埋め込まれていることがわかります。

```csharp
> curl -v http://localhost:5000
* Rebuilt URL to: http://localhost:5000/
*   Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 5000 (#0)
> GET / HTTP/1.1
> Host: localhost:5000
> User-Agent: curl/7.47.0
> Accept: */*
> 
< HTTP/1.1 200 OK
< Date: Thu, 18 Jan 2018 04:27:58 GMT
< Server: Kestrel
< Content-Length: 0
< Cache-Control: no-cache
< Pragma: no-cache
< Expires: -1
< Set-Cookie: .AspNetCore.Cookies=CfDJ8H5PtWI7BWJGl2cIGtbQbFI_QkDCQLPWmWmvo84r_KgLlLHWSpmP9iaVQww667ZXF-jKAdjBCP_1roRbXqwgyZFuzJkQ8uI6uza41HynfM8dp8nA8iGirT9e3jOHpYLymnG25i_1UHO_hHzjlPy8K8ZAil8RMOfQT34sSguZivvOGDZiBvrb2x2SJvDC2exqc0zCCoIzwPESSDxRPqci2nRMwQY_rysjpn_w4pbwjGLuG7Z02f_s9h2JcpxLXwFl87-7DAKzqJYvZPSJG4qXKnlSPdZfAoH9Tgu3QmtN4zif; path=/; samesite=lax; httponly
< 
* Connection #0 to host localhost left intact
```

### AuthenticateAsync
先ほど説明を端折っていた `AuthenticateAsync` メソッドは、主に HTTP リクエストヘッダー内にあるトークンから認証情報を取り出す役割があります。

```csharp
internal sealed class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddAuthentication(options =>
            {
                options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
            })
            .AddCookie();
    }

    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        app.Run(async context =>
        {
            // 認証情報の結果を取り出す
            var result = await context.AuthenticateAsync();

            // 認証チケットから認証情報を取り出す
            var principal = result.Ticket.Principal;

            // 名前を出力
            await context.Response.WriteAsync(principal.FindFirst(ClaimTypes.Name).Value);
        });
    }
}
```

また実行してアクセスします。もちろん、先ほどの Cookie に埋め込まれたトークンを合わせて渡します。

```
$ curl -v -b '.AspNetCore.Cookies=CfDJ8H5PtWI7BWJGl2cIGtbQbFI_QkDCQLPWmWmvo84r_KgLlLHWSpmP9iaVQww667ZXF-jKAdjBCP_1roRbXqwgyZFuzJkQ8uI6uza41HynfM8dp8nA8iGirT9e3jOHpYLymnG25i_1UHO_hHzjlPy8K8ZAil8RMOfQT34sSguZivvOGDZiBvrb2x2SJvDC2exqc0zCCoIzwPESSDxRPqci2nRMwQY_rysjpn_w4pbwjGLuG7Z02f_s9h2JcpxLXwFl87-7DAKzqJYvZPSJG4qXKnlSPdZfAoH9Tgu3QmtN4zif' http://localhost:5000
* Rebuilt URL to: http://localhost:5000/
*   Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 5000 (#0)
> GET / HTTP/1.1
> Host: localhost:5000
> User-Agent: curl/7.47.0
> Accept: */*
> Cookie: .AspNetCore.Cookies=CfDJ8H5PtWI7BWJGl2cIGtbQbFI_QkDCQLPWmWmvo84r_KgLlLHWSpmP9iaVQww667ZXF-jKAdjBCP_1roRbXqwgyZFuzJkQ8uI6uza41HynfM8dp8nA8iGirT9e3jOHpYLymnG25i_1UHO_hHzjlPy8K8ZAil8RMOfQT34sSguZivvOGDZiBvrb2x2SJvDC2exqc0zCCoIzwPESSDxRPqci2nRMwQY_rysjpn_w4pbwjGLuG7Z02f_s9h2JcpxLXwFl87-7DAKzqJYvZPSJG4qXKnlSPdZfAoH9Tgu3QmtN4zif
> 
< HTTP/1.1 200 OK
< Date: Thu, 18 Jan 2018 04:39:14 GMT
< Server: Kestrel
< Transfer-Encoding: chunked
< 
* Connection #0 to host localhost left intact
taro
```

無事、名前が表示されました!


### UseAuthentication
先ほど `AuthenticateAsync` メソッドで認証情報を取り出せるようになりました。これを手軽にしてくれるのが `AuthenticationMiddleare` です。このミドルウェアは `AuthenticateAsync` メソッドから得た認証情報を `HttpContext.User` に登録してくれます。

```csharp
internal sealed class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddAuthentication(options =>
            {
                options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
            })
            .AddCookie();
    }

    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        // トークンを読み取って認証情報を設定するようにする
        app.UseAuthentication();

        app.Run(async context =>
        {
            // HttpContext.User プロパティから認証情報を取り出す
            var principal = context.User;

            // 名前を出力
            await context.Response.WriteAsync(principal.FindFirst(ClaimTypes.Name).Value);
        });
    }
}
```

同じように、実行して Cookie トークン付きでアクセスします。

```
$ curl -v -b '.AspNetCore.CookCfDJ8H5PtWI7BWJGl2cIGtbQbFI_QkDCQLPWmWmvo84r_KgLlLHWSpmP9iaVQww667ZXF-jKAdjBCP_1roRbXqwgyZFuzJkQ8uI6uza41HynfM8dp8nA8iGirT9e3jOHpYLymnG25i_1UHO_hHzjlPy8K8ZAil8RMOfQT34sSguZivvOGDZiBvrb2x2SJvDC2exqc0zCCoIzwPESSDxRPqci2nRMwQY_rysjpn_w4pbwjGLuG7Z02f_s9h2JcpxLXwFl87-7DAKzqJYvZPSJG4qXKnlSPdZfAoH9Tgu3QmtN4zif' http://localhost:5000
* Rebuilt URL to: http://localhost:5000/
*   Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 5000 (#0)
> GET / HTTP/1.1
> Host: localhost:5000
> User-Agent: curl/7.47.0
> Accept: */*
> Cookie: .AspNetCore.Cookies=CfDJ8H5PtWI7BWJGl2cIGtbQbFI_QkDCQLPWmWmvo84r_KgLlLHWSpmP9iaVQww667ZXF-jKAdjBCP_1roRbXqwgyZFuzJkQ8uI6uza41HynfM8dp8nA8iGirT9e3jOHpYLymnG25i_1UHO_hHzjlPy8K8ZAil8RMOfQT34sSguZivvOGDZiBvrb2x2SJvDC2exqc0zCCoIzwPESSDxRPqci2nRMwQY_rysjpn_w4pbwjGLuG7Z02f_s9h2JcpxLXwFl87-7DAKzqJYvZPSJG4qXKnlSPdZfAoH9Tgu3QmtN4zif
> 
< HTTP/1.1 200 OK
< Date: Thu, 18 Jan 2018 04:48:34 GMT
< Server: Kestrel
< Transfer-Encoding: chunked
< 
* Connection #0 to host localhost left intact
taro
```

同じように名前が表示されました!

一般的なユースケースでは、ログインユーザーの取得はこの方法のほうが使われると思います。`AuthenticateAsync` メソッドを使うことは稀ではないかと。


### HandleRequestAsync
認証ハンドラーで少し特殊なのがこの `HandleRequestAsync` メソッドです。`SignInAsync` と同様に認証ハンドラーの基本機能ではなく、`IAuthenticationRequestHandler` インターフェイスを実装する必要があります。`JwtBearerHandler` や `CookieAuthenticationHandler` では実装されておらず、`MicrosoftAccountHandler` を始めとする外部認証の認証ハンドラーで実装されています。

また、他のメソッドと異なり、`HttpContext.HandleReqyestAync` メソッドが用意されておらず、デフォルトスキーマもありません。直接使うことなく、`AuthenticationMiddleware` からリクエストのたびにすべての認証ハンドラーの分が呼び出されます。

このメソッドの目的は、外部認証から認証コードをを受け取るためにあります。

Microsoft Account を始めとする外部認証は、認証の完了後、ブラウザーにウェブアプリに制御を戻すようリダイレクトを指示します。このリダイレクト URL に認証コードが埋め込まれています。

```
Location: http://localhost:5000/signin-microsoft?code=<authorization_code>&...
```

`MicrosoftAccountHandler.HandleRequestAsync` メソッドは、リクエストが `http://localhost:5000/signin-microsoft` の時にだけ反応するようになっています。URL パラメーターから認証コードを取得し、(かなり端折りますけど) 認証情報を作成します。この認証情報を `HttpContext.SignInAsync` メソッドを呼び出して登録します。

ところで、`MicrosoftAccountHandler` は `SignInAsync` メソッドを提供していませんので、その部分は `CookieAuthenticationHandler` を利用する必要があります。また、`AuthenticateAsync` も自分自身では処理をせず、デフォルトの `SignIn` スキームに登録されている認証ハンドラーに処理を委譲します。

つまり、Microsoft Account で認証をする場合は、`SignIn` スキームに `CookieAuthenticationHandler` を登録する必要があります。

```csharp
internal sealed class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddAuthentication(options =>
            {
                // デフォルトを Microsoft Account に
                options.DefaultScheme = MicrosoftAccountDefaults.AuthenticationScheme;
                // SignInAsync/SingOutAsync だけは Cookie を使う
                options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                options.DefaultSignOutScheme = CookieAuthenticationDefaults.AuthenticationScheme;
            })
            .AddMicrosoftAccount(options =>
            {
                options.ClientId = "....";
                options.ClientSecret = "...."
            })
            .AddCookie();
    }

    // ...
}
```

* `ChallengeAsync` は `MicrosoftAccountHandler` が処理し、外部認証ページへリダイレクト
* `AuthenticateAsync は `MicrosoftAccountHandler` が呼び出されるが、処理自体は `CookieAuthenticationHandler` に委譲される
* `SignInAsync` は `CookieAuthenticationHandler` が処理し、Cookie にトークンを埋め込む
* 外部認証からの認証情報の取得は `MicrosoftAccountHandler` が担当し、`CookieAuthenticationHandler` の `SignInAsync` を呼び出して、Cookie にトークンを埋め込んでいる


## 終わりに
ASP.NET Core Authentication は ASP.NET MVC (Web API) や ASP.NET Identity とは無関係に使えるんですね。特に、ASP.NET identity を使っているサンプルと使っていないサンプルで混乱していたので、調べてみてかなりすっきりしました。