/// <summary>Flush</summary> public override void Flush() { byte[] bb = TempBuffer.GetBuffer(); if (bb != null && bb.Length > 0) { // 書き換え処理 Encoding enc = Response.ContentEncoding; string content = enc.GetString(bb); // JSON形式なので、JsonConvertでaccess_tokenを抜き出す。 Dictionary <string, object> accessTokenResponse = JsonConvert.DeserializeObject <Dictionary <string, object> >(content); // access_tokenを if (accessTokenResponse.ContainsKey("access_token")) { string access_token = (string)accessTokenResponse["access_token"]; string id_token = OidcTokenEditor.ChangeToIdTokenFromAccessToken(access_token, "", HashClaimType.None); if (!string.IsNullOrEmpty(id_token)) { // responseにid_tokenとして、このJWTを追加する。 accessTokenResponse.Add("id_token", id_token); bb = enc.GetBytes(JsonConvert.SerializeObject(accessTokenResponse)); } } } ResponseStream.Write(bb, 0, bb.Length); ResponseStream.Flush(); }
/// <summary> /// ChangeToIdTokenFromAccessToken /// OIDC対応(AccessTokenからIdTokenを生成) /// </summary> /// <param name="access_token">string</param> /// <param name="code">string</param> /// <param name="HashClaimType">HashClaimType</param> /// <returns>IdToken</returns> /// <remarks> /// OIDC対応 /// </remarks> public static string ChangeToIdTokenFromAccessToken(string access_token, string code, HashClaimType hct) { if (access_token.Contains(".")) { string[] temp = access_token.Split('.'); string json = CustomEncode.ByteToString(CustomEncode.FromBase64UrlString(temp[1]), CustomEncode.UTF_8); Dictionary <string, object> authTokenClaimSet = JsonConvert.DeserializeObject <Dictionary <string, object> >(json); // ・access_tokenがJWTで、payloadに"nonce" and "scope=openidクレームが存在する場合、 if (authTokenClaimSet.ContainsKey("nonce") && authTokenClaimSet.ContainsKey("scopes")) { JArray scopes = (JArray)authTokenClaimSet["scopes"]; // ・OpenID Connect : response_type=codeに対応する。 if (scopes.Any(x => x.ToString() == ASPNETIdentityConst.Scope_Openid)) { //・payloadからscopeを削除する。 authTokenClaimSet.Remove("scopes"); //・payloadにat_hash, c_hashを追加する。 switch (hct) { case HashClaimType.None: break; case HashClaimType.AtHash: // at_hash authTokenClaimSet.Add( "at_hash", OidcTokenEditor.CreateHash(access_token)); break; case HashClaimType.CHash: // c_hash authTokenClaimSet.Add( "c_hash", OidcTokenEditor.CreateHash(code)); break; case HashClaimType.Both: // at_hash, c_hash authTokenClaimSet.Add( "at_hash", OidcTokenEditor.CreateHash(access_token)); authTokenClaimSet.Add( "c_hash", OidcTokenEditor.CreateHash(code)); break; } //・編集したpayloadを再度JWTとして署名する。 string newPayload = JsonConvert.SerializeObject(authTokenClaimSet); JWT_RS256 jwtRS256 = null; // 署名 jwtRS256 = new JWT_RS256(ASPNETIdentityConfig.OAuthJWT_pfx, ASPNETIdentityConfig.OAuthJWTPassword, X509KeyStorageFlags.Exportable | X509KeyStorageFlags.MachineKeySet); string id_token = jwtRS256.Create(newPayload); // 検証 jwtRS256 = new JWT_RS256(ASPNETIdentityConfig.OAuthJWT_cer, ASPNETIdentityConfig.OAuthJWTPassword, X509KeyStorageFlags.Exportable | X509KeyStorageFlags.MachineKeySet); if (jwtRS256.Verify(id_token)) { // 検証できた。 return(id_token); } else { // 検証できなかった。 } } else { // OIDCでない。 } } else { // OIDCでない。 } } else { // JWTでない。 } return(""); }
/// <summary>OnPreSendRequestHeaders</summary> /// <param name="sender">object</param> /// <param name="e">EventArgs</param> private void OnPreSendRequestHeaders(object sender, EventArgs e) { // PreSendRequestHeadersのロジックはここに挿入 #region 数 #region Context HttpApplication application = (HttpApplication)sender; HttpContext context = application.Context; HttpResponse response = context.Response; #endregion #region ワーク // code string code = ""; // state string state = ""; // expires_in ulong expires_in = 0; // redirect_uri string redirect_url = ""; // access_token string access_token = ""; // id_token string id_token = ""; #endregion #endregion if (context.Request.Url.AbsolutePath.IndexOf(ASPNETIdentityConfig.OAuth2AuthorizeEndpoint2) == -1 && context.Request.Url.AbsolutePath.IndexOf(ASPNETIdentityConfig.OAuth2AuthorizeEndpoint) != -1) { #region response_type if (this.RewritedResponseTypeFrom_IdToken || this.RewritedResponseTypeFrom_IdTokenToken) { // OpenID Connect : Implicit Flowに対応 // - [response_type=id_token] // - or [response_type=id_token token] // レスポンス内容を参照して書き換え(fragmentにid_tokenを追加) string location = response.Headers[OAuth2AndOIDCConst.HttpHeader_Location]; if (!string.IsNullOrEmpty(location) && location.IndexOf("#access_token=") != -1) { string pattern = ""; // ・正規表現でaccess_tokenを抜き出す。 pattern = "(\\#access_token=)(?<accessToken>.+?)(\\&)"; access_token = Regex.Match(location, pattern).Groups["accessToken"].Value; // ・正規表現でstateを抜き出す。 pattern = "&state=(?<state>.+)"; state = Regex.Match(location, pattern).Groups["state"].Value; // at_hash と s_hashを付与 id_token = IdToken.ChangeToIdTokenFromAccessToken( access_token, "", state, HashClaimType.AtHash | HashClaimType.SHash, // ★ ASPNETIdentityConfig.OAuth2JWT_pfx, ASPNETIdentityConfig.OAuth2JWTPassword); if (!string.IsNullOrEmpty(id_token)) { // responseにid_tokenとして、このJWTを追加する。 if (this.RewritedResponseTypeFrom_IdTokenToken) { response.Headers[OAuth2AndOIDCConst.HttpHeader_Location] = location + "&id_token=" + id_token; } else if (this.RewritedResponseTypeFrom_IdToken) { // ココは未サポート状態なので、テストできていない。 location = location.Replace("access_token=" + access_token + "&", ""); location = location.Replace("&token_type=beara", ""); response.Headers[OAuth2AndOIDCConst.HttpHeader_Location] = location + "&id_token=" + id_token; } } } } else if (this.RewritedResponseTypeFrom_CodeIdToken || this.RewritedResponseTypeFrom_CodeToken || this.RewritedResponseTypeFrom_CodeIdTokenToken) { // OpenID Connect : Hybrid Flowに対応 // - [response_type=code id_token] // - or [response_type=code token] // - or [response_type=code id_token token] //レスポンス内容を参照して書き換え(RedirectをFragmentに変更) if (response.StatusCode == 302) // 302 Found { // ・Rewriteした為か、 // 何故か、以下が、 // "?code=code値&state=state値&redirect_uri=Urlエンコードされたredirect_uri値" // となっている。 // ※ redirect_uriがlocationのQueryStringに混じる。 // // ・本来は、 // "http(s)://redirect_uri値?code=code値&state=state値" // を期待していた。 string location = response.Headers[OAuth2AndOIDCConst.HttpHeader_Location]; string paramStrCode = "?code="; string paramStrState = "&state="; string paramStrRedirectUri = "&redirect_uri="; if (location.IndexOf(paramStrCode) != -1 || location.IndexOf(paramStrState) != -1 || location.IndexOf(paramStrRedirectUri) != -1) { response.Headers.Remove(OAuth2AndOIDCConst.HttpHeader_Location); // 以下は、"?code=XXX&state=YYY&" という並びが前提。 MatchCollection matches = StringChecker.Matches( location, "\\?code=(?<code>.+)&state=(?<state>.+)"); foreach (Match match in matches) { GroupCollection groups = match.Groups; code = groups[OAuth2AndOIDCConst.code].Value; state = groups[OAuth2AndOIDCConst.state].Value; } redirect_url = location.Substring(0, location.IndexOf('?')); // ★ Hybrid Flow対応なので、expを短縮してもイイ。 expires_in = ulong.Parse( ASPNETIdentityConfig.OAuth2AccessTokenExpireTimeSpanFromMinutes.TotalSeconds.ToString()); // Fragmentに組み込む string fragment = ""; if (this.RewritedResponseTypeFrom_CodeIdToken) { // id_tokenを取得 string access_token_payload = AuthorizationCodeProvider.GetInstance().GetAccessTokenPayload(code); // c_hash, s_hashを付与 id_token = IdToken.ChangeToIdTokenFromAccessToken( OidcTokenEditor.ProtectFromAccessTokenPayload(access_token_payload, expires_in), code, state, HashClaimType.CHash | HashClaimType.SHash, ASPNETIdentityConfig.OAuth2JWT_pfx, ASPNETIdentityConfig.OAuth2JWTPassword); fragment = "#id_token={0}&token_type=Bearer&code={1}&expires_in={2}&state={3}"; fragment = string.Format(fragment, new object[] { id_token, code, expires_in, state }); } else if (this.RewritedResponseTypeFrom_CodeToken) { // access_tokenを取得 string access_token_payload = AuthorizationCodeProvider.GetInstance().GetAccessTokenPayload(code); access_token = OidcTokenEditor.ProtectFromAccessTokenPayload(access_token_payload, expires_in); fragment = "#access_token={0}&token_type=Bearer&code={1}&expires_in={2}&state={3}"; fragment = string.Format(fragment, new object[] { access_token, code, expires_in, state }); } else if (this.RewritedResponseTypeFrom_CodeIdTokenToken) { // id_token, access_tokenを取得 string access_token_payload = AuthorizationCodeProvider.GetInstance().GetAccessTokenPayload(code); access_token = OidcTokenEditor.ProtectFromAccessTokenPayload(access_token_payload, expires_in); // at_hash, c_hash, s_hashを付与 id_token = IdToken.ChangeToIdTokenFromAccessToken(access_token, code, state, HashClaimType.AtHash | HashClaimType.CHash | HashClaimType.SHash, ASPNETIdentityConfig.OAuth2JWT_pfx, ASPNETIdentityConfig.OAuth2JWTPassword); fragment = "#access_token={0}&id_token={1}&token_type=Bearer&code={2}&expires_in={3}&state={4}"; fragment = string.Format(fragment, new object[] { access_token, id_token, code, expires_in, state }); } // Locationを追加(redirect_url + fragment)。 response.Headers.Add(OAuth2AndOIDCConst.HttpHeader_Location, redirect_url + fragment); } } } #endregion #region response_mode if (this.RewritedResponseModeFrom_FromPost) { // OAuth2.0, OpenID Connect : response_mode=form_postに対応 //レスポンス内容を参照して書き換え //(Redirect(get)をAuto-Submit Form(post)に変更) if (response.StatusCode == 302) // 302 Found { // ・Rewriteした為か、 // 何故か、以下が、 // "?code=code値&state=state値&redirect_uri=Urlエンコードされたredirect_uri値" // となっている。 // ※ redirect_uriがlocationのQueryStringに混じる。 // // ・本来は、 // "http(s)://redirect_uri値?code=code値&state=state値" // を期待していた。 string location = response.Headers[OAuth2AndOIDCConst.HttpHeader_Location]; string paramStrCode = "?code="; string paramStrState = "&state="; string paramStrRedirectUri = "&redirect_uri="; if (location.IndexOf(paramStrCode) != -1 || location.IndexOf(paramStrState) != -1 || location.IndexOf(paramStrRedirectUri) != -1) { // 302 Found ---> 200 OK response.StatusCode = 200; response.Headers.Remove(OAuth2AndOIDCConst.HttpHeader_Location); // 以下は、"?code=XXX&state=YYY&redirect_uri=ZZZ" という並びが前提。 MatchCollection matches = StringChecker.Matches( location, "\\?code=(?<code>.+)&state=(?<state>.+)&redirect_uri=(?<redirect_uri>.+)"); foreach (Match match in matches) { GroupCollection groups = match.Groups; code = groups[OAuth2AndOIDCConst.code].Value; state = groups[OAuth2AndOIDCConst.state].Value; redirect_url = CustomEncode.UrlDecode(groups[OAuth2AndOIDCConst.redirect_uri].Value); } // form_postに必要な、HTTP response message body string body = "<html>" + " <body onload=\"javascript: document.forms[0].submit()\">" + " <form method=\"post\" action =\"{0}\">" + " <input type=\"hidden\" name =\"code\" value =\"{1}\"/>" + " <input type=\"hidden\" name =\"state\" value =\"{2}\"/>" + " </form>" + " </body>" + "</html>"; // bodyに組み込んで body = string.Format(body, redirect_url, code, state); // HTTP Responseに書き出し byte[] buffer = response.ContentEncoding.GetBytes(body); response.OutputStream.Write(buffer, 0, buffer.Length); } } } #endregion if (this.RewritedResponseTypeFrom_IdToken || this.RewritedResponseTypeFrom_IdTokenToken || this.RewritedResponseTypeFrom_CodeIdToken || this.RewritedResponseTypeFrom_CodeToken || this.RewritedResponseTypeFrom_CodeIdTokenToken) //|| this.RewritedResponseModeFrom_FromPost) { context.RewritePath(this.OriginalVirtualPath); } #region 再利用されるinstance memberの初期化を忘れずに!! // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ this.OriginalVirtualPath = ""; this.RewritedResponseTypeFrom_IdTokenToken = false; this.RewritedResponseTypeFrom_IdToken = false; this.RewritedResponseTypeFrom_CodeIdToken = false; this.RewritedResponseTypeFrom_CodeToken = false; this.RewritedResponseTypeFrom_CodeIdTokenToken = false; this.RewritedResponseModeFrom_FromPost = false; #endregion } }