「Open棟梁 wiki」は、「Open棟梁Project」,「OSSコンソーシアム .NET開発基盤部会」によって運営されています。 目次 †概要 †汎用認証サイト(Multi-purpose Authentication Site) (4) では、「認証連携コードの実装」を行う。 サンプル・プロジェクト †ココで実装するコードは、 下記に添付した、
からダウンロード可能です。 汎用認証サイトをセットアップする。 †
クライアントのプロジェクトを作成する。 †ここでは、クライアントのプロジェクトとして、ASP.NET MVCを選択する。 ASP.NET MVCのプロジェクトを新規作成する。 †ASP.NET MVCの(ASP.NET Web Application)プロジェクトを新規作成する。 [メニュー] ---> [新規作成] --->
新しいプロジェクト †
テンプレート選択 †テンプレート選択画面で以下のように選択する。
余分なコードを削除する。 †Controller †HomeController?から以下のアクション・メソッドを削除
View †
クライアントにRedirectエンドポイントを作成する。 †サンプルコードをコピーする。 †汎用認証サイトのAccountControllerからサンプルのRedirectエンドポイントである Authorization Codeグラント種別 †AccountController.OAuthAuthorizationCodeGrantClientアクション・メソッドをコピーする。 Implicitグラント種別 †AccountController.OAuthImplicitGrantClientアクション・メソッドをコピーする。 サンプルコードを貼り付ける。 †Authorization Codeグラント種別 †
Implicitグラント種別 †
汎用認証サイトにRedirectエンドポイントのURLを設定する。 †
認可リクエスト・認可レスポンス †クライアントにスターター(認可リクエスト)のリンクを設置する。 †スターターのリンクを取得 †
スターターのリンクを設置 †
クライアントをデバッグ実行し、認可レスポンスを確認する。 †Authorization Codeグラント種別 †
Implicitグラント種別 †
アクセストークン・リクエスト、レスポンス(Authorization Codeのみ) †
によって、仲介コードを
変換する。 なお、この処理は、Authorization Codeでのみ必要になる。 必要な情報の収集 †以下を必要とする。 アクセストークン・リクエストの仕方。 †
TokenエンドポイントのURL †
client_Idとclient_secret †
RedirectエンドポイントのURL †
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形式から選択できる。 署名検証、内容検証 †アクセストークンのformatgが、JWT形式の場合、
アクセストークンを使用しResource ServerのWebAPIへのアクセス結果を出力する。 †Authorization Codeグラント種別 †必要な情報の収集 †
/userinfoエンドポイントにアクセスして、ユーザ属性を取得する。 †
注意事項 †
Implicitグラント種別 †前述のAuthorization Codeグラント種別と異なり、 必要な情報の収集 †前述と同じ。 /userinfoエンドポイントにアクセスして、ユーザ属性を取得する。 †以下のモジュールから、
以下のようにOAuthImplicitGrantClient?.cshtmlにコードを移植する。
注意事項 †URLフラグメントを使用するため、Authorization Codeグラント種別と比べ、 OpenID Connect †Authorization Code Flow †OpenID ConnectのAuthorization Code Flowをテストする。 スターター †上記のスターターのScopeにopenidを加えるだけで、 <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を加えるだけでなく、 <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); } } |