ASP.NET COREでOpenIdConnect(OIDC)を使う。ついでにテスト用のOIDCサーバを自作する

概要

ASP.NET COREのサーバサイド(MVC)とブラウザサイド(Blazor WebAssembly)でOpenIdConnectを使う方法と、
テスト用のOpenIdConnectをASP.NET COREのコントローラで自作する方法について書いていきます。
このページはASP.NET COREにおけるOpenIdConnectの概要が中心で、詳細や実装はこのページの各リンクをご参照ください。

IDトークンやアクセストークンのなりすまし防止策(With ASP.NET CORE6)
サーバサイドでのOIDCシーケンスとインタフェース項目(ASP.NET COREの場合)
ブラウザサイドでのOIDCシーケンスとインタフェース項目(WebAssemblyの場合)
テスト用のOIDCサーバを自作する(ASP.NET COREのControllerで作る)
サーバサイドからの自作OIDCサーバの呼び出し(ASP.NET CORE)
ブラウザサイド(Blazor WebAssembly)からの自作OIDCサーバの呼び出し(ASP.NET CORE)

OpenIdConnectの仕組みと、なぜそれが認証になるのか

OpenIdConnectの仕組みはサーバサイドとブラウザサイドでちょっと異なります。

サーバサイド

サーバサイドは従来のログイン画面を外だししたイメージになります。
主にシングルサインオンで使用されます。

ブラウザ → ASP.NETサーバ(MVC) → OpenIdConnect(OIDC)サーバ

使うのはIDトークン(id_token)になります。
ASP.NETサーバからOpenIdConnect(以降OIDC)サーバにアクセスすると、OIDCサーバはログイン画面を表示します。
ユーザーは、ログイン画面にユーザーID、パスワードを入力し、認証OKになると、OIDCサーバは「Idトークン(id_token)」を生成し、ASP.NETサーバに返却します。
Idトークンには、ユーザを一意に特定可能なIDが設定されます。
ASP.NETサーバは初回の場合、IDトークンのIDに紐づける形でアカウントを作成します。
IDトークンにはユーザの名前やメールアドレスも含まれるので、それらの情報を利用してアカウントを作成します。
2回目以降は、IDトークンのIDに紐づくアカウントを探し、そのアカウントでログインします。
他のシステムに同一ユーザがログインした際は、OIDCサーバがセッションにすでに認証情報を保持しているため、ログイン画面へのユーザ情報の入力なしにシステムにログインできます。

ASP.NETサーバではパスワードは管理しません。IDトークンを発行したOIDCサーバを信頼し、IDトークンが発行された=認証済と判断します。

IDトークンには(標準の仕様では)アプリの各機能の利用権限のロールを保持していないため、ロールはアプリ側で管理する必要があります(製品によってはアプリごとにIDとロールをマッピングする機能がある)
ちなみにid_token上の権限は、情報の公開権限を指し、要求元(今回の例ではWebサーバ)に名前やメールアドレスを公開してよいかどうかの情報になります。

というわけで、IDトークンの役割は、アクセスしている人を特定するためのIDになります。
ASP.NETサーバでは、IDトークンが送られてきた時点で本人認証は終わっていると判断して、IDトークンに紐づくアカウントの権限で処理を行います。

そうなるとIDトークンのなりすましや偽造が怖いですが、OIDCにはIDトークンのなりすましをしにくくする仕組みが組み込まれています。
詳細はid_tokenのなりすまし防止策をご参照ください。

ASP.NETでは通常、IDトークンおよびIDトークンをもとにして作った認証情報をCookieに保存し、ブラウザからWEBサーバにアクセスがある都度、(WEBサーバ側で)Cookieを読み込み、認証済かどうか、IDトークンの有効期限が過ぎてないかなどを判定します。

ブラウザサイド(Blazor WebAssembly)

ブラウザからASP.NETサーバのWEB APIにアクセスする際の通行許可証のように使われます。
使うのはアクセストークン(access_token)になります。

Webサーバが、ブラウザから送られてきたaccess_tokenをきっちりチェックすることで認証します。
ちゃんと、信頼している(事前に取り決めている)OIDCサーバから発行されたものなのか、改ざんはないか、利用者(Audience:システム毎に割り当てられたIDのようなもの)は正しいか、などをチェックし、間違いなく正当なaccess_tokenであると判定し、なのでそのtokenに設定されているユーザも正しい、と判定して認証が成り立ちます。

処理の流れとしては、ブラウザ(WebAssembly)からOIDCサーバにアクセスすると、OIDCサーバはログイン画面を表示します。
ユーザーは、ログイン画面にユーザーID、パスワードを入力し、認証OKになると、OIDCサーバは、アクセストークン(access_token)を生成し、ブラウザに返却します(大抵はid_tokenとaccess_tokenは同じデータ)。
アクセストークンにはユーザを一意に特定可能なIDが設定されています。

ブラウザ(WebAssembly)は、(たぶん)ブラウザ内のメモリ上にアクセストークンを保持し、ASP.NETサーバのWEB APIにアクセスする際に、HTTPヘッダ(Request.Headers[“Authorization”])にアクセストークンを付与してアクセスします。
ASP.NETサーバは、HTTPヘッダからアクセストークンを取り出し、アクセストークンに設定されている利用システム(Audience。client_idが設定されている。ブラウザサイドとASP.NETサーバで同じclient_idを使う)とトークン発行者(Issuer。OIDCサーバ)を調べ、対象アプリ用のアクセストークンであることを確認します。OIDCサーバには事前にclient_idを登録しておき、認可code要求時に指定されたclient_idがOIDCサーバに登録されていたら、codeやアクセストークンを発行します。

対象アプリ用のアクセストークンと判定したあと、ASP.NETサーバ側からもOIDCサーバにアクセスし、OIDCサーバからアクセストークンの署名検証用の公開鍵を取得します(jwks_uri)。
署名検証用の公開鍵を取得すると複数の鍵情報が返却されるので、鍵情報のkid(client_id)とアクセストークンのヘッダに設定されているkidが一致するものを使用します。

ASP.NETサーバは、取得した公開鍵で署名を複合化できたら、正しいアクセストークンとして受け入れ、アクセストークンに設定されたIDに紐づくアカウントを(アプリ内のDBなどから)検索し、アカウントに設定されているロールによって該当のWEB APIが利用できるかを判定します。

OpenIdConnectを使ったシングルサインオンの仕組み

話のメインではないので概要を書くと、OpenIdConnectサーバの製品によってはOIDCサーバにアクセスしてきたユーザ(ブラウザ)に対してCookieを発行し、Cookieとログイン情報を紐づけておきます。
同じユーザ(ブラウザ)が再度アクセスしてきた場合、Cookieに紐づくログイン情報を探して、存在すればログインなしで認証OKとします。
ユーザがシステムAを使うためにOIDCサーバでログインした後、今度はシステムBを使う場合、システムBがOIDCサーバにアクセスした時点ですでにCookieが発行されているのでログイン画面が表示されずに認証OKとなります。
Cookieはサイトごとに発行されるので、システムAとシステムBでは別々のCookieが発行され、シングルサインオンできないように思えますが、OpenIdConnectの特徴として、各システムからOIDCサーバにアクセスする際に、リダイレクトの形でOIDCサーバにアクセスします。
つまり、ブラウザから直接OIDCサーバにアクセスする形になるので、ブラウザからOIDCサーバのサイトに対するCookieが発行され、システムAとシステムBで同じCookieがOIDCサーバに受け渡されます。

サーバサイドでのOIDCシーケンスとインタフェース項目(ASP.NET COREの場合)

ざっくりいうと、以下の順になります。
1.Meta情報要求
  公開鍵取得のためのURLや、id_token取得のためのURLをOIDCサーバから取得します。
2.公開鍵情報要求
  id_tokenの署名を複合化するための公開鍵をOIDCサーバから取得します。
3.id_token要求
  id_token(ユーザを特定するための情報)をOIDCサーバから取得します。
4.id_token再要求(id_tokenの有効期限が切れた場合)
  id_tokenに設定されている有効期限が切れた場合は再度id_tokenを要求します。

詳細はサーバサイドでのOIDCシーケンスとインタフェース項目(要求、応答の項目含む)をご参照ください。

ブラウザサイドでのOIDCシーケンスとインタフェース項目(WebAssemblyの場合)

ざっくりいうと、以下の順になります。
1.ブラウザ:Meta情報要求
  code要求、access_token要求のためのURL情報をOIDCサーバから取得します。
2.ブラウザ:code要求(ログインなし)
  access_token取得のための認可コードをOIDCサーバから取得します。
  他のシステムでシングルサインオンしている可能性があるので、
  ログイン画面を表示しないモードで要求します。
  シングルサインオンしていない場合はエラーが返却されます。
3.ブラウザ:code要求(ログインあり)
  上記でエラーだった場合は、ログイン画面を表示するモードで要求します。
4.ブラウザ:access_token要求
  返却されたcodeを指定してaccess_tokenをOIDCサーバから取得します。
5.ブラウザ:ユーザ情報要求
  access_tokenをHTTPヘッダに設定して、
  ユーザ名などのユーザ情報を取得します。
6.ブラウザ:WebAPI要求(OIDCとは関係なし)
  access_tokenをHTTPヘッダに設定して、WebAPIを呼び出します。
7.WEBサーバ:Meta情報要求
  access_tokenをチェックするために公開鍵情報取得のためのURLを
  OIDCサーバから取得します。
8.WEBサーバ:公開鍵情報要求
  access_tokenの署名を複合化するための公開鍵をOIDCサーバから取得します。
9.WEBサーバ:WebAPI応答(OIDCとは関係なし)
  チェックが問題なければWebAPIを実行し、結果をブラウザに返却します。
10.ブラウザ:access_token再要求(access_tokenの有効期限が切れた場合)
  有効期限が切れた場合は、refresh_tokenを使ってaccess_tokenを再度要求します。

詳細はブラウザサイドでのOIDCシーケンスとインタフェース項目をご参照ください。

テスト用のOIDCサーバを自作する

OIDCのテストと理解のためにテスト用のOIDCサーバをASP.NET COREのControllerで作成します。
長くなるので、テスト用のOIDCサーバを自作するをご参照ください。

サーバサイドでテスト用のOIDCサーバを利用する

ASP.NET COREのMVCでOpenIdConnectをセットアップします。
id_tokenを取得し、その後ロールを、アプリの機能により取得し、id情報とロールを合わせて認証情報Claimを作り、Cookieに保存します。
Cookieに保存後は、HttpContext.User.Identityからユーザの認証情報を取得できるようになります。
詳細は、サーバサイドからの自作OIDCサーバの呼び出しをご参照ください。

ブラウザサイドでテスト用のOIDCサーバを利用する

ブラウザ(Blazor WebAssembly)側には、「AddOidcAuthentication」をセットアップし、
ASP.NETサーバ側には、「AddJwtBearer」をセットアップします。
AddOidcAuthenticationは、OIDCサーバにアクセスしてaccess_tokenを取得します。
サーバ側のAddJwtBearerはOIDCサーバにアクセスしてブラウザから送信されたaccess_tokenを検証します。
詳細は、ブラウザサイド(Blazor WebAssembly)からの自作OIDCサーバの呼び出しをご参照ください。