====== IDトークン獲得までの流れ ====== ===== 全フローチャート ===== {{map>handshake12-clickable.png?600x400|IDトークン獲得のフローチャート}} [[idトークン獲得の流れ#ログイン_oidc_openid_connect_スタート|ログイン @ 34,74,116,110]] [[idトークン獲得の流れ#認可サーバーの認可エンドポイントに到達_4認証画面にジャンプ|認可エンドポイントに到達 @ 500,140,600,300]] [[idトークン獲得の流れ#認証情報入力_8認可レスポンス|KeyCloak @ 175,160,265,180]] [[idトークン獲得の流れ#idトークンの検証|IDトークンの検証 @ 120,330,200,350]] [[idトークン獲得の流れ#到達urlのクエリパラメータ解説|リダイレクトurl @ 40,250,115,270]] [[idトークン獲得の流れ#トークンリクエスト|トークンリクエスト @ 120,275,220,295]] {{上記の楕円部分はクリックで当該説明にジャンプ。 ===== 1.ログイン OIDC(OpenID Connect)スタート ===== {{:logon-fido.png?600|}} ===== 2.ブラウザにリダイレクト+3.認可リクエスト+4.認証画面 ===== ==== 2.1.ClientAdapter経由 ==== * ClientAdapter=http://hpkicardless-clientadapter-server:3000 {{:logon-clientadapter.png?600|}} {{:clientadapterusage.png?400|}} \\ * 各クライアントと認証サーバーの仲介をするので、「ClientAdapter」と称する(のであろう) * 上図真中の「トークン集中管理」の場合は嬉しいが、左端の分散管理だと一枚layerかぶるだけとなるワナ。 ==== 2.2.認可サーバーの認可エンドポイントに到達 → 4.認証画面にジャンプ ==== * 認可サーバー=mahpki-auth.2nds.medis.or.jp * 認可エンドポイント=https://mahpki-auth.2nds.medis.or.jp/auth * この場面での到達url+クエリパラメータ https://mahpki-auth.2nds.medis.or.jp/auth/realms/mediauth/protocol/openid-connect/auth?client_id=FIDO&scope=openid&response_type=code&redirect_uri=http://hpkicardless-clientadapter-server:3000/authfido/callback&prompt=login&nonce=zXXXLE4T4_dpWnedoV9VPfJ2gTA&state=eyJyZXR1cm5UbyI6Ii9hdXRoZmlkby9sb2dpbj9zZWNvbmRhcnljZXJ0LWlkPTI5OTAwMyJ9&code_challenge_method=S256&code_challenge=LZ0apCeviQwFyYNRExX2KrTnHv-DXNb1phi5xNeRYKs * この場面での到達url+クエリパラメータの表示画面 {{:startauthscreen.png?200|}} === 2.2.1.到達urlのクエリパラメータ解説 === * client_id=FIDO ← 「FIOD:スマホ認証」ということ。マイナカード認証ならJPKI。HPKIカード認証ならHPKI。 * response_type=code ← 「認可コードグラント」ということ。ExamplePublisherClient.exe は、ネイティブアプリケーションでパブリッククライアントである。パブリッククライアント向けのグラントタイプとして推奨されている「認可コードグラント+PKCE」が採用されている。 * scope=openid ← [[https://zenn.dev/mryhryki/articles/2021-01-30-openid-connect|「scopeに openid を指定する、というのが OAuth2.0 と OpenID Connect の違いです」]] * redirect_uri=http://hpkicardless-clientadapter-server:3000/authfido/callback 8番で認可コード(Access Token)をもらった後コールバックで呼び出されるurl。__自由にリダイレクトURLが設定できてしまうと、外部に認可コード(Access Token)が漏れてしまうため、事前設定されたURLのみコールバックされる。__ そのため、クライアントアダプタのFQDNの統一化「サーバ名称=hpkicardless-clientadapter-server」が図られている。 * state=e3f1b42c0b5b4f5f9d7b3c9c3c8c3a9e * code_challenge=XyZAbCdEfGHiJkLmNoPqRsTuVwXyZ1234567890= (Base64 encoded SHA-256 hash of code_verifier) * code_challenge_method=S256 * nonce=b6f2d4a8e3c1a5b2d7c9e8f4b3c2a1d9 * prompt=login === 2.2.2.state,code_challenge,nonce(Number Used Once)の比較 === ^ Parameter ^ Used In ^ Protects Against ^ Checked by ^ | state | OAuth 2.0 | CSRF Attacks | Client app (compared with original value) | | code_challenge | OAuth + PKCE | Authorization Code Interception | Authorization Server (IdP) | | nonce | OpenID Connect | Token Replay Attacks | Client app (compared with original value) | * state:1番のあとクライアントはstateパラメーターの値としてランダム文字列を生成し、セッションと紐づけて管理する。2番3番の流れで認可サーバーにstateの値が渡され、8番、9番の流れでクライアントにstateの値が返ってくる。このとき、クライアントは「9のセッションとstateの値」が、「1のセッションとstateの値」と一致することを確認する。「雰囲気で使わずきちんと理解する!」39頁 * code_challenge:2番、3番の認可リクエストのパラメータにcode_challengeとcode_challenge_methodを追加する。4番で受け取ったcode_challengeとcode_challenge_methodを認可サーバーが保存する。11番のトークンリクエストのパラメーターにcode_verifierを追加  する。12番の認可サーバーによるcode_verifierの検証する。「雰囲気で使わずきちんと理解する!」74頁 * nonce:2 番、3番のリクエストでnonceが送られる。ユーザーの認証と同意が完了したら認証サーバーは8番でIDトークンにnonceを埋め込む。IDトークンを受け取ったクライアントはIDトークンのiss(トークン発行元)、aud(トークンの発行先)、iat(発行時間)、署名、nonceの値を検証する。「OAuth、OAuth認証、OpenID Connectの違いを整理して理解できる本」76頁 ===== 5.認証情報入力~8.認可レスポンス ===== * Usernameを入力した直後の到達url+クエリパラメータ:https://mahpki-auth.2nds.medis.or.jp/auth/realms/mediauth/login-actions/authenticate?execution=xxxxxxxx-1234-1111-xxxx-zzzzzzzzzzzz&client_id=FIDO&tab_id=x12XXzz1234 * Usernameを入力した直後の到達url+クエリパラメータの表示画面 {{:bioauth.png?200|}} ==== 5.1.keycloak ==== * 上述の4.認証画面と5.認証情報入力周りがKeycloakである。 * The Keycloak authentication page is the web-based login interface provided by Keycloak, an Identity and Access Management (IAM) system. This page is where users enter their credentials (such as username/password, FIDO authentication, or other methods) to log in. ==== 5.2.FIDO生体認証 ==== * Keycloakでチャレンジ・レスポンス認証が行われる。 * 秘密鍵は、スマートフォン内のセキュアストレージに保管されており指紋認証しないとアクセスできない。 cf.https://tarumi.co.jp/Resources/doku.php?id=ジェネリック薬品:電子処方箋#セカンド証明書 * 秘密鍵は、最初に{{:firstqrcode.jpg?200|}} を読み込んでパスワード入力したときに、「HPKIセカンド電子証明書」の秘密鍵がスマートフォンのセキュア領域に格納される。指紋認証できるスマホしか受け付けないのはこのため。 ===== 9.カスタムスキームによるアプリ起動 ===== ==== 9.1.カスタムスキーム ==== * 通常の認可コードグラントでは [[https://tarumi.co.jp/Resources2/doku.php?id=idトークン獲得の流れ#到達urlのクエリパラメータ解説|到達urlのクエリパラメータ解説]] 中で示したリダイレクトurl http://hpkicardless-clientadapter-server:3000/authfido/callback=xxxx がブラウザーに提示される。しかし、認可コードグラント+PKCEでは、カスタムスキーム myapp:// が呼ばれ、OSは登録されたNative application=ExamplePublischerClient.exeをブラウザーの代わりに起動し、認可コードを受け取る。「雰囲気」77頁 * ↑ により、登録されたアプリのみがコールバックを処理し、Browserの挙動も隠蔽できるため安全性が増す。 * カスタムスキーム myapp://は、Windows: Registers via the registry (HKEY_CLASSES_ROOT\myapp\shell\open\command) に登録されるとあるが見当たらない。RemoteSignatureClientAdapter.exeとExamplePublisherClient.exeの間で他の手段でやり取りされているのであろう、と推察する。なぜならIDトークンもPC上には残らず、管理サーバーからとってくる仕組みになっていることから、非常にSecuritySensitiveに作っているのだろう、と推測する。 ===== 10.トークンリクエスト ===== * ここで (OAuth2.0の)client_id=FIDO,__client_secret=xxxxxxxxxxxxxxxx__ が送られる。 * All requests are sent over HTTPS (TLS encryption) to prevent interception ===== 12.IDトークンの検証 ===== ==== 12.1.IDトークンの実体 ==== * jwt(JSON WebToken):HEADER.PAYLOAD.SIGNATURE * Header: Metadata, such as the algorithm used (alg) and token type (typ). * Payload: Contains claims (user data, expiration, etc.). * Signature: Ensures integrity by signing the token using a private key. ==== 12.2.IDトークンのHeader ==== === 12.2.1.IDトークンのHeaderで標準的なもの === *alg (Algorithm) – The cryptographic algorithm used for signing or encryption (e.g., RS256, HS256). *typ (Type) – The type of token, usually "JWT". *kid (Key ID) – Helps identify which key was used for signing (useful for key rotation). === 12.2.2.IDトークンのHeaderの実例 === *alg: RS256 *typ: JWT *kid: ipKzXxxlbS9wMP6oMO7wywCNKxbAbh4j9AOwN2-0eiU ==== 12.3.IDトークンのclaims ==== === 12.3.1.IDトークンのclaimsで標準的なもの === * iss (Issuer) – Who issued the token. * sub (Subject) – Who the token is about. * aud (Audience) – Who should accept the token. * exp (Expiration Time) – When the token expires. * iat (Issued At) – When the token was created. * nbf (Not Before) – When the token becomes valid. === 12.3.1.IDトークンのclaimsの実例 === * exp: 1739743326 * iat: 1739678526 * auth_time: 1739678526 * jti: 4a0d0ede-a713-4af9-8e03-dbf57ec8dd08 * iss: https://mahpki-auth.2nds.medis.or.jp/auth/realms/mediauth * aud: account * sub: 08e74bad-616c-460d-85bc-56dffe4512bb * typ: Bearer * azp: FIDO * nonce: auDFx2QhoZ4XwBhDQfegeCcs-v_tvEm9LSOtndDDR2s * session_state: 656ce900-97bf-4e4c-9ea0-41e2c6caacee * realm_access: {"roles":["default-roles-mediauth","offline_access","uma_authorization"]} * resource_access: {"account":{"roles":["manage-account","manage-account-links","view-profile"]}} * scope: openid profile email * sid: 656ce900-97bf-4e4c-9ea0-41e2c6caacee * DoctorID: 123456 * email_verified: False * HcRole: Medical Doctor * preferred_username: 123456 * SubjectSN: 123456 ==== 12.4.IDトークンのSignature ==== === 12.4.1.IDトークンの署名 === * Identity Provider (IdP)(≒認証サーバー)が、IdPの秘密鍵で署名する。それを、↓の公開鍵で検証することによって、眞正のIDトークンと判断する。 === 12.4.2.IDトークンのSignatureの公開鍵 === * [[https://mahpki-auth.2nds.medis.or.jp/auth/realms/mediauth/protocol/openid-connect/certs|公開鍵の場所]]から入手する。 === 12.4.3.IDトークンのSignatureの検証コード === {{ :verifyjwtsignature.zip |verifyjwtsignature.cs}} ====== OAutu2.0 と OpenIDConnect ====== ===== 提供機能の違い ===== ==== OAuth 2.0 = 「入館証」(認可) ==== * ビルに入るとき、受付で「このフロアには入れるけど、他のフロアには入れないよ」という「入館証」をもらったとします。 * この入館証を見せれば、特定の部屋には入れますが、あなたが誰なのか(名前や身分証の情報)はわかりません。 🔹 ポイント:OAuth 2.0は、「サードパーティアプリケーションによるHTTPサービスへの限定的なアクセスを可能にする認可フレームワーク」 を提供する。 * 例:「Twitterアカウントで他のアプリにツイートの権限を与える」 * 例:「Googleでログイン」ボタンを押す→「このアプリにあなたのプロフィール情報を共有しても良いですか?」 ==== OpenID Connect(OIDC) = 「身分証明書」(認証) ==== * 一方で、銀行で口座を開くときには、「あなたが本当に本人であること」を証明するために「身分証明書(免許証やパスポート)」が必要です。 * 身分証明書には「名前」「住所」「生年月日」などの情報が記載されており、これを見せることで**「私は◯◯です!」**と証明できます。 🔹 ポイント:OpenID Connectは、OAuth 2.0の仕組みを使って、「この人は本当に本人か?」(認証) を行う。 * 例:「Googleアカウントでアプリにログインする」 ===== 用語の違い ===== {{:rolenamedifference.png?600|}} ====== RemoteSignatureClientAdapterとRemoteSignatureService ====== ===== 提供機能の違い ===== ==== RemoteSignatureClientAdapter ==== * 起動するとnode.jsが走る。 * node.jsのWebSever上ポート番号:3000をリスニングする。 * config.json(含、OAuth2.0のsecret)を受け持つ。 {{ :config.json.zip |}} * IDトークンを受け取る。 ==== RemoteSignatureClientService ==== * node.jsのWebSever上ポート番号:5000をリスニングする。 * appsettings.json(含、HPKIセカンド電子証明書管理サービスクライアント証明書のPIN)を受け持つ。{{ :appsettings.json.zip |}} * 署名付き電子処方箋xmlを受け取る。 ====== ExamplePublisherClientのライブラリとWebAPI ====== ===== 提供機能の違い ===== ==== ライブラリ ==== * 署名API https://keymgsv-jpki-dev.hpki-cardless-signature.net/v1/Signature/Prescription/Sign にポストする。 * コアの部分 /// ライブラリ署名(処方) public PrescriptionXml PrescriptionSignLibrary() { // クライアント証明書の取得 // 環境に合わせてクライアント証明書を取得してください。 var cert = CreateClientCertificate( Properties.Settings.Default.ClientCertificatePath, Properties.Settings.Default.ClientCertificatePassword); var proxy = CreateProxy(); // 設定情報作成 var config = new RemoteSignatureConfiguration(new Uri(this.SignServerUri), cert, proxy); // 処方箋の署名 var signaturePrescription = new SignaturePrescriptionFacade(config); return signaturePrescription.Sign(this.CurrentToken, this.PrescriptionCsv); }    ⇓   public PrescriptionXml Sign(Token token, string prescriptionCsv) { if (string.IsNullOrEmpty(prescriptionCsv)) { throw new ArgumentNullException("prescriptionCsvのデータがありません"); } try { string prescriptionData = EncodeBase64Data(prescriptionCsv); PrescriptionXml xml = new PrescriptionXml(); xml.SetPrescriptionData(prescriptionData); string digest = xml.GetPrescriptionDocumentDigestValue(); SignatureRequest request = new SignatureRequest { Token = token.Original, Digest = digest }; FillInstitute(request, prescriptionCsv); SignatureResponse signedXml = GetSignedXml(request); xml.SignStatus = new SignStatus(signedXml); if (!string.IsNullOrEmpty(signedXml.SignedXmlData)) { xml.SetPrescriptionSign(signedXml.SignedXmlData.Base64ToUtf()); } return xml; } catch (Exception ex) { throw; } }    ⇓   private SignatureResponse GetSignedXml(SignatureRequest request) { return new WebApiRequest(Configuration.Uri, Configuration.ClientCertificate, Configuration.Proxy).Post("v1/Signature/Prescription/Sign", request); } ==== WebAPI ==== * 署名APIUrlのポート番号:5000で待ち受けしているRemoteSignatureClientServiceにポストする。 * コアの部分 /// WebApi署名(処方) public byte[] PrescriptionSignWebApi() { // 処方箋の署名 var request = new JObject( new JProperty("token", this.CurrentToken.Original), new JProperty("csvData", ToBase64(this.PrescriptionCsv)) ); using (var client = new WebClient()) { client.Headers[HttpRequestHeader.ContentType] = "application/json"; client.Encoding = System.Text.Encoding.UTF8; var result = client.UploadString(this.SignApiUri + "signature/prescription/sign", request.ToString()); var resJson = JObject.Parse(result); if (!resJson.SelectToken("isSuccess").Value()) throw new Exception($"{resJson.SelectToken("statusMessage").Value()}({resJson.SelectToken("statusCode").Value()})"); return Convert.FromBase64String(resJson.SelectToken("signedXmlData").Value()); } } ==== 「管理サーバーに何を渡しているのか」の考察 ==== === 処方箋情報の扱い === * ライブラリを選んだ場合 string digest = xml.GetPrescriptionDocumentDigestValue(); SignatureRequest request = new SignatureRequest { Token = token.Original, Digest = digest }; FillInstitute(request, prescriptionCsv); SignatureResponse signedXml = GetSignedXml(request); xml.SignStatus = new SignStatus(signedXml); if (!string.IsNullOrEmpty(signedXml.SignedXmlData)) { xml.SetPrescriptionSign(signedXml.SignedXmlData.Base64ToUtf()); } return xml; をみると、IDトークンとPrescriptionDocumentのHash値を、管理サーバーの署名APIに渡しているようである。__PrescriptionDocument要素のハッシュ値のみ管理サーバーに渡し、管理サーバーがもっているKeyinfo要素とSignedProtperties要素とあわせた3要素を、管理サーバーのセカンドHPKI証明書の秘密鍵で署名したSignatureValueをもつPrescriptionSign要素を返却__してきて、xml.SetPrescriptionSign(signedXml.SignedXmlData.Base64ToUtf());で両者を合体しているのだろう。 * WbAPIを選んだ場合 var request = new JObject( new JProperty("token", this.CurrentToken.Original), new JProperty("csvData", ToBase64(this.PrescriptionCsv)) ); var result = client.UploadString(this.SignApiUri + "signature/prescription/sign", request.ToString()); をみると、IDトークンと処方箋csvをRemoteSignatueClientServiceに渡しているようである。↑のライブラリのコードから推察すると、RemoteSignatueClientService.exeがローカルでcsvDataから、PrescriptionDocument要素を作る。__PrescriptionDocument要素のハッシュ値のみ管理サーバーに渡し、管理サーバーがもっているKeyinfo要素とSignedProtperties要素とあわせた3要素を、管理サーバーのセカンドHPKI証明書の秘密鍵で署名したSignatureValueをもつPrescriptionSign要素を返却__してきて、ローカルでRemoteSignatueClientService.exeが処理したPrescriptionDocument要素と合体しているのだろう。 === HPKIセカンド電子証明書管理サービスクライアント証明書の扱い === * ライブラリを選んだ場合 /// ライブラリ署名(処方) public PrescriptionXml PrescriptionSignLibrary() { // クライアント証明書の取得 // 環境に合わせてクライアント証明書を取得してください。 var cert = CreateClientCertificate( Properties.Settings.Default.ClientCertificatePath, Properties.Settings.Default.ClientCertificatePassword); var proxy = CreateProxy(); // 設定情報作成 var config = new RemoteSignatureConfiguration(new Uri(this.SignServerUri), cert, proxy); // 処方箋の署名 var signaturePrescription = new SignaturePrescriptionFacade(config); return signaturePrescription.Sign(this.CurrentToken, this.PrescriptionCsv); } より、「HPKIセカンド電子証明書管理サービスクライアント証明書」を渡しているようである。 * WbAPIを選んだ場合 var request = new JObject( new JProperty("token", this.CurrentToken.Original), new JProperty("csvData", ToBase64(this.PrescriptionCsv)) ); var result = client.UploadString(this.SignApiUri + "signature/prescription/sign", request.ToString()); では、「HPKIセカンド電子証明書管理サービスクライアント証明書」を渡していないように見える。多分、RemoteSignatureClientService.exeがappsettings.jsonの情報に基づいて認証を行っているのであろう。 ===== どちらを選ぶべきか ===== * WebAPIを選んでRemoteSignatureClientServiceに丸投げするほうが簡単でstraightforward. ====== Tools&Tips ====== ===== Tools ===== * 5000番ポートをlisteningしているかどうか確認 netstat -ano | findstr :5000 * [[https://tips.crosslaboratory.com/post/json-viewer-editor/|JSON Viewer/Editor バージョン1.0.0公開]] ===== Tips ===== * ClientAdapter=http://hpkicardless-clientadapter-server:3000 を https://にすると、他マシンで動かしている場合にはエラーが発生する。完全ローカルなら問題なし。