「[[Open棟梁 wiki>https://opentouryo.osscons.jp]]」は、「[[Open棟梁Project>https://github.com/OpenTouryoProject/]]」,「[[OSSコンソーシアム .NET開発基盤部会>https://www.osscons.jp/dotNetDevelopmentInfrastructure/]]」によって運営されています。 -[[戻る>汎用認証サイトのファーストステップガイド]] --[[汎用認証サイトのファーストステップガイド (1)]] --[[汎用認証サイトのファーストステップガイド (2)]] --[[汎用認証サイトのファーストステップガイド (3)]] --汎用認証サイトのファーストステップガイド (4) --[[汎用認証サイトのファーストステップガイド (5)]] *目次 [#hddf7023] #contents *概要 [#lbdaf1e3] [[汎用認証サイト(Multi-purpose Authentication Site)]]~ の導入前の評価を行うためのファーストステップガイド。 (4) では、「認証連携コードの実装」を行う。 *サンプル・プロジェクト [#nadadb6c] ココで実装するコートは、 下記に添付した、 &ref(WebApplication1.zip); からダウンロード可能です。 *汎用認証サイトをセットアップする。 [#m6718c37] -適当なところに汎用認証サイトをセットアップする。 -[[3 分割テスト>汎用認証サイトのファーストステップガイド (3)#r68f6218]]で行ったように、ポート番号が違うならlocalhostでも良い。 -ここでは、別サーバーのIIS上にデプロイ、セットアップしたので汎用認証サイトは常に実行可能状態にある。 -ローカル開発環境でテストする場合は、必要に応じて、汎用認証サイトをデバッグ起動して実行可能状態にする。 *クライアントのプロジェクトを作成する。 [#l8d51863] ここでは、クライアントのプロジェクトとして、ASP.NET MVCを選択する。 **ASP.NET MVCのプロジェクトを新規作成する。 [#v7dfdc59] ASP.NET MVCの(ASP.NET Web Application)プロジェクトを新規作成する。 [メニュー] ---> [新規作成] ---> >[プロジェクト] ---> [ASP.NET Web Application(.NET Framework)] ***新しいプロジェクト [#le7af872] -任意のパスを指定 -名前を入力 --例えば[WebApplication1]という既定の名称を使用する。 --ソリューション名も同じ名称のままにする。 -[OK]ボタンを押下。 ***テンプレート選択 [#s719107d] テンプレート選択画面で以下のように選択する。 -テンプレートとして、[MVC]を選択する。 -[認証の変更ボタン]を押下し、[認証なし]に変更する。 -[OK]ボタンを押下してプロジェクトを作成する。 -※ [クラウドにホストする]チェック ボックスのチェックは外す。 **余分なコードを削除する。 [#tbfa323e] ***Controller [#k9aeec88] HomeControllerから以下のアクション・メソッドを削除 -About -Contact ***View [#rc37b85d] -[[上記のアクション・メソッド>#k9aeec88]]に対応するViewも削除する。 --~/Home/About.cshtml --~/Home/Contact.cshtml -_Layout.cshtml --以下のdivは不要なので消す。 <div class="navbar navbar-inverse navbar-fixed-top"> --以下を以下で置き換える。 ---以下のdivを <div class="container body-content"> ---以下のステートメントで置き換える。 @RenderBody() --以下のような状態になる。 <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>@ViewBag.Title - マイ ASP.NET アプリケーション</title> @Styles.Render("~/Content/css") @Scripts.Render("~/bundles/modernizr") </head> <body> @RenderBody() @Scripts.Render("~/bundles/jquery") @Scripts.Render("~/bundles/bootstrap") @RenderSection("scripts", required: false) </body> </html> -~/Home/Index.cshtml --以下を残して全て消す。 @{ ViewBag.Title = "Home Page"; } *クライアントにRedirectエンドポイントを作成する。 [#we465a2c] **サンプルコードをコピーする。 [#j5235722] [[汎用認証サイトのAccountController>https://github.com/OpenTouryoProject/MultiPurposeAuthSite/blob/develop/root/programs/MultiPurposeAuthSite/MultiPurposeAuthSite/Controllers/AccountController.cs]]からサンプルのRedirectエンドポイントである ***Authorization Codeグラント種別 [#f687dd62] [[AccountController.OAuthAuthorizationCodeGrantClientアクション・メソッド>https://github.com/OpenTouryoProject/MultiPurposeAuthSite/blob/develop/root/programs/MultiPurposeAuthSite/MultiPurposeAuthSite/Controllers/AccountController.cs#L1669]]をコピーする。 ***Implicitグラント種別 [#a9669a55] [[AccountController.OAuthImplicitGrantClientアクション・メソッド>https://github.com/OpenTouryoProject/MultiPurposeAuthSite/blob/develop/root/programs/MultiPurposeAuthSite/MultiPurposeAuthSite/Controllers/AccountController.cs#L1848]]をコピーする。 **サンプルコードを貼り付ける。 [#u2eec88f] ***Authorization Codeグラント種別 [#eae7d995] -HomeControllerにコピーしたOAuthAuthorizationCodeGrantClientアクション・メソッドを貼り付ける。 -最初のうちは、ほぼほぼコンパイル エラーになるので、以下のようにコメント アウトし、 -最後に、「return View("");」を加えて、 [HttpGet] [AllowAnonymous] public async Task<ActionResult> OAuthAuthorizationCodeGrantClient(string code, string state) { // ・・・コメントアウト・・・ return View(""); } -Viewモジュールを~/Home/OAuthAuthorizationCodeGrantClient.cshtmlという名称で作成しておく。 ***Implicitグラント種別 [#z4b87f68] -HomeControllerにコピーしたOAuthImplicitGrantClientアクション・メソッドを貼り付ける。 -最初のうちは、ほぼほぼコンパイル エラーになるので、以下のようにコメント アウトし、 -最後に、「return View("");」を加えて、 [HttpGet] [AllowAnonymous] public async Task<ActionResult> OAuthImplicitGrantClient(string code, string state) { // ・・・コメントアウト・・・ return View(""); } -Viewモジュールを~/Home/OAuthImplicitGrantClient.cshtmlという名称で作成しておく。 *汎用認証サイトにRedirectエンドポイントのURLを設定する。 [#dba08db1] -[[以下のclient_id部分に、以下のように、Redirectエンドポイントを設定する>汎用認証サイトのコンフィギュレーション#v637e44e]]。 "f53469c17c5a432f86ce563b7805ab89": { "client_secret": "cKdwJb6mRKVIJpGxEWjIC94zquQltw_ECfO-55p21YM", "redirect_uri_code": "http://(クライアント・サイトのアドレス:ポート)/Home/OAuthAuthorizationCodeGrantClient", "redirect_uri_token": "http://(クライアント・サイトのアドレス:ポート)/Home/OAuthImplicitGrantClient", "client_name": "test_icon" -なお、このセクションは「CreateClientsIdentity.exe」使用して自動生成できる。 *認可リクエスト・認可レスポンス [#x3db3b0f] **クライアントにスターター(認可リクエスト)のリンクを設置する。 [#q8d7b767] ***スターターのリンクを取得 [#jd4f0e5f] -スターターは汎用認証サイトを実行したトップ画面画から取得する。 http://(汎用認証サイトのアドレス:ポート)/MultiPurposeAuthSite/Account/OAuthAuthorize?client_id=67d328bfe8604aae83fb15fa44780d8b&response_type=code&scope=profile%20email%20phone%20address%20userid%20aaa%20bbb&state=vj9NCxij4L #ref(0.png,left,nowrap,0) ***スターターのリンクを設置 [#fe72f177] -スターターの「ホスト:ポート」部分を書き換えてIndex.cshtmlに貼り付ける。 <a href="http://(クライアント・サイトのアドレス:ポート)/MultiPurposeAuthSite/Account/OAuthAuthorize?client_id=67d328bfe8604aae83fb15fa44780d8b&response_type=code&scope=profile%20email%20phone%20address%20userid%20aaa%20bbb&state=vj9NCxij4L">開始</a> -次に、client_idを書き換える。具体的には、「67d328bfe8604aae83fb15fa44780d8b」から「f53469c17c5a432f86ce563b7805ab89」に書き換える。 <a href="http://(クライアント・サイトのアドレス:ポート)/MultiPurposeAuthSite/Account/OAuthAuthorize?client_id=f53469c17c5a432f86ce563b7805ab89&response_type=code&scope=profile%20email%20phone%20address%20userid%20aaa%20bbb&state=vj9NCxij4L">開始</a> -ポイント:~ サーバーに指定したRedirectエンドポイントを使用するため、~ ここでは、認可リクエストにredirect_uriパラメタを追加しない。 **クライアントをデバッグ実行し、仲介コード(認可レスポンス)を確認する。 [#p1b6df91] ***ブレークポイントを仕掛ける。 [#x5c3dcc8] RedirectエンドポイントであるOAuthAuthorizationCodeGrantClientアクション・メソッドにブレークポイントを仕掛ける。 ***クライアントをデバッグ実行してスターターをクリック [#q259f27b] この状態でクライアント・サイトをデバッグ実行して、スターターをクリックしてみる。 ***プレークポイントにブレーク(中断)するのを確認する。 [#rc13061b] -汎用認証サイトの認証、(認可エンドポイントでの)認可プロセスを経て、 -クライアント・サイトのRedirectエンドポイントである~ OAuthAuthorizationCodeGrantClientアクション・メソッドでブレーク(中断)するのを確認する。 -ここで、以下のように、仲介コードを確認することができる。~ 以降コレ(仲介コード)を、アクセストークンとリフレッシュトークンに変換する処理を実装する。 #ref(1.png,left,nowrap,1) *アクセストークン・リクエスト、アクセストークン・レスポンス [#m5b26214] アクセストークン・リクエスト、アクセストークン・レスポンスによって、仲介コードをアクセストークンとリフレッシュトークンに変換する。 **必要な情報の収集 [#q97ffaa9] 以下を必要とする。 ***アクセストークン・リクエストの仕方。 [#k1d931a7] -アクセストークン・リクエストについては、[[コチラ>https://techinfoofmicrosofttech.osscons.jp/index.php?OAuth#yfeb403d]]を参照。 -ザックリと、以下のようなリクエストを送信する。 POST /token HTTP/1.1 Host: server.example.com Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW Content-Type: application/x-www-form-urlencoded grant_type=authorization_code&code=SplxlOBeZQQYbYS6WxSbIA &redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb -ここでは、以下のようになる。 POST /MultiPurposeAuthSite/OAuthBearerToken HTTP/1.1 Host: (汎用認証サイトのアドレス:ポート) Authorization: Basic ["client_Id:client_secret"をbase64 url encodeした値] Content-Type: application/x-www-form-urlencoded grant_type=authorization_code&code=[取得した仲介コードの値] ***TokenエンドポイントのURL [#vaa26fc4] -[[app.configにTokenエンドポイントのパスが設定されている。>汎用認証サイトのコンフィギュレーション#se5f4174]] -汎用認証サイト(AuthorizationServerエンドポイント)のURLと繋げて、以下のようになる。 http://(汎用認証サイトのアドレス:ポート)/MultiPurposeAuthSite/OAuthBearerToken ***client_Idとclient_secret [#qc267a07] -[[app.configにclient_Idとclient_secretが設定されている。>汎用認証サイトのコンフィギュレーション#v637e44e]] -ここでは、client_Idとclient_secretの値として、それぞれ、~ --client_Id : f53469c17c5a432f86ce563b7805ab89 --client_secret : cKdwJb6mRKVIJpGxEWjIC94zquQltw_ECfO-55p21YM >を利用する。 ***RedirectエンドポイントのURL [#vaa26fc4] -[[Redirectエンドポイントは前述の値>#dba08db1]]。 -ここでは、以下のようになる。 http://(クライアント・サイトのアドレス:ポート)/Home/OAuthAuthorizationCodeGrantClient **[[HttpClient>https://techinfoofmicrosofttech.osscons.jp/index.php?HttpClient%E3%81%AE%E9%A1%9E%E3%81%AE%E4%BD%BF%E3%81%84%E6%96%B9]]を使用して仲介コードをアクセストークンとリフレッシュトークンに変換する。 [#b8eff20c] ***コード [#l1699430] だいたい、以下のような感じのコードになる。 using System; using System.Text; using System.Collections.Generic; using System.Web.Mvc; using System.Net.Http; using System.Net.Http.Headers; using System.Threading.Tasks; using Microsoft.Owin.Security.DataHandler.Encoder; using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace WebApplication1.Controllers { public class HomeController : Controller { public ActionResult Index() { return View(); } [HttpGet] [AllowAnonymous] public async Task<ActionResult> OAuthAuthorizationCodeGrantClient(string code, string state) { if (state == "vj9NCxij4L") // CSRF(XSRF)対策のstateの検証は重要 { HttpClient httpClient = new HttpClient(); HttpRequestMessage httpRequestMessage = null; HttpResponseMessage httpResponseMessage = null; // HttpRequestMessage (Method & RequestUri) httpRequestMessage = new HttpRequestMessage { Method = HttpMethod.Post, RequestUri = new Uri("http://xxx.xxx.xxx.xxx/MultiPurposeAuthSite/OAuthBearerToken"), }; // HttpRequestMessage (Headers & Content) httpRequestMessage.Headers.Authorization = new AuthenticationHeaderValue( "Basic", Convert.ToBase64String(System.Text.Encoding.ASCII.GetBytes( string.Format("{0}:{1}", "f53469c17c5a432f86ce563b7805ab89", "cKdwJb6mRKVIJpGxEWjIC94zquQltw_ECfO-55p21YM")))); httpRequestMessage.Content = new FormUrlEncodedContent( new Dictionary<string, string> { { "grant_type", "authorization_code" }, { "code", code }, { "redirect_uri", System.Web.HttpUtility.HtmlEncode("http://localhost:62517/Home/OAuthAuthorizationCodeGrantClient") }, }); // HttpResponseMessage httpResponseMessage = await httpClient.SendAsync(httpRequestMessage); string response = await httpResponseMessage.Content.ReadAsStringAsync(); // 汎用認証サイトのOAuth2.0のレスポンスに含まれるaccess_tokenは、id_tokenのようなformatをしている。ここからsubを取得可能。 Base64UrlTextEncoder base64UrlEncoder = new Base64UrlTextEncoder(); Dictionary<string, string> dic = JsonConvert.DeserializeObject<Dictionary<string, string>>(response); string jwtPayload = Encoding.UTF8.GetString(base64UrlEncoder.Decode(dic["access_token"].Split('.')[1])); // id_tokenライクなJWTなので、中からsubなどを取り出すことができる。 JObject jobj = ((JObject)JsonConvert.DeserializeObject(jwtPayload)); string sub = (string)jobj["sub"]; } return View(""); } } } ***実行結果 [#i62ef920] 上記の、「dic["access_token"]」の部分が、アクセストークンである。 汎用認証サイトでは、[[設定によって>汎用認証サイトのコンフィギュレーション#k8279ce6]]アクセストークンのformatを、ASP.NET Identity形式と[[JWT>https://techinfoofmicrosofttech.osscons.jp/index.php?JWT]]形式から選択できる。 #ref(2.png,left,nowrap,2) ***署名検証、内容検証 [#va8ac3c1] アクセストークンのformatgが、[[JWT>https://techinfoofmicrosofttech.osscons.jp/index.php?JWT]]形式の場合、~ 必要に応じて、[[JWT>https://techinfoofmicrosofttech.osscons.jp/index.php?JWT]]の署名検証、内容検証を行えば、セキュリティ的に、より確実と言える。 -署名検証 --汎用認証サイトから公開鍵を入手する。 --Open棟梁では、JWTの署名検証ライブラリを提供している。 --JWTの署名検証ライブラリの使用方法については以下が参考になる。~ https://github.com/OpenTouryoProject/MultiPurposeAuthSite/blob/develop/root/programs/MultiPurposeAuthSite/MultiPurposeAuthSite/Models/ASPNETIdentity/TokenProviders/AccessTokenFormatJwt.cs#L191 -内容検証~ まず、"sub"が取得できれば、アクセストークンの取得に成功していると言える。~ そして、最低限、"iss"・"aud"、"nonce"クレームの内容検証を行うと良い。 ---"iss"クレームには、当該トークンを発行したSTSを表すUri値が格納される。 ---"aud"クレームには、起動パラメタの"client_id"値が格納される。 ---"nonce"クレームには、起動パラメタの"state"値が格納される。 -この[[JWT>https://techinfoofmicrosofttech.osscons.jp/index.php?JWT]]の署名検証、内容検証には「https://jwt.io/」を使用できる。 --詳しくは[[コチラ>汎用認証サイトのファーストステップガイド (1)#b6c3cd0c]]を参照。 *アクセストークンを使用しResource ServerのWebAPIへのアクセス結果を出力する。 [#u9bd4699] **必要な情報の収集 [#sb982345] ***/userinfoエンドポイントのURL [#ff57968f] -[[app.configに/userinfoエンドポイントのパスが設定されている。>汎用認証サイトのコンフィギュレーション#se5f4174]] -汎用認証サイト(Resource Serverエンドポイント)のURLと繋げて、以下のようになる。 http://(汎用認証サイトのアドレス:ポート)/MultiPurposeAuthSite/userinfo **/userinfoエンドポイントにアクセスして、ユーザ属性を取得する。 [#df413385] ***コード [#q033da03] [[先程のコード>#l1699430]]に追記を行い、以下のような感じにする。 using System; using System.Text; using System.Collections.Generic; using System.Web.Mvc; using System.Net.Http; using System.Net.Http.Headers; using System.Threading.Tasks; using Microsoft.Owin.Security.DataHandler.Encoder; using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace WebApplication1.Controllers { public class HomeController : Controller { public ActionResult Index() { return View(); } [HttpGet] [AllowAnonymous] public async Task<ActionResult> OAuthAuthorizationCodeGrantClient(string code, string state) { if (state == "vj9NCxij4L") // CSRF(XSRF)対策のstateの検証は重要 { HttpClient httpClient = new HttpClient(); HttpRequestMessage httpRequestMessage = null; HttpResponseMessage httpResponseMessage = null; // HttpRequestMessage (Method & RequestUri) httpRequestMessage = new HttpRequestMessage { Method = HttpMethod.Post, RequestUri = new Uri("http://xxx.xxx.xxx.xxx/MultiPurposeAuthSite/OAuthBearerToken"), }; // HttpRequestMessage (Headers & Content) httpRequestMessage.Headers.Authorization = new AuthenticationHeaderValue( "Basic", Convert.ToBase64String(System.Text.Encoding.ASCII.GetBytes( string.Format("{0}:{1}", "f53469c17c5a432f86ce563b7805ab89", "cKdwJb6mRKVIJpGxEWjIC94zquQltw_ECfO-55p21YM")))); httpRequestMessage.Content = new FormUrlEncodedContent( new Dictionary<string, string> { { "grant_type", "authorization_code" }, { "code", code }, { "redirect_uri", System.Web.HttpUtility.HtmlEncode("http://localhost:62517/Home/OAuthAuthorizationCodeGrantClient") }, }); // HttpResponseMessage httpResponseMessage = await httpClient.SendAsync(httpRequestMessage); string response = await httpResponseMessage.Content.ReadAsStringAsync(); // 汎用認証サイトのOAuth2.0のレスポンスに含まれるaccess_tokenは、id_tokenのようなformatをしている。ここからsubを取得可能。 Base64UrlTextEncoder base64UrlEncoder = new Base64UrlTextEncoder(); Dictionary<string, string> dic = JsonConvert.DeserializeObject<Dictionary<string, string>>(response); string jwtPayload = Encoding.UTF8.GetString(base64UrlEncoder.Decode(dic["access_token"].Split('.')[1])); // id_tokenライクなJWTなので、中からsubなどを取り出すことができる。 JObject jobj = ((JObject)JsonConvert.DeserializeObject(jwtPayload)); string sub = (string)jobj["sub"]; ↓↓↓ 下を追加 ↓↓↓ // Userエンドポイントに問い合わせを行う。 // HttpRequestMessage (Method & RequestUri) httpRequestMessage = new HttpRequestMessage { Method = HttpMethod.Get, RequestUri = new Uri("http://xxx.xxx.xxx.xxx/MultiPurposeAuthSite/userinfo"), }; // HttpRequestMessage (Headers) httpRequestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", dic["access_token"]); // HttpResponseMessage httpResponseMessage = await httpClient.SendAsync(httpRequestMessage); string userinfo = await httpResponseMessage.Content.ReadAsStringAsync(); jobj = ((JObject)JsonConvert.DeserializeObject(userinfo)); sub = (string)jobj["sub"]; } // 基本的に、仲介コードやアクセストークンは画面に露見させないこと。 // このまま画面に表示させるとQuerystringに仲介コードが露見するので、 // 必要な情報位を取得した後に、一段、Redirect処理などを経由させると良い。 ↑↑↑ 上を追加 ↑↑↑ return View(""); } } } ***実行結果 [#w47ed253] #ref(3.png,left,nowrap,3) **注意事項 [#z796a2ba] -基本的に、仲介コードやアクセストークンは画面に露見させないこと。 -このまま画面に表示させるとQuerystringに仲介コードが露見するので、~ 必要な情報位を取得した後に、一段、Redirect処理などを経由させると良い。 *OpenID Connect [#xf70352f] **Authorization Code Flow [#i3cda345] OpenID ConnectのAuthorization Code Flowをテストする。 上記の[[スターター>#fe72f177]]のScopeにopenidを加えるだけで、~ OpenID ConnectのAuthorization Code Flowをテストできる。 <a href="http://(クライアント・サイトのアドレス:ポート)/MultiPurposeAuthSite/Account/OAuthAuthorize?client_id=f53469c17c5a432f86ce563b7805ab89&response_type=code&scope=profile%20email%20phone%20address%20userid%20aaa%20openid&state=vj9NCxij4L">開始</a> **Implicit Flow [#odb23e5a] OpenID ConnectのImplicit Flowをテストする。 Implicit Flowでは、Scopeにopenidを加えるだけでなく、~ response_typeを、response_type=id_token token, id_tokenに変更する必要がある。 <a href="http://(クライアント・サイトのアドレス:ポート)/MultiPurposeAuthSite/Account/OAuthAuthorize?client_id=f53469c17c5a432f86ce563b7805ab89&response_type=id_token%20token&scope=profile%20email%20phone%20address%20userid%20aaa%20openid&state=vj9NCxij4L">開始</a> *ライブラリを使用してみる。 [#ec6bf749] **OAuth 2ライブラリ [#vc1c5486] **OpenID Connectライブラリ [#g1b7f1b5]