diff --git a/root/programs/CS/Frameworks/Infrastructure/Framework/Authentication/AuthenticationHeader.cs b/root/programs/CS/Frameworks/Infrastructure/Framework/Authentication/AuthenticationHeader.cs index eef4a5a58..e76c39c48 100644 --- a/root/programs/CS/Frameworks/Infrastructure/Framework/Authentication/AuthenticationHeader.cs +++ b/root/programs/CS/Frameworks/Infrastructure/Framework/Authentication/AuthenticationHeader.cs @@ -30,6 +30,8 @@ //* 2018/12/26 西野 大介 新規作成 //* 2026/09/12 玄人 幸道 方式の後ろに値の無い Authorization ヘッダで //* 例外になっていたのを修正 +//* 2026/09/25 玄人 幸道 Basic認証の資格情報を、RFC 6749 §2.3.1 に従って +//* 符号化・復号するようにした //********************************************************************************** using System; @@ -71,6 +73,38 @@ public static bool GetCredentials(string authHeader, out string client_id, out s return false; } + /// GetCredentials(Basic、RFC 6749 §2.3.1 の復号あり) + /// string + /// string(復号後) + /// string(復号後) + /// string(復号前=ヘッダに載っていたまま) + /// string(復号前=ヘッダに載っていたまま) + /// bool + /// + /// RFC 6749 §2.3.1 は、client_idとclient_secretを + /// application/x-www-form-urlencodedで符号化してからBase64にすることを求めている。 + /// このため、受け側は復号してから照合する。 + /// ただし、符号化しないクライアントも在るため、復号前の値も返す。 + /// 両方と照合すれば、どちらのクライアントも受けられる。 + /// + public static bool GetCredentials(string authHeader, + out string client_id, out string client_secret, + out string rawClientId, out string rawClientSecret) + { + client_id = ""; + client_secret = ""; + + if (AuthenticationHeader.GetCredentials(authHeader, out rawClientId, out rawClientSecret)) + { + client_id = CustomEncode.UrlDecode(rawClientId); + client_secret = CustomEncode.UrlDecode(rawClientSecret); + + return true; + } + + return false; + } + /// GetCredentials(Bearer) /// string /// string @@ -148,6 +182,23 @@ public static string GetCredentials(string authHeader, out string[] credentials) /// string /// string /// AuthenticationHeaderValue + /// + /// RFC 6749 §2.3.1 は、idとsecretを + /// application/x-www-form-urlencodedで符号化してから ":" で連結し、 + /// Base64にすることを求めている。 + /// 英数字だけなら符号化しても同じ文字列なので、見た目は変わらない。 + /// + /// 空白は "+" ではなく "%20" になる(CustomEncode.UrlEncodeがUri.EscapeDataStringのため)。 + /// RFC 6749 Appendix B の例は "+" だが、"%20" はフォーム形式の復号器でも + /// 素朴なURI復号器(Uri.UnescapeDataString)でも空白に戻るのに対し、 + /// "+" は後者では "+" のまま残る。壊れにくい方を選んでいる。 + /// 字面どおりに寄せるなら、WebUtility.UrlEncodeを使う。 + /// + /// 変わるのは "+" "/" "=" "%" ":" などを含む場合で、 + /// 受け側は復号してから照合する必要がある + /// (復号ありのGetCredentialsを参照。復号しないサーバも在るため、 + /// 復号前の値とも照合する形にしておくと、どちらとも繋がる)。 + /// public static AuthenticationHeaderValue CreateBasicAuthenticationHeaderValue(string id, string secret) { // id + x509 のパターンをサポート @@ -156,7 +207,9 @@ public static AuthenticationHeaderValue CreateBasicAuthenticationHeaderValue(str return new AuthenticationHeaderValue( OAuth2AndOIDCConst.Basic, CustomEncode.ToBase64String(CustomEncode.StringToByte( - string.Format("{0}:{1}", id, secret), CustomEncode.us_ascii))); + string.Format("{0}:{1}", + CustomEncode.UrlEncode(id), + CustomEncode.UrlEncode(secret ?? "")), CustomEncode.us_ascii))); } else { diff --git a/root/programs/CS/Frameworks/Infrastructure/Framework/Authentication/OAuth2AndOIDCClient.cs b/root/programs/CS/Frameworks/Infrastructure/Framework/Authentication/OAuth2AndOIDCClient.cs index ce7dc074a..9447cdcd8 100644 --- a/root/programs/CS/Frameworks/Infrastructure/Framework/Authentication/OAuth2AndOIDCClient.cs +++ b/root/programs/CS/Frameworks/Infrastructure/Framework/Authentication/OAuth2AndOIDCClient.cs @@ -33,6 +33,9 @@ //* 2019/08/01 西野 大介 client_secret_postのサポートを追加 //* 2020/03/04 西野 大介 FAPI CIBAの認可リクエスト(WebAPI)を追加 //* 2020/12/18 西野 大介 Device AuthZの認可リクエスト(WebAPI)を追加 +//* 2026/09/25 玄人 幸道 PAR(RFC 9126)とCIBAのRequest Object直接送信を追加 +//* 2026/09/25 玄人 幸道 FAPI 2.0向けにprivate_key_jwt / tls_client_authを追加 +//* 2026/09/26 玄人 幸道 private_key_jwtのトークン要求で、client_assertionも併せて送る //********************************************************************************** using System; @@ -116,6 +119,10 @@ public static async Task GetAccessTokenByCodeAsync( /// assertion /// OAuth2AndOIDCEnum.AuthMethods /// 結果のJSON文字列 + /// + /// アサーションは、RFC 7523 §2.2 のclient_assertion_type + client_assertionと、 + /// 従来のassertionの両方で送る。どちらを読む認可サーバにも繋がる。 + /// public static async Task GetAccessTokenByCodeAsync( Uri tokenEndpointUri, string redirect_uri, string code, string assertion, OAuth2AndOIDCEnum.AuthMethods authMethod = OAuth2AndOIDCEnum.AuthMethods.private_key_jwt) @@ -202,12 +209,20 @@ private static async Task GetAccessTokenByCodeAsync(Uri tokenEndpointUri authMethod == OAuth2AndOIDCEnum.AuthMethods.private_key_jwt) { // FAPI1のアクセストークン・リクエスト + // + // クライアント認証のアサーションは、RFC 7523 §2.2 では + // client_assertion_type + client_assertion で送る。 + // 従来のassertionしか読まない認可サーバも在るため、両方送る。 + // grant_typeはauthorization_codeなので、 + // JWT Bearerグラント(§2.1)のassertionとして解釈されることはない。 httpRequestMessage.Content = new FormUrlEncodedContent( new Dictionary { { OAuth2AndOIDCConst.grant_type, OAuth2AndOIDCConst.AuthorizationCodeGrantType }, { OAuth2AndOIDCConst.code, code }, { OAuth2AndOIDCConst.assertion, assertion }, + { OAuth2AndOIDCConst.client_assertion_type, OAuth2AndOIDCConst.JwtBearerClientAssertionType }, + { OAuth2AndOIDCConst.client_assertion, assertion }, { OAuth2AndOIDCConst.redirect_uri, HttpUtility.HtmlEncode(redirect_uri) }, }); } @@ -687,11 +702,87 @@ public static async Task GetJwkSetAsync(Uri jwkSetEndpointUri) #region FAPI (Financial-grade API) + #region クライアント認証 + + /// クライアント認証を付加する + /// HttpRequestMessage + /// Dictionary(本文。client_idと資格情報を追加する) + /// client_id + /// client_secret(client_secret_basic / client_secret_postで使用) + /// client_assertion(private_key_jwt / client_secret_jwtで使用) + /// OAuth2AndOIDCEnum.AuthMethods + /// bool(認証方式に依らずclient_idを本文に載せるか) + /// + /// FAPI 2.0 は、秘密ベースの方式(client_secret_basic / client_secret_post)をprofileから外し、 + /// private_key_jwt と MTLS(tls_client_auth)だけを認めている。 + /// + /// client_idを本文に載せるかは、エンドポイントによって違う。 + /// PARは認可リクエストのパラメタを送る口なので、client_idが必須(RFC 9126 §2.1)だが、 + /// CIBAのバックチャネル認証は「認証方式が要求するパラメタ以外をJWTの外に置いてはならない」 + /// (CIBA Core 1.0 §7.1.1)ため、client_secret_postとtls_client_authのときだけになる。 + /// そこで、常に載せるかどうかをalwaysSetClientIdで受け取る。 + /// + /// tls_client_authは、クライアント証明書がTLSの層で示されている必要があるため、 + /// HttpClientプロパティに、証明書を載せたHttpClientHandlerのHttpClientを設定しておく + /// (証明書は、CmnClientParams.ClientCertPfxFilePath / ClientCertPfxPasswordで取得できる)。 + /// HttpClientプロパティはstaticなので、差し替えはプロセス全体に効く。 + /// 1つのプロセスが複数のクライアントとして振る舞い、一部だけmTLSという構成では、 + /// 差し替えの順序に注意する。 + /// + private static void SetClientAuthentication( + HttpRequestMessage httpRequestMessage, Dictionary body, + string client_id, string client_secret, string client_assertion, + OAuth2AndOIDCEnum.AuthMethods authMethod, bool alwaysSetClientId) + { + if (alwaysSetClientId) + { + body[OAuth2AndOIDCConst.client_id] = client_id; + } + + if (authMethod == OAuth2AndOIDCEnum.AuthMethods.client_secret_basic) + { + // RFC 6749 §2.3.1 + httpRequestMessage.Headers.Authorization + = AuthenticationHeader.CreateBasicAuthenticationHeaderValue(client_id, client_secret); + } + else if (authMethod == OAuth2AndOIDCEnum.AuthMethods.client_secret_post) + { + // RFC 6749 §2.3.1(この方式はclient_idも本文で送る) + body[OAuth2AndOIDCConst.client_id] = client_id; + body[OAuth2AndOIDCConst.client_secret] = client_secret; + } + else if (authMethod == OAuth2AndOIDCEnum.AuthMethods.private_key_jwt + || authMethod == OAuth2AndOIDCEnum.AuthMethods.client_secret_jwt) + { + // RFC 7523 §2.2(署名の鍵が違うだけで、送り方は同じ) + body[OAuth2AndOIDCConst.client_assertion_type] = OAuth2AndOIDCConst.JwtBearerClientAssertionType; + body[OAuth2AndOIDCConst.client_assertion] = client_assertion; + } + else if (authMethod == OAuth2AndOIDCEnum.AuthMethods.tls_client_auth) + { + // RFC 8705 §2.1(本文はclient_idだけで、証明書はTLSの層で示す) + body[OAuth2AndOIDCConst.client_id] = client_id; + } + else + { + throw new ArgumentException( + PublicExceptionMessage.ARGUMENT_INCORRECT, nameof(authMethod)); + } + } + + #endregion + #region PAR (Pushed Authorization Requests) /// RequestObjectを登録する /// Uri /// string /// RequestObjectの登録結果 + /// + /// Request Object(署名付きJWT)を、生の本文としてPOSTし、request_uriを得る。 + /// RFC 9101(JAR)§5.2.1 が認可サーバによるこの預け先を認めており、この形で正しい。 + /// 本文の形式もクライアント認証も異なるPAR(RFC 9126)には、 + /// PushAuthorizationRequestAsyncメソッドを使用する。 + /// public static async Task RegisterRequestObjectAsync( Uri requestObjectRegUri, string requestObject) { @@ -713,6 +804,115 @@ public static async Task RegisterRequestObjectAsync( return await httpResponseMessage.Content.ReadAsStringAsync().ConfigureAwait(false); } + /// 認可リクエストをPushする(PAR : RFC 9126) + /// Uri(PARエンドポイント) + /// string(Request Object=署名付きJWT) + /// client_id + /// client_secret(tls_client_authではnull) + /// OAuth2AndOIDCEnum.AuthMethods + /// 結果のJSON文字列(request_uri と expires_in) + /// + /// Request Objectをrequestパラメタに載せて送る形(RFC 9126 §3、RFC 9101)。 + /// private_key_jwt / client_secret_jwtには、client_assertionを取るオーバーロードを使用する。 + /// + public static async Task PushAuthorizationRequestAsync( + Uri authRequestPushUri, string requestObject, + string client_id, string client_secret, + OAuth2AndOIDCEnum.AuthMethods authMethod = OAuth2AndOIDCEnum.AuthMethods.client_secret_basic) + { + return await OAuth2AndOIDCClient.PushAuthorizationRequestAsync( + authRequestPushUri, requestObject, + client_id, client_secret, null, authMethod).ConfigureAwait(false); + } + + /// 認可リクエストをPushする(PAR : RFC 9126) + /// Uri(PARエンドポイント) + /// string(Request Object=署名付きJWT) + /// client_id + /// client_secret(client_secret_basic / client_secret_postで使用) + /// client_assertion(private_key_jwt / client_secret_jwtで使用) + /// OAuth2AndOIDCEnum.AuthMethods + /// 結果のJSON文字列(request_uri と expires_in) + /// Request Objectをrequestパラメタに載せて送る形(RFC 9126 §3、RFC 9101)。 + public static async Task PushAuthorizationRequestAsync( + Uri authRequestPushUri, string requestObject, + string client_id, string client_secret, string client_assertion, + OAuth2AndOIDCEnum.AuthMethods authMethod = OAuth2AndOIDCEnum.AuthMethods.client_secret_basic) + { + return await OAuth2AndOIDCClient.PushAuthorizationRequestAsync( + authRequestPushUri, + new Dictionary + { + { OAuth2AndOIDCConst.request, requestObject } + }, + client_id, client_secret, client_assertion, authMethod).ConfigureAwait(false); + } + + /// 認可リクエストをPushする(PAR : RFC 9126) + /// Uri(PARエンドポイント) + /// Dictionary(認可リクエストのパラメタ) + /// client_id + /// client_secret(tls_client_authではnull) + /// OAuth2AndOIDCEnum.AuthMethods + /// 結果のJSON文字列(request_uri と expires_in) + /// + /// private_key_jwt / client_secret_jwtには、client_assertionを取るオーバーロードを使用する。 + /// + public static async Task PushAuthorizationRequestAsync( + Uri authRequestPushUri, Dictionary authZRequestParams, + string client_id, string client_secret, + OAuth2AndOIDCEnum.AuthMethods authMethod = OAuth2AndOIDCEnum.AuthMethods.client_secret_basic) + { + return await OAuth2AndOIDCClient.PushAuthorizationRequestAsync( + authRequestPushUri, authZRequestParams, + client_id, client_secret, null, authMethod).ConfigureAwait(false); + } + + /// 認可リクエストをPushする(PAR : RFC 9126) + /// Uri(PARエンドポイント) + /// Dictionary(認可リクエストのパラメタ) + /// client_id + /// client_secret(client_secret_basic / client_secret_postで使用) + /// client_assertion(private_key_jwt / client_secret_jwtで使用) + /// OAuth2AndOIDCEnum.AuthMethods + /// 結果のJSON文字列(request_uri と expires_in) + /// + /// PARは、認可エンドポイントへ送るはずのパラメタを、 + /// Tokenエンドポイントと同じクライアント認証を付けてPOSTする(§2)。 + /// FAPI 2.0 は、この認証をprivate_key_jwtかMTLS(tls_client_auth)に限っている。 + /// client_idは認可リクエストの必須パラメタなので、認証方式に依らず本文に載せる(§2.1)。 + /// 応答のrequest_uriを、認可リクエストのrequest_uriパラメタに指定して使用する。 + /// + public static async Task PushAuthorizationRequestAsync( + Uri authRequestPushUri, Dictionary authZRequestParams, + string client_id, string client_secret, string client_assertion, + OAuth2AndOIDCEnum.AuthMethods authMethod = OAuth2AndOIDCEnum.AuthMethods.client_secret_basic) + { + // 通信用の変数 + HttpRequestMessage httpRequestMessage = null; + HttpResponseMessage httpResponseMessage = null; + + // HttpRequestMessage (Method & RequestUri) + httpRequestMessage = new HttpRequestMessage + { + Method = HttpMethod.Post, + RequestUri = authRequestPushUri, + }; + + // body(引数のDictionaryは変更しない) + Dictionary body = new Dictionary(authZRequestParams); + + // 認証情報の付加(PARはclient_idが必須 : RFC 9126 §2.1) + OAuth2AndOIDCClient.SetClientAuthentication( + httpRequestMessage, body, client_id, client_secret, client_assertion, authMethod, true); + + httpRequestMessage.Content = new FormUrlEncodedContent(body); + + // HttpResponseMessage + httpResponseMessage = await OAuth2AndOIDCClient._HttpClient.SendAsync(httpRequestMessage).ConfigureAwait(false); + return await httpResponseMessage.Content.ReadAsStringAsync().ConfigureAwait(false); + } + #endregion #region FAPI CIBA @@ -720,6 +920,10 @@ public static async Task RegisterRequestObjectAsync( /// Uri /// string /// 結果のJSON文字列 + /// + /// request_uriでRequest Objectの在り処を渡す(CIBA Core 1.0 に無い、独自の形)。 + /// 標準の形(§7.1.1)で送るには、requestパラメタで送るオーバーロードを使用する。 + /// public static async Task CibaAuthZRequestAsync(Uri cibaAuthZUri, string requestObjectUri) { // 通信用の変数 @@ -744,6 +948,73 @@ public static async Task CibaAuthZRequestAsync(Uri cibaAuthZUri, string return await httpResponseMessage.Content.ReadAsStringAsync().ConfigureAwait(false); } + /// FAPI CIBAの認可リクエスト(WebAPI、CIBA Core 1.0 §7.1.1) + /// Uri(バックチャネル認証エンドポイント) + /// string(Request Object=署名付きJWT) + /// client_id + /// client_secret(tls_client_authではnull) + /// OAuth2AndOIDCEnum.AuthMethods + /// 結果のJSON文字列 + /// + /// private_key_jwt / client_secret_jwtには、client_assertionを取るオーバーロードを使用する。 + /// + public static async Task CibaAuthZRequestAsync( + Uri cibaAuthZUri, string requestObject, + string client_id, string client_secret, + OAuth2AndOIDCEnum.AuthMethods authMethod = OAuth2AndOIDCEnum.AuthMethods.client_secret_basic) + { + return await OAuth2AndOIDCClient.CibaAuthZRequestAsync( + cibaAuthZUri, requestObject, + client_id, client_secret, null, authMethod).ConfigureAwait(false); + } + + /// FAPI CIBAの認可リクエスト(WebAPI、CIBA Core 1.0 §7.1.1) + /// Uri(バックチャネル認証エンドポイント) + /// string(Request Object=署名付きJWT) + /// client_id + /// client_secret(client_secret_basic / client_secret_postで使用) + /// client_assertion(private_key_jwt / client_secret_jwtで使用) + /// OAuth2AndOIDCEnum.AuthMethods + /// 結果のJSON文字列 + /// + /// CIBA Core 1.0 は、署名付きJWTをrequestパラメタで送る形(§7.1.1)を定めており、 + /// バックチャネル認証エンドポイントには、Tokenエンドポイントと同じクライアント認証が要る。 + /// FAPI-CIBAは、この認証にprivate_key_jwt(またはMTLS)を要求する。 + /// + public static async Task CibaAuthZRequestAsync( + Uri cibaAuthZUri, string requestObject, + string client_id, string client_secret, string client_assertion, + OAuth2AndOIDCEnum.AuthMethods authMethod = OAuth2AndOIDCEnum.AuthMethods.client_secret_basic) + { + // 通信用の変数 + HttpRequestMessage httpRequestMessage = null; + HttpResponseMessage httpResponseMessage = null; + + // HttpRequestMessage (Method & RequestUri) + httpRequestMessage = new HttpRequestMessage + { + Method = HttpMethod.Post, + RequestUri = cibaAuthZUri, + }; + + // body + Dictionary body = new Dictionary + { + { OAuth2AndOIDCConst.request, requestObject } + }; + + // 認証情報の付加 + // (CIBAは、認証方式が要求するパラメタ以外をJWTの外に置けない : CIBA Core 1.0 §7.1.1) + OAuth2AndOIDCClient.SetClientAuthentication( + httpRequestMessage, body, client_id, client_secret, client_assertion, authMethod, false); + + httpRequestMessage.Content = new FormUrlEncodedContent(body); + + // HttpResponseMessage + httpResponseMessage = await OAuth2AndOIDCClient._HttpClient.SendAsync(httpRequestMessage).ConfigureAwait(false); + return await httpResponseMessage.Content.ReadAsStringAsync().ConfigureAwait(false); + } + /// FAPI CIBAのTokenリクエスト /// Uri /// client_id diff --git a/root/programs/CS/Frameworks/Infrastructure/Framework/Authentication/OAuth2AndOIDCConst.cs b/root/programs/CS/Frameworks/Infrastructure/Framework/Authentication/OAuth2AndOIDCConst.cs index 760d3a56b..332eb9967 100644 --- a/root/programs/CS/Frameworks/Infrastructure/Framework/Authentication/OAuth2AndOIDCConst.cs +++ b/root/programs/CS/Frameworks/Infrastructure/Framework/Authentication/OAuth2AndOIDCConst.cs @@ -32,6 +32,7 @@ //* 2020/02/27 西野 大介 FAPI CIBAのパラメタを追加 //* 2020/12/18 西野 大介 Device AuthZのパラメタを追加 //* 2026/09/12 玄人 幸道 RFC 6750 / RFC 7662 / CIBA / OIDCの定数を追加 +//* 2026/09/25 玄人 幸道 client_assertion(RFC 7523)の定数を追加 //********************************************************************************** // urnはClaimのurnで、 @@ -52,6 +53,15 @@ public class OAuth2AndOIDCConst /// client_secret public const string client_secret = "client_secret"; + /// client_assertion(private_key_jwt / client_secret_jwtで使用) + public const string client_assertion = "client_assertion"; + + /// client_assertion_type + public const string client_assertion_type = "client_assertion_type"; + + /// client_assertionのtype(RFC 7523 §2.2) + public const string JwtBearerClientAssertionType = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"; + /// grant_type public const string grant_type = "grant_type"; diff --git a/root/programs/CS/Frameworks/Infrastructure/Framework/Authentication/OAuth2AndOIDCParams.cs b/root/programs/CS/Frameworks/Infrastructure/Framework/Authentication/OAuth2AndOIDCParams.cs index d2f11a79a..510496503 100644 --- a/root/programs/CS/Frameworks/Infrastructure/Framework/Authentication/OAuth2AndOIDCParams.cs +++ b/root/programs/CS/Frameworks/Infrastructure/Framework/Authentication/OAuth2AndOIDCParams.cs @@ -30,6 +30,7 @@ //* 2017/09/06 西野 大介 新規作成 //* 2020/06/19 西野 大介 GetConfigSectionメソッドを廃止に伴う変更 //* GetConfigSection → GetAnyConfigSection +//* 2026/09/25 玄人 幸道 AuthRequestPushUri(PARエンドポイント)を追加 //********************************************************************************** using System.Collections.Generic; @@ -142,6 +143,18 @@ public static string RequestObjectRegUri return GetConfigParameter.GetConfigValue("RequestObjectRegUri"); } } + + /// + /// PAR(RFC 9126)のエンドポイント + /// (Discoveryのpushed_authorization_request_endpoint) + /// + public static string AuthRequestPushUri + { + get + { + return GetConfigParameter.GetConfigValue("AuthRequestPushUri"); + } + } #endregion } }