Open棟梁 wiki」は、「Open棟梁Project」,「OSSコンソーシアム .NET開発基盤部会」によって運営されています。

目次

概要

汎用認証サイト(Multi-purpose Authentication Site)
の導入前の評価を行うためのファーストステップガイド。

(4) では、「認証連携コードの実装」を行う。

サンプル・プロジェクト

ココで実装するコードは、

下記に添付した、

からダウンロード可能です。

汎用認証サイトをセットアップする。

  • 適当なところに汎用認証サイトをセットアップする。
  • 3 分割テストで行ったように、ポート番号が違うならlocalhostでも良い。
  • ここでは、別サーバーのIIS上にデプロイ、セットアップしたので汎用認証サイトは常に実行可能状態にある。
  • ローカル開発環境でテストする場合は、必要に応じて、汎用認証サイトをデバッグ起動して実行可能状態にする。

クライアントのプロジェクトを作成する。

ここでは、クライアントのプロジェクトとして、ASP.NET MVCを選択する。

ASP.NET MVCのプロジェクトを新規作成する。

ASP.NET MVCの(ASP.NET Web Application)プロジェクトを新規作成する。

[メニュー] ---> [新規作成] --->

[プロジェクト] ---> [ASP.NET Web Application(.NET Framework)]

新しいプロジェクト

  • 任意のパスを指定
  • 名前を入力
    • 例えば[WebApplication1]という既定の名称を使用する。
    • ソリューション名も同じ名称のままにする。
  • [OK]ボタンを押下。

テンプレート選択

テンプレート選択画面で以下のように選択する。

  • テンプレートとして、[MVC]を選択する。
  • [認証の変更ボタン]を押下し、[認証なし]に変更する。
  • [OK]ボタンを押下してプロジェクトを作成する。
  • ※ [クラウドにホストする]チェック ボックスのチェックは外す。

余分なコードを削除する。

Controller

HomeController?から以下のアクション・メソッドを削除

  • About
  • Contact

View

  • _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エンドポイントを作成する。

サンプルコードをコピーする。

汎用認証サイトのAccountControllerからサンプルのRedirectエンドポイントである

Authorization Codeグラント種別

AccountController.OAuthAuthorizationCodeGrantClientアクション・メソッドをコピーする。

Implicitグラント種別

AccountController.OAuthImplicitGrantClientアクション・メソッドをコピーする。

サンプルコードを貼り付ける。

Authorization Codeグラント種別

  • HomeController?にコピーしたOAuthAuthorizationCodeGrantClient?アクション・メソッドを貼り付ける。
  • 最初のうちは、ほぼほぼコンパイル エラーになるので、以下のようにコメント アウトし、
  • 最後に、「return View("");」を加えて、
    [HttpGet]
    [AllowAnonymous]
    public async Task<ActionResult> OAuthAuthorizationCodeGrantClient(string code, string state)
    {
        // ・・・コメントアウト・・・
        
        return View("");
    }
  • Viewモジュールを~/Home/OAuthAuthorizationCodeGrantClient?.cshtmlという名称で作成しておく。

Implicitグラント種別

  • HomeController?にコピーしたOAuthImplicitGrantClient?アクション・メソッドを貼り付ける。
  • 最初のうちは、ほぼほぼコンパイル エラーになるので、以下のようにコメント アウトし、
  • 最後に、「return View("");」を加えて、
    [HttpGet]
    [AllowAnonymous]
    public async Task<ActionResult> OAuthImplicitGrantClient(string code, string state)
    {
        // ・・・コメントアウト・・・
        
        return View("");
    }
  • Viewモジュールを~/Home/OAuthImplicitGrantClient?.cshtmlという名称で作成しておく。

汎用認証サイトにRedirectエンドポイントのURLを設定する。

  • 以下のclient_id部分に、以下のように、Redirectエンドポイントを設定する
    "21c7769f16634dabaf14282602b9a5fc": {
      "client_secret": "xrRczIidMMZcMxvYWpIkvSZX1oRj2CLzVFSOkl7ocLY",
      "redirect_uri_code": "http://(MVCクライアント・サイトのアドレス:ポート)/Home/OAuthAuthorizationCodeGrantClient",
      "redirect_uri_token": "http://(MVCクライアント・サイトのアドレス:ポート)/Home/OAuthImplicitGrantClient",
      "client_name": "WebApplication1(MVC)"
    },
    "a0d280a6da034eb8ba821a651da829fc": {
      "client_secret": "eufLXjWaaQgiBXiiGZ-36N-bb4hOHy8H1TIEk126QDg",
      "redirect_uri_code": "http://(WebFormクライアント・サイトのアドレス:ポート)/OAuthAuthorizationCodeGrantClient.aspx",
      "redirect_uri_token": "http://(WebFormクライアント・サイトのアドレス:ポート)/OAuthImplicitGrantClient.aspx",
      "client_name": "WebApplication2(WebForms)"
    },
  • なお、このセクションは「CreateClientsIdentity?.exe」使用して自動生成できる。

認可リクエスト・認可レスポンス

クライアントにスターター(認可リクエスト)のリンクを設置する。

スターターのリンクを取得

  • スターターは汎用認証サイトを実行したトップ画面画から取得する。
    • Authorization Codeグラント種別
      http://(汎用認証サイトのアドレス:ポート)/MultiPurposeAuthSite/Account/OAuthAuthorize?client_id=67d328bfe8604aae83fb15fa44780d8b&response_type=code&scope=profile%20email%20phone%20address%20userid%20aaa%20bbb&state=vj9NCxij4L
  • Implicitグラント種別
    http://(汎用認証サイトのアドレス:ポート)/MultiPurposeAuthSite/Account/OAuthAuthorize?client_id=67d328bfe8604aae83fb15fa44780d8b&response_type=token&scope=profile%20email%20phone%20address%20userid%20aaa%20bbb&state=vj9NCxij4L
0

スターターのリンクを設置

  • スターターの「ホスト:ポート」部分を書き換えてIndex.cshtmlに貼り付ける。
    • Authorization Codeグラント種別
      http://(汎用認証サイトのアドレス:ポート)/MultiPurposeAuthSite/Account/OAuthAuthorize?client_id=21c7769f16634dabaf14282602b9a5fc&response_type=code&scope=profile%20email%20phone%20address%20userid%20aaa%20bbb&state=vj9NCxij4L
  • Implicitグラント種別
    http://(汎用認証サイトのアドレス:ポート)/MultiPurposeAuthSite/Account/OAuthAuthorize?client_id=21c7769f16634dabaf14282602b9a5fc&response_type=token&scope=profile%20email%20phone%20address%20userid%20aaa%20bbb&state=vj9NCxij4L
  • 次に、client_idを書き換える。
    • 具体的には、「67d328bfe8604aae83fb15fa44780d8b」から
      • MVC「21c7769f16634dabaf14282602b9a5fc」に書き換える。
      • Web Forms「a0d280a6da034eb8ba821a651da829fc」に書き換える。
  • Authorization Codeグラント種別
    • MVC
      <a href="http://(汎用認証サイトのアドレス:ポート)/MultiPurposeAuthSite/Account/OAuthAuthorize?client_id=21c7769f16634dabaf14282602b9a5fc&response_type=code&scope=profile%20email%20phone%20address%20userid%20aaa%20bbb&state=vj9NCxij4L">開始(code)</a><br/>
    • Web Forms
      <a href="http://localhost:63359/MultiPurposeAuthSite/Account/OAuthAuthorize?client_id=a0d280a6da034eb8ba821a651da829fc&response_type=code&scope=profile%20email%20phone%20address%20userid%20aaa%20bbb&state=vj9NCxij4L">開始(code)</a><br/>
  • Implicitグラント種別
    • MVC
      <a href="http://(汎用認証サイトのアドレス:ポート)/MultiPurposeAuthSite/Account/OAuthAuthorize?client_id=21c7769f16634dabaf14282602b9a5fc&response_type=token&scope=profile%20email%20phone%20address%20userid%20aaa%20bbb&state=vj9NCxij4L">開始(token)</a><br />
    • Web Forms
      <a href="http://localhost:63359/MultiPurposeAuthSite/Account/OAuthAuthorize?client_id=a0d280a6da034eb8ba821a651da829fc&response_type=token&scope=profile%20email%20phone%20address%20userid%20aaa%20bbb&state=vj9NCxij4L">開始(token)</a><br />
  • ポイント:
    サーバーに指定したRedirectエンドポイントを使用するため、
    ここでは、認可リクエストにredirect_uriパラメタを追加しない。

クライアントをデバッグ実行し、認可レスポンスを確認する。

Authorization Codeグラント種別

  • ブレークポイントを仕掛ける。
    RedirectエンドポイントであるOAuthAuthorizationCodeGrantClient?アクション・メソッドにブレークポイントを仕掛ける。
  • クライアントをデバッグ実行してAuthorization Codeグラント種別のスターターをクリック
    この状態でクライアント・サイトをデバッグ実行して、スターターをクリックしてみる。
  • プレークポイントにブレーク(中断)するのを確認する。
  • 汎用認証サイトの認証、(認可エンドポイントでの)認可プロセスを経て、
  • クライアント・サイトのRedirectエンドポイントである
    OAuthAuthorizationCodeGrantClient?アクション・メソッドでブレーク(中断)するのを確認する。
  • ここで、以下のように、仲介コードを確認することができる。
    以降コレ(仲介コード)を、アクセストークンとリフレッシュトークンに変換する処理を実装する。
    1

Implicitグラント種別

  • ブレークポイントを仕掛ける。
    RedirectエンドポイントであるOAuthImplicitGrantClient?アクション・メソッドにブレークポイントを仕掛ける。
  • クライアントをデバッグ実行してImplicitグラント種別のスターターをクリック
    この状態でクライアント・サイトをデバッグ実行して、スターターをクリックしてみる。
  • プレークポイントにブレーク(中断)するのを確認する。
  • 汎用認証サイトの認証、(認可エンドポイントでの)認可プロセスを経て、
  • クライアント・サイトのRedirectエンドポイントである
    OAuthImplicitGrantClient?アクション・メソッドでブレーク(中断)するのを確認する。
  • Implicitグラント種別では、「URLフラグメント」を使用して情報を受け渡すため、
    サーバー側では、何も確認できないので、F5押下で画面表示し、URLからアクセストークンを確認する。
    1a

アクセストークン・リクエスト、レスポンス(Authorization Codeのみ)

  • アクセストークン・リクエスト
  • アクセストークン・レスポンス

によって、仲介コードを

  • アクセストークンと
  • リフレッシュトークンに

変換する。

なお、この処理は、Authorization Codeでのみ必要になる。

必要な情報の収集

以下を必要とする。

アクセストークン・リクエストの仕方。

  • アクセストークン・リクエストについては、コチラを参照。
  • ザックリと、以下のようなリクエストを送信する。
    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

client_Idとclient_secret

  • ここでは、client_Idとclient_secretの値として、それぞれ、
    • client_Id : 21c7769f16634dabaf14282602b9a5fc
    • client_secret : xrRczIidMMZcMxvYWpIkvSZX1oRj2CLzVFSOkl7ocLY

を利用する。

RedirectエンドポイントのURL

  • ここでは、以下のようになる。
    http://(クライアント・サイトのアドレス:ポート)/Home/OAuthAuthorizationCodeGrantClient

HttpClientを使用して仲介コードをアクセストークンとリフレッシュトークンに変換する。

コード

だいたい、以下のような感じのコードになる。

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://(汎用認証サイトのアドレス:ポート)/MultiPurposeAuthSite/OAuthBearerToken"),
                };

                // HttpRequestMessage (Headers & Content)
                httpRequestMessage.Headers.Authorization = new AuthenticationHeaderValue(
                    "Basic",
                    Convert.ToBase64String(System.Text.Encoding.ASCII.GetBytes(
                        string.Format("{0}:{1}",
                            "21c7769f16634dabaf14282602b9a5fc",
                            "xrRczIidMMZcMxvYWpIkvSZX1oRj2CLzVFSOkl7ocLY"))));

                httpRequestMessage.Content = new FormUrlEncodedContent(
                    new Dictionary<string, string>
                    {
                        { "grant_type", "authorization_code" },
                        { "code", code },
                        { "redirect_uri", System.Web.HttpUtility.HtmlEncode("http://(クライアント・サイトのアドレス:ポート)/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("");
        }

    }
}

実行結果

上記の、「dic["access_token"]」の部分が、アクセストークンである。

汎用認証サイトでは、設定によってアクセストークンのformatを、ASP.NET Identity形式とJWT形式から選択できる。

2

署名検証、内容検証

アクセストークンのformatgが、JWT形式の場合、
必要に応じて、JWTの署名検証、内容検証を行えば、セキュリティ的に、より確実と言える。

  • 内容検証
    まず、"sub"が取得できれば、アクセストークンの取得に成功していると言える。
    そして、最低限、"iss"・"aud"、"nonce"クレームの内容検証を行うと良い。
    • "iss"クレームには、当該トークンを発行したSTSを表すUri値が格納される。
    • "aud"クレームには、起動パラメタの"client_id"値が格納される。
    • "nonce"クレームには、起動パラメタの"state"値が格納される。

アクセストークンを使用しResource ServerのWebAPIへのアクセス結果を出力する。

Authorization Codeグラント種別

必要な情報の収集

  • 汎用認証サイト(Resource Serverエンドポイント)のURLと繋げて、以下のようになる。
    http://(汎用認証サイトのアドレス:ポート)/MultiPurposeAuthSite/userinfo

/userinfoエンドポイントにアクセスして、ユーザ属性を取得する。

  • コード
    先程のコードに追記を行い、以下のような感じにする。
    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://(汎用認証サイトのアドレス:ポート)/MultiPurposeAuthSite/OAuthBearerToken"),
                    };
    
                    // HttpRequestMessage (Headers & Content)
                    httpRequestMessage.Headers.Authorization = new AuthenticationHeaderValue(
                        "Basic",
                        Convert.ToBase64String(System.Text.Encoding.ASCII.GetBytes(
                            string.Format("{0}:{1}",
                                "21c7769f16634dabaf14282602b9a5fc",
                                "xrRczIidMMZcMxvYWpIkvSZX1oRj2CLzVFSOkl7ocLY"))));
    
                    httpRequestMessage.Content = new FormUrlEncodedContent(
                        new Dictionary<string, string>
                        {
                            { "grant_type", "authorization_code" },
                            { "code", code },
                            { "redirect_uri", System.Web.HttpUtility.HtmlEncode("http://(クライアント・サイトのアドレス:ポート)/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://(汎用認証サイトのアドレス:ポート)/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("");
        }

    }
}
  • 実行結果
    3

注意事項

  • 仲介コードやアクセストークンは画面に露見させないこと。
  • このまま画面に表示させるとQuerystringに仲介コードが露見するので、
    必要な情報位を取得した後に、一段、Redirect処理などを経由させると良い。

Implicitグラント種別

前述のAuthorization Codeグラント種別と異なり、
この後の処理をクライアントサイドのJavaScript?で行う。

必要な情報の収集

前述と同じ。

/userinfoエンドポイントにアクセスして、ユーザ属性を取得する。

以下のモジュールから、

以下のようにOAuthImplicitGrantClient?.cshtmlにコードを移植する。

  • コード
    <a href="#" id="OAuthGetUserClaimsWebAPI" url="http://(汎用認証サイトのアドレス:ポート)/MultiPurposeAuthSite/userinfo">Get user claims</a>
    
    @section scripts{
        <script type="text/javascript">
    
            var fragment = "";
            var token = "";
    
            function CallOAuthAPI(url, httpMethod, postdata) {
                alert(
                    "<httpMethod>" + "\n" + httpMethod + "\n" +
                    "<url>" + "\n" + url + "\n" +
                    "<token>" + "\n" + token);
    
                $.ajax({
                    type: httpMethod,
                    url: url,
                    crossDomain: true,
                    headers: {
                        'Authorization': 'Bearer ' + token
                    },
                    data: postdata,
                    xhrFields: {
                        withCredentials: true
                    },
                    success: function (responseData, textStatus, jqXHR) {
                        alert(textStatus + ', ' + JSON.stringify(responseData));
                    },
                    error: function (responseData, textStatus, errorThrown) {
                        alert(textStatus + ', ' + errorThrown.message);
                    }
                });
            }
    
            $(function () {
                // フラグメントを取得し、
                fragment = getFragment();
                // フラグメントに access_token (Bearer Token) がある場合、
                if (fragment.access_token) {
    
                    // access_token (Bearer Token) を使用して
                    // ResourceServerのWebAPIにアクセスする。
                    token = fragment.access_token;
    
                    // 「access_token」(Bearer Token)が
                    // "露見"しないようwindow.location.hashを消去。
                    // ~~~~~~
                    window.location.hash = fragment.state || '';
                    
                    // OAuthGetUserClaimsWebAPI
                    $('#OAuthGetUserClaimsWebAPI').on('click', function () {
                        CallOAuthAPI($('#OAuthGetUserClaimsWebAPI').attr("url"), 'get', null)
                    });
                }
            });
    
            // -----------------------------------------------------------
            // フラグメント(#~の部分)を取得する。
            // -----------------------------------------------------------
            function getFragment() {
                // URLの「#」記号の後の部分を取得し、
                if (window.location.hash.indexOf("#") === 0) {
                    // # が1文字目にある場合
                    // 2文字目以降をobjectにparse。
                    return parseQueryString(window.location.hash.substr(1));
                } else {
                    // そうではない場合。
                    return {}; // 空
                }
            };
            
            // -----------------------------------------------------------
            // QueryStringをobjectにparseする。
            // -----------------------------------------------------------
            function parseQueryString(queryString) {
                //alert(queryString);
                var data = {},
                    pairs, pair, separatorIndex, escapedKey, escapedValue, key, value;
            
                if (queryString === null) {
                    return data; // 空で返す。
                }
            
                // 分解して、
                pairs = queryString.split("&");
            
                // 詰めて、
                for (var i = 0; i < pairs.length; i++) {
                    pair = pairs[i];
                    separatorIndex = pair.indexOf("=");
            
                    if (separatorIndex === -1) {
                        escapedKey = pair;
                        escapedValue = null;
                    } else {
                        escapedKey = pair.substr(0, separatorIndex);
                        escapedValue = pair.substr(separatorIndex + 1);
                    }
            
                    key = decodeURIComponent(escapedKey);
                    value = decodeURIComponent(escapedValue);
            
                    // インデクサで。
                    data[key] = value;
                }
            
                // 返す。
                return data;
            }
    
        </script>
    }
  • 実行結果
    3a

注意事項

URLフラグメントを使用するため、Authorization Codeグラント種別と比べ、
非常に、アクセストークンが画面に露見し易いので注意する。

OpenID Connect

Authorization Code Flow

OpenID ConnectのAuthorization Code Flowをテストする。

スターター

上記のスターターのScopeにopenidを加えるだけで、
OpenID ConnectのAuthorization Code Flowをテストできる。

<a href="http://(汎用認証サイトのアドレス:ポート)/MultiPurposeAuthSite/Account/OAuthAuthorize?client_id=21c7769f16634dabaf14282602b9a5fc&response_type=code&scope=profile%20email%20phone%20address%20userid%20aaa%20openid&state=vj9NCxij4L">開始</a>

コード

特に、id_tokenの検証に関して、以下のコードが参考になる。

Implicit Flow

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=21c7769f16634dabaf14282602b9a5fc&response_type=id_token%20token&scope=profile%20email%20phone%20address%20userid%20aaa%20openid&state=vj9NCxij4L">開始</a>

コード

JavaScript?でid_tokenの検証をするには...。

WebCrypto? APIなるものがあるもよう。

SpringMVC(Java)でやる。

SpringMVC(Java)

コチラを参考にする。

コード

OAuth2.0、OpenID Connect のサンプル・コード(Redirectエンドポイント)

package com.example.test1;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.io.InputStream;
import java.lang.reflect.Type;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.Signature;
import java.security.SignatureException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.interfaces.RSAPublicKey;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Base64.Decoder;
import java.util.List;
import java.util.Map;

import org.apache.commons.lang3.StringEscapeUtils;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpHost;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.NameValuePair;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.ModelAndView;

import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;

@Controller
public class HeloController {
	
	@RequestMapping(value="/", method=RequestMethod.GET)
	public ModelAndView index(ModelAndView mav) {
		mav.setViewName("index");
		return mav;
	}
	
	@RequestMapping(value="/", method=RequestMethod.POST)
	public ModelAndView send(@RequestParam("text1")String str, ModelAndView mav) {
		mav.setViewName(
				"redirect:http://localhost:63359/MultiPurposeAuthSite/Account/OAuthAuthorize" 
				+ "?client_id=084b7157a4d7427794012ee8a8e6d415" 
				+ "&response_type=code" 
				+ "&scope=profile%20email%20phone%20address%20openid"
				+ "&state=q58aqOg3"
				+ "&nonce=CiC1wEsurMkmMRBh");
		return mav;
	}
	
	@RequestMapping(value="/code", method=RequestMethod.GET)
	public ModelAndView code(@RequestParam("code") String code, @RequestParam("state") String state, ModelAndView mav) {
		
		if(state.equals("q58aqOg3")) // CSRF(XSRF)対策のstateの検証は重要
		{
			try {
				// Requestの準備
				String host = "localhost";
				String path1 = "/MultiPurposeAuthSite/OAuthBearerToken";
				String path2 = "/MultiPurposeAuthSite/userinfo";
				String username = "084b7157a4d7427794012ee8a8e6d415";
				String password = "JxBaXLNFyK4lCawEY9_HPA2zzjyiLgIiV4MQ2uogYms";
				
				HttpHost httpHost = new HttpHost(host, 63359, "http");
				HttpPost httpPost = new HttpPost(path1);
				HttpClient client = HttpClientBuilder.create().build();
				
				String auth = username + ":" + password;
				byte[] encodedAuth = Base64.getEncoder().encode(auth.getBytes(Charset.forName("ISO-8859-1")));
				String authHeader = "Basic " + new String(encodedAuth);
				httpPost.setHeader(HttpHeaders.AUTHORIZATION, authHeader);
				
				List<NameValuePair> requestParams = new ArrayList<>();
				requestParams.add(new BasicNameValuePair("grant_type","authorization_code"));
				requestParams.add(new BasicNameValuePair("code",code));
				requestParams.add(new BasicNameValuePair("redirect_uri", StringEscapeUtils.unescapeHtml4("http://localhost:8090/code")));
				
				HttpResponse response = null;
				
				// Requestの発行
				httpPost.setEntity(new UrlEncodedFormEntity(requestParams));
				response = client.execute(httpHost, httpPost);
				
				// HttpStatusの確認
				int status = response.getStatusLine().getStatusCode();
				if (status == HttpStatus.SC_OK){	
					
					// responseの取得
					String responseData = EntityUtils.toString(
							response.getEntity(), StandardCharsets.UTF_8);
					
					// JSONのParse
					Gson gson = new Gson();
					Type type = new TypeToken<Map<String, String>>(){}.getType();
					Map<String, String> map = gson.fromJson(responseData, type);
					
					String access_token = map.get("access_token");
					//String token_type = map.get("token_type");
					//String expires_in = map.get("expires_in");
					//String refresh_token = map.get("refresh_token");
					String id_token = map.get("id_token");
					
					// id_tokenの検証
					String[] jwt = id_token.split("\\.");
					
					Decoder base64urlDecoder = Base64.getUrlDecoder();
					
					//String jwtHeaderString = new String(base64urlDecoder.decode(jwt[0]), "UTF-8");
					String jwtClaimString  = new String(base64urlDecoder.decode(jwt[1]), "UTF-8");
					//String jwtSignitureString  = new String(base64urlDecoder.decode(jwt[2]), "UTF-8");
					
					byte[] message = (jwt[0] + "." + jwt[1]).getBytes("UTF-8");
					byte[] sign = base64urlDecoder.decode(jwt[2]);
					
					// JWT(RS-256の検証)
					
					// RSAによる署名と、その検証方法の例。
					// 1. RSAキーペアを生成し、公開キー・非公開キーをおさめたPKCS12(*.p12)形式と、公開キーをおさめたX.509のDERファイル形式を生成する。 
					// 2. PKCS12を読み取り、秘密キーでメッセージを署名する。 
					// 3. DERを読み取り、公開キーでメッセージの署名をベリファイする。
					// 手順1の、X.509の生成に、sunの非公開関数を用いている。
					// 代替ライブラリとしてはbouncycastleなどがある。 手順2以降は標準APIだけで実装可。 · GitHub
					// https://gist.github.com/seraphy/5359542
					
					// X509証明書から、RSA公開キーを復元する.
					RSAPublicKey publicKey2;
					try (InputStream is = new FileInputStream("C:\\root\\files\\resource\\X509\\RS256.cer")) {
						X509Certificate x509 = null;
						
						try {
							x509 = this.loadX509(is);
						} catch (GeneralSecurityException e) {
							// TODO Auto-generated catch block
							e.printStackTrace();
						}
						
						publicKey2 = (RSAPublicKey) x509.getPublicKey();
					}
					
					// RSA公開キーで署名器を生成する.
					Signature verifier = null;
					boolean result = false;
					
					verifier = Signature.getInstance("SHA256withRSA");
					verifier.initVerify(publicKey2);					
					verifier.update(message);
					result = verifier.verify(sign);
					
					if(result)
					{
						map = gson.fromJson(jwtClaimString, type);
						String iss = map.get("iss");
						String aud = map.get("aud");
						String nonce = map.get("nonce");
						String sub = map.get("sub");
						String iat = map.get("iat");
						String exp = map.get("exp"); //1505490503;
						
						if(
								iss.equals("http://jwtssoauth.opentouryo.com")
								&& aud.equals("084b7157a4d7427794012ee8a8e6d415")
								&& nonce.equals("CiC1wEsurMkmMRBh")
								&& (System.currentTimeMillis() / 1000L) <= Long.parseLong(exp))
						{
							// /userinfoへアクセス。
							HttpGet httpGet = new HttpGet(path2);
							client = HttpClientBuilder.create().build();
							
							authHeader = "Bearer " + access_token;
							httpGet.setHeader(HttpHeaders.AUTHORIZATION, authHeader);
							
							// Requestの発行
							response = client.execute(httpHost, httpGet);
							
							// HttpStatusの確認
							status = response.getStatusLine().getStatusCode();
							if (status == HttpStatus.SC_OK){	
								
								// responseの取得
								responseData = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
								
								// JSONのParse
								gson = new Gson();
								type = new TypeToken<Map<String, String>>(){}.getType();
								map = gson.fromJson(responseData, type);
								sub = map.get("sub");
							}
						}
					}
				}
				
			} catch (NoSuchAlgorithmException e) {
				e.printStackTrace();
			} catch (InvalidKeyException e) {
				e.printStackTrace();
			} catch (SignatureException e) {
				e.printStackTrace();
			} catch (ClientProtocolException e) {
				e.printStackTrace();
			} catch (UnsupportedEncodingException e) {
				e.printStackTrace();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
		
		mav.setViewName("index");
		return mav;
	}
	
	public X509Certificate loadX509(InputStream is) throws IOException, GeneralSecurityException {
		CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
		return (X509Certificate) certFactory.generateCertificate(is);
	}
}

添付ファイル: fileWebApplication2.zip 76件 [詳細] fileWebApplication1.zip 68件 [詳細] file3a.png 90件 [詳細] file1a.png 77件 [詳細] file3.png 168件 [詳細] file2.png 106件 [詳細] file0.png 113件 [詳細] file1.png 117件 [詳細]

トップ   編集 凍結 差分 バックアップ 添付 複製 名前変更 リロード   新規 一覧 単語検索 最終更新   ヘルプ   最終更新のRSS
Last-modified: 2018-06-14 (木) 17:23:33 (125d)