ASP.NET Core 2.0 Authentication
ASP.NET Core は認証周りもフレームワークがサポートしていてかなり便利なのですが、サンプルで提示されている以外のことをやろうとするとドキュメント不足に陥りました。調べた結果をまとめます。
前提知識
- ASP.NET Core MVC/Web API のチュートリアルくらいは分かる
本題
認証ハンドラーの登録
ASP.NET Core 2.0 では認証のための実装はサービスとしてすでに用意されています。そちらの基本部分の登録は AddAuthentication
メソッドで行います。
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
オブジェクトとなっていて、ここで認証処理用のハンドラーを登録します。
internal sealed class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication()
.AddJwtBearer();
}
// ...
}
登録した認証ハンドラーは HttpContext
オブジェクトから呼び出せるようになります。認証されていない状態 (HTTP Code 401) を返す ChallengeAsync
メソッドを呼び出します。(認証ハンドラーとして JwtBearer
を明示的に指定しています)
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
を使ってみます。
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
メソッドでデフォルトスキームを登録します。
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 トークンをと使い分けることもできます。
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultForbidScheme = JwtBearerDefaults.AuthenticationScheme;
全て共通のデフォルトスキームにすることもできます。以下の一行だけで、すべてのメソッドのデフォルトスキーマを設定できます。
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
SignInAsync
少し脱線して、SignInAsync
メソッドを説明します。メソッドの名前からは分かりにくいですが、認証情報をブラウザーへ返す役割があります。
ただ、すべての認証ハンドラーがこれを実装しているかというとそうではなく、標準では CookieAuthenticationHadleer
のみのようです。認証ハンドラーが IAuthenticationSignInHandler
インターフェイスを実装しているかどうかで区別がつきます。
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 にトークンが埋め込まれていることがわかります。
> 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 リクエストヘッダー内にあるトークンから認証情報を取り出す役割があります。
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
に登録してくれます。
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
を登録する必要があります。
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 を使っているサンプルと使っていないサンプルで混乱していたので、調べてみてかなりすっきりしました。