fbpx

CL LAB

HOME > CL LAB > keycloak > Keycloak(15.02)でOpenID Connect CIBAとプッシュ通知を試す #keycloak #ciba #oauth #oidc

Keycloak(15.02)でOpenID Connect CIBAとプッシュ通知を試す #keycloak #ciba #oauth #oidc

 ★ 5

1. 目次


  • 1. 目次
  • 2. 概要
  • 3. Keycloak14系から15系でのCIBA関連の差分
  • 4. CIBAの認証方法について
  • 5. 今回試す全体像の説明
  • 6. 今回使う環境の説明
  • 7. 検証用データの作成・確認
  • 8. シーケンス図でのやりとりベースでの解説
  • 9. 後書き

2. 概要


shiba チームの中村です。 今回は周りから CIBA = プッシュ通知が来て認証及び認可を行う というような話をよくにお聞きするので、CIBAと認証周りお話と、実際に Keycloakの最新版である 15.02 のバージョンでCIBAを絡めて、プッシュ通知でユーザーに通知が行き、プッシュ通知で許可する一連の流れを簡単に試してみたいと思います。

今回の記事で解説する箇所

今回の記事では主に下記の部分について解説していきます。
  • Keycloak14.0.0から15.0.2でCIBAに関連する機能がどう変わったか
  • CIBAと認証について
  • プッシュ通知を含んだフローの簡易版のデモ

今回の記事で説明しない所

下記の内容については詳細な解説は割愛させていただきます。参考になるリンクを貼っておいたので必要に応じて確認お願いします。

3. Keycloak14系から15系でのCIBA関連の差分


では14系から15系へのバージョンアップにおいて、主な差分としては下記のようなものになります。
  • 3.1 FAPICIBA および OpenBanking Brasil に対応
  • 3.2 CIBA pingモードのサポート
  • 3.3 CIBAのサポートレベルが Supported に変更
    • デフォルトで有効化
これらについて後ほど1つずつ簡単に見ていきます。
また、各バージョンのリリース情報は下記をご確認ください。

3.1 FAPICIBA および OpenBanking Brasil に準拠

リリースノートを見る限り、FAPICIBA および OpenBanking Brasilに対応したようです。

ではFAPIとFAPICIBAについて簡単にお話しますと、FAPI(Financial-grade API)は OpenID Foundation の Financial-grade API ワーキンググループ が策定している技術仕様です。

この仕様は、標準的なOAuthやOpenID Connectで提供されるよりも高いレベルのセキュリティを必要とする、セキュリティと相互運用性に関する具体的な実装ガイドラインを提供することを目的とした、安全性の高いOAuth関連の仕様の集まりです。元々はオープンバンキング関連のシナリオでの使用を目的としていましたが、現在は他の高度なセキュリティのユースケースにも急速に拡大しており、あらゆる市場分野のAPIに適用できます。

ではFAPI CIBAは何かというと、CIBAの仕様を他のFAPIの仕様と組み合わせるために、推奨事項などの仕様の集まりを提供しているものです。詳しくは FAPI 1.0 の CIBA Profileをご確認ください。

ただし、OpenID Foundationの Certified Financial-grade API Client Initiated Backchannel Authentication Profile (FAPI-CIBA) OpenID Providers のページ見る限り、まだ FAPI CIBA の認定はまだ取得していないように見えます。また、redhatさんの issue で現状を確認するとPass All Conformance Tests for FAPI OpenID testsuite after Keycloak 15という チケットがまだ開いているので、テストスイートの合格を目指す状態のようです。

次に、OpenBanking Brasil ですが、これは Open Banking Brasil Financial-grade API Security Profile への対応を指しており、Open Banking Brasil GT Security によって作成された仕様です。

この仕様は Financial-grade API Security Profile 1.0 - Part 2: Advanced で提供されるよりも高いレベルのプライバシーを必要とするブラジルのオープンバンキング分野のAPIに適用可能な、セキュリティと相互運用性に関する具体的な実装ガイドラインを提供することを目的とした仕様の集まりです。

また Keycloak15.02のdocumentの Open Banking Brasil Financial-grade API Security Profile やredhatさんの issue OpenBanking Brasil support にもあるように、Keycloakが対応しているのは Open Banking Brasil Financial-grade API Security Profile 1.0 Implementations Draft1 です。しかし、現在はそのDraftである Open Banking Brasil Financial-grade API Security Profile 1.0 Implementers Draft 2 がでており、Draft1は非推奨とされていますのでご注意ください。

3.2 CIBA pingモードのサポート

次にCIBA pingモードのサポートです。CIBAはIDトークンやアクセストークンないしはオプションでリフレッシュトークンなどを受け取る手法として poll, ping, push という3つのモードがあります。Kyecloakは13.0.0からCIBAのサポートを開始してから長らくpollモードのみでしたが、15.0.0でpingのサポートが増えました。

3.3 CIBAのサポートレベルが Supported に変更

Keycloak14系ではCIBAに関する機能がPreview状態で、 デフォルトで無効でした。ですがバージョンが上がったことで、Supportedになるとともに、デフォルトで有効になりました。これは、14系のバージョンまでの下記のどちらかの対応が不要になったということです。

  • Dkeycloak.profile.feature.ciba=enabled のようにCIBAの設定だけ有効化する
  • Dkeycloak.profile=preview のように preview 機能を有効化する

4. CIBAの認証方法について


では次にCIBAの認証方法の話についてです。最初に結論を述べますが CIBAで認証の方法は仕様として何も決まっておりません。 

Oauth2.0 ではユーザーの認証の方法は仕様の範囲外のため決まっておりませんが、IDとPasswordでのフォームによるログインをよく見ます。あれらは仕様による制約によるものではありません。

同じようにCIBAも認証の方法は仕様の範囲外のため決まっておりませんが、プッシュ通知をよくみるという話です。勿論、仕様による制約ではありません。

例えば、電車の切符などのようにプッシュ通知が届くスマホを持っている事ということのみで権限がある対象として認証し、購入するかどうか・許可を与えるかどうかを問うケースも考えられますし、派生で指紋認証などで本人を確認するケースも考えられます。この点についてはFAPI CIBAの7.7. Authentication Device securityにも記載されていますが、こちらでも具体的な認証方法などは記載されておりません。

5. 今回試す全体像の説明


先程のパートで認証方式は任意の方法だと説明させていただきました。そこで今回は簡単にプッシュ通知を含んだ処理がどう見えるかを実際に試してみます。

プッシュ通知を試す環境は下記の通りです。

Xcode 12.5.1 (12E507)
環境(シミュレーター) iOS (iPhone SE - 2nd generation)
検証方法 Xcode のシミュレーターにコマンドラインツールでプッシュ通知を呼び出す
今回はKeycloakでプッシュ通知を含めてCIBAのフローを検証します。今回試す処理の簡易のシーケンス図が下記になります。

上記のポーリングでやりとりするケースのシーケンスの中で緑色の点線に囲まれた箇所がCIBAの仕様で決まっている部分で、主に下記の2箇所です。

  • Authentication Request とそのレスポンス
  • Token Request とそのレスポンス
それ以外の箇所はCIBAの仕様で決まっていない部分で、今回はClient側をMacのターミナルのアプリを用います。Keycloakから通信するAuthentication entity以降の流れはXcodeのシミュレーターを用いて、ユーザープッシュ通知で確認していきます。

今回のサンプルアプリケーションやdockerに関するソースコードは下記の場所にアップロードしています。

6. 今回使う環境の説明


ではまず今回試した環境について説明していきます。今回はdocker環境で試すこととします。keycloakのイメージは jboss/keycloak を用いて、認証用のエンティティとしては仮でRuby + Railsで簡易のAPIを実装していきます。

ここで例となるdocker-composeの設定例を記載します。

version: '3.8'

services:
  keycloak:
    container_name: keycloak
    image: jboss/keycloak:15.0.2
    command: -b 0.0.0.0
    ports:
      - "8088:8080"
    volumes:
      - ./docker/keycloak/demo-config/standalone-ha.xml:/opt/jboss/keycloak/standalone/configuration/standalone-ha.xml
    environment:
      KEYCLOAK_USER: admin
      KEYCLOAK_PASSWORD: password
  
  authn-server:
    container_name: authn-server
    build:
      context: ./docker/authn-server
    command: bash -c "rm -f tmp/pids/server.pid && bundle e rails s -p 3000 -b '0.0.0.0'"
    ports:
      - 3000:3000
    volumes:
      - ./docker/authn-server:/my_app

上記のdocker-composeの設定で確認が必要な点は2箇所です。

1箇所目は authn-server についてです。CIBAのドキュメントでは、ADによるユーザー認証の方法を規定しておらず、Keycloakでは、この認証を外部の authentication entity に委ねます。今回の検証では、 authentication entity の機能の一部を行うために authn-server というコンテナを作成します。

また、この authn-server が行うのは下記の2点のみです。

  • APIとして Authentication Delegation Request を
    受け取り、レスポンスを返す
  • プッシュ通知検証用に apns ファイル(JSON)を作成する

制約があまりないためお好きな言語で実装していただけますが、Ruby+Railsのの例だと下記のような感じになります。

module Api
    module V1
      class AuthController < ApplicationController
        def delegation
          alert = {
            title: 'ユーザーの購入の許可の確認について',
            subtitle: "#{params[:login_hint]} さんの A Shop で購入の許可の申請",
            body: "識別コードは #{params[:binding_message]} です。"
          }
          path = Rails.root.join('tmp/apns')
          File.open("#{path}/test.apns", 'w') do |file|
            apns_hash = { 
              aps: {
                alert: alert,
                sound: 'default',
                badge: 1,
                category: 'TEST_CATEGORY',
              },
              PUSH_TEST_MESSAGE_ID: '0000000001',
              AUTHORIZATION: request.headers['Authorization'],
            } 
            JSON.dump(apns_hash, file)
          end
          
          head(201)
        end
      end
    end
  end

 

2箇所目はauthentication Channel Providerの設定です。Keycloakで CIBA を使う時は下記2つのプロパイダーを利用します。

  • Authentication Channel Provider
    KeycloakとAD(Authentication Device)を介して実際に利用者を認証するエンティティとの間のコミュニケーションを提供。
  • User Resolver Provider
    クライアントから提供された情報からKeycloakのUserModelを取得し、ユーザを識別。

Keycloakには両方のデフォルトプロバイダーが準備されていますが、Authentication Channel Provider のみ設定する必要があります。公式の設定例を見てみましょう。

<spi name="ciba-auth-channel">
    <default-provider>ciba-http-auth-channel</default-provider>
    <provider name="ciba-http-auth-channel" enabled="true">
        <properties>
            <property name="httpAuthenticationChannelUri" value="https://backend.internal.example.com/auth"/>
        </properties>
    </provider>
</spi>

この設定の中で変更が必要な項目は、httpAuthenticationChannelUri です。この項目はAD(Authentication Device)を介してユーザーを実際に認証するエンティティのURIで、実際に検証を行う場合などはこの値を指定する必要があります。デフォルトでは、https://backend.internal.example.com/auth になっています。

より詳しい詳細は Keycloakのドキュメントの provider-setting をご確認ください。

ではこの設定をどこで設定するかというと、standalone-ha.xmlを書き換えることで試してみます。これは jboss/keycloak のdockerのイメージでは docker-entrypoint.sh の中で、サーバー構成のパラメーターを渡さない場合はデフォルトでは standalone-ha.xml を使用するためです。今回は設定の変更を、standalone-ha.xml で行っていますが、CLIからの設定も可能です。

例示したdocker-composeでは下記のように volumes で standalone-ha.xml を書き換えていますが、適宜自分の設定で置き換える用に設定してください。

volumes:
  - ./docker/keycloak/demo-config/standalone-ha.xml:/opt/jboss/keycloak/standalone/configuration/standalone-ha.xml

ではどんな風に書き換えるかというと、下記の <subsystem xmlns="urn:jboss:domain:keycloak-server:1.1">の要素の中に該当の処理を追加していきましょう。

<subsystem xmlns="urn:jboss:domain:keycloak-server:1.1">
    ※多くの処理
</subsystem>

追加する位置についてですが、他のSPIの設定 <spi name="hostname"> の要素の後ろに下記のように追加してみましょう。今回追加した設定は <spi name="ciba-auth-channel">の要素の処理になります。

<spi name="hostname">
    <default-provider>${keycloak.hostname.provider:default}</default-provider>
    <provider name="default" enabled="true">
        <properties>
            <property name="frontendUrl" value="${keycloak.frontendUrl:}"/>
            <property name="forceBackendUrlToFrontendUrl" value="false"/>
        </properties>
    </provider>
    <provider name="fixed" enabled="true">
        <properties>
            <property name="hostname" value="${keycloak.hostname.fixed.hostname:localhost}"/>
            <property name="httpPort" value="${keycloak.hostname.fixed.httpPort:-1}"/>
            <property name="httpsPort" value="${keycloak.hostname.fixed.httpsPort:-1}"/>
            <property name="alwaysHttps" value="${keycloak.hostname.fixed.alwaysHttps:false}"/>
        </properties>
    </provider>
</spi>
<spi name="ciba-auth-channel">
    <default-provider>ciba-http-auth-channel</default-provider>
    <provider name="ciba-http-auth-channel" enabled="true">
        <properties>
            <property name="httpAuthenticationChannelUri" value="http://authn-server:3000/api/v1/auth"/>
        </properties>
    </provider>
</spi>

ですが、この http://authn-server:3000/api/v1/auth はあくまで今回のdocker-composeの設定の場合での一例です。各自の環境に合わせた FQDN や IP、 Port、パスを設定してください。

7. 検証用データの作成・確認


CIBAを試すためには検証用の適当なデータが必要です。Keycloakの管理画面にログインして準備していきましょう。この資料に記載したようなdocker-composeの設定をしている場合はURLとユーザー情報は下記のような情報になります。

管理画面URL http://localhost:8088/auth/admin/
Username
admin
Password password

管理画面にログイン後、下記のデータを順に設定してください。

  • レルムの作成
  • ユーザーの作成
  • クライアントの作成
  • シークレットトークンの確認
  • CIBAの設定の確認・変更  (※ この変更はより検証がしやすくするための変更です)

レルムの作成

まずは任意のレルムの作成をしてください。今回の記事ではレルムの名前は SampleRealm で作成します。今後の文章ではレルム名の部分を SampleRealm として説明していくので、今後の資料では適宜自分の環境に合わせて読み替えてください。

ユーザーの作成

次に ユーザーを作成します。今回の記事では下記の値で作成します。

フィールド名
Username cl-taro

今後の文章ではユーザーの値を上記の値として説明していくので、今後の資料では適宜自分の環境に合わせて読み替えてください。

Usernameは任意の値で問題ありませんが、後々の手順で使用する機会があるため、検証時はわかりやすい名前のほうがよいかもしれません。

クライアントの作成

次に クライアントの作成 をします。今回の記事では下記の値で作成します。

フィールド名
Client ID
test-client
Client Protocol
openid-connect ※これはデフォルト値です

備考: Root URL は今回のフローでは登録不要です。

作成後の Test-client の Setting タブで下記の変更を行っていきます。

  1. Access Type のセレクトボックスの値を public から confidential に変更
  2. Standard Flow Enabled を ON から OFF に変更
    補足: CIBAのフローではリダイレクト用のURIの登録は不要です。ですが、デフォルトだと他のフローのサポートのために、リダイレクト用のURI登録が必要になります。そのため Standard Flow Enabled を意図的に無効にします。例えば、仮に ON のままクライアントを保存しようとすると次のエラーが表示されます。

    Error! You must specify at least one redirect uri

  3. OIDC CIBA Grant Enabledのチェックを OFF から ON に変更

上記の設定が終わり次第 Save からクライアントに対する設定を保存してください。

クライアントのシークレットトークンの確認

Test-client の Credentials タブで Secret の値を確認します。今回の例では仮に 996334ba-b796-4d14-a3ba-01e3420953b8 とします。この値は各環境で変わりますので、適宜自分の環境に合わせて読み替えてください。

Client Authenticator は初期値のままで問題ありません。念の為 Client and Secret であることを確認しておいてください。

CIBAの設定の確認・変更

レルムのAuthenticationのConfigureの画面にCIBA Policy というタブがあると思うので開いてください。

サンプルのようなdockerの設定と、レルム名にしている場合は下記のようなURLで開くこともできます。

http://localhost:8088/auth/admin/master/console/#/realms/SampleRealm/authentication/ciba-policy

 

keycloak-ciba-setting

各項目を簡単に説明していきます。

名前 説明
Backchannel Token Delivery Mode
ライアントがアクセストークンやオプションでリフレッシュトークンなどを取得する方法で。CIBAでは "poll", "ping", "push" の3つがあります。Keycloakは現時点では"poll" 及び "ping" モードのみをサポートしています。 pushモードには対応しておりません。
Expires In
auth_req_id の有効期限を秒単位で示す、正の整数値です。Keycloakのデフォルト値は "120" です この値はCIBAの仕様上必須です。
Interval
クライアントがトークンエンドポイントへのポーリング要求を再度行うまでに待機しなければならない最小時間を、秒単位で示す正の整数値です。指定がない場合のデフォルト値はCIBAの仕様上は5秒です。Keycloakのデフォルトも "5" ですこの値はCIBAの仕様上オプショナルです。
Authentication Requested User Hint
認証が要求されているエンドユーザーを識別する方法です。CIBAでは "login_hint", "login_hint_token", "id_token_hint" の3つがあります。 Keycloakは現時点では "login_hint" のみサポートしており、デフォルトで "login hint" です。

今回の検証では各リクエストに着目して少しずつ確認していくため、検証途中で auth_req_id の有効期限切れをむかえないようにExpires Inの設定を "600" に変更しておきしょう。

より詳しい詳細はこちらをご確認ください。

8. シーケンス図でのやりとりベースでの解説


ではシーケンス図での各やりとりなどをベースに処理を簡単に説明していきますが、もう1度、簡易シーケンス図をふりかえってみましょう。

大まかなかたまりとしては、 ①~⑥は クライアントのTerminal がKeycloakに認証を要求する部分です。内部でKeycloakはAutentication entityに認証を委任するリクエスト(③)を送信しています。

次のかたまりは、⑥~⑩で プッシュ通知を送りユーザーが許可し、認証結果をKeycloakに返す(⑩)部分です、今回はプッシュ通知を試す環境はXcodeのシミュレーターを用いて確認を行っていきます。

そして最後の ⑪~⑫はアクセストークンなどをリクエストで取得する部分です。ここは 前回のCIBAに関する記事 から変更はありませんが、認証が終えていない時にトークンをリクエストしてエラーが返ってくるケースを今回は省略しています。

⑪ Token Request
⑫ Successful Token Response

では①番から順に各処理を見ていくことにしましょう。

① Terminalを起動

任意のターミナルを起動してください。Mac の ターミナル でもいいですし、iTerm などのターミナルアプリでも構いません。今回は Mac のデフォルトの ターミナル で行っていきます。

② Authentication Request

CIBAのフローはまず認証を要求するリクエストから始まります。

このリクエストは、HTTP POST メソッドを使用し、パラメータを application/x-www-form-urlencoded 形式でHTTPリクエストのBodyに含め送信します。

また、クライアントの認証方式に関しては OpenID Connect Core 1.0 で、client_secret_basic として説明されているような、Basic認証の形式を用いて行っていきます。

この形式は クライアントIDとクライアントシークレットをコロン(:)ではさんで結合しbase64化したものを Authorization ヘッダーに載せます。私の設定の場合は、クライアントIDが test-client で クライアントシークレットが 2ae6d81d-bc78-434e-a7b8-985921e0d47b なので、Macでのコマンドの一例だと下記のようになります。

$ echo -n 'test-client:996334ba-b796-4d14-a3ba-01e3420953b8' | openssl base64

dGVzdC1jbGllbnQ6OTk2MzM0YmEtYjc5Ni00ZDE0LWEzYmEtMDFlMzQyMDk1M2I4

今回使うヘッダーは下記になります。

ヘッダー名 説明
Content-Type
application/x-www-form-urlencoded
Authorization
Basic dGVzdC1jbGllbnQ6OTk2MzM0YmEtYjc5Ni00ZDE0LWEzYmEtMDFlMzQyMDk1M2I4
client_secret_basic 形式時の渡し方です。

 

Bodyに含ませる値は下記になります。

名前 説明
scope
openid email example-scope
アクセスを要求している範囲です。この値は必須です。
login_hint
cl-taro
認証が要求されているエンドユーザーを特定するための、OpenIDプロバイダーへのヒントになる情報です。
Keycloakのデフォルトでは、これはユーザーのユーザー名になります。
CIBAの仕様としては login_hint_token, id_token_hint, login_hint のうち1つは必須になります。
binding_message
識別コードは hogefuga です。
CD(consumption device) と AD(authentication device) の両方に表示されることを意図した人間が読める識別子またはメッセージなどです。
この値により、エンドユーザーは、AD(authentication device)で行われたアクションがCD(consumption device)で開始された要求に関連していることを確認することができるため、CIBAの仕様としてはこの値はオプショナルですが、多くの場合必要だと個人的には考えております。

またリクエストURLには レルム名 を含んでおります。各自作成したレルム名で読み替えてください。http://localhost:8088/auth/realms/{レルム名}/protocol/openid-connect/ext/ciba/auth

このリクエストのより詳しいKeycloak側の詳細は Keycloakのドキュメントの Backchannel Authentication Endpoint を、Authentication request自体についてはOpen ID Foundation の CIBAのドキュメントの 7.1. Authentication Request ご確認ください。

サンプルの場合はこのようになります。http://localhost:8088/auth/realms/SampleRealm/protocol/openid-connect/ext/ciba/auth

サンプルの cURL文としては下記になります。

curl -i -X POST \
-H "Content-Type:application/x-www-form-urlencoded" \
-H "Authorization:Basic dGVzdC1jbGllbnQ6OTk2MzM0YmEtYjc5Ni00ZDE0LWEzYmEtMDFlMzQyMDk1M2I4" \
-d "scope=openid email example-scope" \
-d "login_hint=cl-taro" \
-d "binding_message=識別コードは hogefuga です。" \
'http://localhost:8088/auth/realms/SampleRealm/protocol/openid-connect/ext/ciba/auth'

 ③ Authentication Delegation Request

この処理は認証を要求するリクエストです。KeycloakにCIBAの仕様に沿った ②のAuthentication Request のリクエストが届くと、KeycloakはAuthentication entityに認証を委任するリクエスト(Authentication Delegation Request)をHTTP POST メソッドを使用して送ります。

このリクエストのヘッダーは下記になります。(※ このリクエストは Keycloakが送信するリクエストのため、Authorization ヘッダーの値などはKeycloak側が都度決めており下記の表の値は仮で記載しております)

ヘッダー名 説明
Content-Type
application/x-www-form-urlencoded
Authorization
Bearer eyJhbGciOiJSUzI1NiIsInR5

※ 1 省略した値です

この値は都度違う識別子で Authentication entity が認証の結果をKeycloakに通知するリクエストを行うときに使用します。

※1 実際の値は下記のような長い文字列が入りますが、資料ではみやすさのため今後省略した値を用います。

  • 実際の値のサンプル: Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJVdVBUQUF6bGZZN1Y1c2Y3eExQeHVObmhYUTZXRUM0emRDZEM2eldUQWVvIn0.eyJleHAiOjE2MzIyMjAxODEsImp0aSI6IjA0OTBlZmZiLWEwMjctNGMzZi1hOGJmLTg2MGIzYTFkNWI2ZCIsImlzcyI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODA4OC9hdXRoL3JlYWxtcy9TYW1wbGVSZWFsbSIsImF1ZCI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODA4OC9hdXRoL3JlYWxtcy9TYW1wbGVSZWFsbSIsInN1YiI6ImNjYWIyMjAxLWZlYWMtNDQ4Yi1iNGEyLWE3YWQ2ZTkxZDI2NSIsInR5cCI6IkJlYXJlciIsImF6cCI6InRlc3QtY2xpZW50In0.cUfq_s_3yGJ_BN_pDO6VmY6ge3AYsMQ38zr6SJ96WRQzfzYwf-2hljBcRz0SZgX_byUZ9j5tvkjMLV3Y0hKPA3SkJZ_FQw3FghACoizfU7LbzHxibqKAD7gD6raztSeO4159GHQDPvF5vunpY__-g4hNXjw0R_30NVWcOun5rP85N6vKj1-gC9Qd0_LCoZNXQPOVoRwYNOGg5hHf9hZ87WPFCAa5X9d04XaxWZKatxVVsLOGBEkk6HerKV0Amt89t5diJQcOQtt009Rll3OyxOX43vJTicuhh1_H0R5leH9lTvcszhRG4lHNr9-A_ctsVi_5FrqCw62EvfIhaeeKLA
  • 省略した値のサンプル: Bearer eyJhbGciOiJSUzI1NiIsInR5

Bodyに含ませる値は下記になります。

名前 説明
scope
openid email example-scope profile roles email
Authentication entityが認証後にユーザーに同意を求める範囲です。この値は必須です。
login_hint
cl-taro
認証が要求されているエンドユーザーを特定するための、Authentication entityへのヒントになる情報です。
Keycloakのデフォルトでは、これはユーザーの ユーザー名 になります。
is_consent_required
false
Authentication entityが、scopeについて認証されたユーザーから同意を得る必要があるかどうかを示す値です。
binding_message
識別コードは hogefuga です。
②のAuthentication Requestに含めた値と同じで、CD(consumption device) と AD(authentication device) の両方に表示されることを意図した人間が読める識別子またはメッセージなどです。

このリクエストは、Authentication Channel Providerの設定で登録したURLにリクエストを送信します。例えば、今回は下記のように設定を記載したので、 http://authn-server:3000/api/v1/auth になります。

<spi name="ciba-auth-channel">
    <default-provider>ciba-http-auth-channel</default-provider>
    <provider name="ciba-http-auth-channel" enabled="true">
        <properties>
            <property name="httpAuthenticationChannelUri" value="http://authn-server:3000/api/v1/auth"/>
        </properties>
    </provider>
</spi>

 

④ プッシュ通知検証用のapnsファイルを作成

この処理は今回のフローでは前回のフローとは違い、ネイティブアプリのプッシュ通知(リモート通知)を、Xcodeのシミュレーターに対して試すために必要です。

ではapnsファイルとはなにかというと、簡単に説明するとプッシュ通知関連の振る舞いを決めるJSONファイルで、その中で最も重要な部分はapsで、Apple定義のキーが含まれており、通知を受け取ったシステムがどのようにユーザーに通知を出すのかなどが含まれます。

また、このapnsファイルには PUSH_TEST_MESSAGE_ID や AUTHORIZATION のようなカスタムキーも含むことも可能です。

Appleのリモート通知用のJSONのフォーマットの詳細については公式のドキュメントの Generating a Remote Notification をご確認ください。

今回使うサンプルのファイルは下記のようなものになります。
{
    "aps": {
        "alert": {
            "title": "ユーザーの購入の許可の確認について",
            "subtitle": "cl-taro さんの A Shop で購入の許可の申請",
            "body": "識別コードは hogefuga です。"
        },
        "sound": "default",
        "badge": 1,
        "category": "TEST_CATEGORY"
    },
    "PUSH_TEST_MESSAGE_ID": "0000000001",
    "AUTHORIZATION": "Bearer eyJhbGciOiJSUzI1NiIsInR5"
}

 

今回試すプッシュ通知用のJSONで用いるカスタムキーは下記の2つです
カスタムキー名 用途
PUSH_TEST_MESSAGE_ID
ユーザーへの許可を依頼するプッシュ通知を他のプッシュ通知などと識別するために、仮で決めたIDを渡すために使っています。
AUTHORIZATION
Authentication Delegation Request のリクエストで受け取った、 Authroization ヘッダーの値です。

今後でてくる、 ⑩ Authentication Result Notification のリクエストで使うために渡します ※1

※1 今回は検証の用途で直接リクエストを贈りますが、実際に実装する場合はネイティブアプリから直接KeycloakのAPIを叩くことはほぼなく、間に任意のシステム or プロキシが挟まるのではないかと思います。

⑤ Response (③ Authentication Delegation Request のレスポンス)

③ Authentication Delegation Request のレスポンス(⑤)として必要な用件は、 HTTP status code として 201 を返却することだけです。

⑥ Successful Authentication Request Acknowledgement

②の Authentication Request のレスポンス(⑥)は application/json 形式でauth_req_id などを返却します。

レスポンスを整形すると下記のようになります。

{
    "auth_req_id": "eyJhbGciOiJkaXIiLCJlbmMiOiJBMTI",
    "expires_in": 600,
    "interval": 5
}

※ auth_req_idは実際の値は下記のような長い文字列が入りますが、資料ではみやすさのため省略した値を用います。

  • 実際の値のサンプル: eyJhbGciOiJkaXIiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2In0.._tjXHp5sj8wsavnwlRtgqw.aZChxARXLiIKgnCFBhYORdUJzp-G-cDL74pxjwWVljQ0aaAaZJuxwl_ix-SskbwKykUcj_Qwh3kM8b8CtmCU7oytGgJIx5uTYfd0M_T5FRUxWvjq2vlqvcojvvlbK_LaUk6ARAPK_lmlbNmBqw6i8JAXRtgXX8c60T_8GqfYLpnz8L6kRlMpmvsiKK6n6faczO31MVSfvBrwD4ntLdkyEvIwEBDR6VcdQzfRm0NUke82dXN7kYMinz5LkcQ-GIIFsZMYh6fLrq29THBZbBddHJx1kklWn3yWkrdEPzzlhN-eqC8b2wycJUVvuuVyjku-wDp1X3L-rJ2V5sHaKXaeC_aDuKbEbp9Ph7HyboHV1hdc48k_NZG4RiN-Kn0bA47IFq2WSegdzR2OiDm_53RQTOyCeVIrFhZZeU0WU7jIovnA5EnoB0jv9WhJV7e4C3UTt2mJCLvPHgK57XOKDWGQDweXU-NZnThfBLYKT_689qprEz0D42hdzlpdVgu43x8ogpAV5nxR1SmIbk0fFofUe31Rpc2aX1jGQldGbo06tY_7S4zZUsng2LWr3io5rwAK2TUissPpzn8gYGCEaAV82Ar872AX3J1rFp7h6YPGMBhzejVk-pcLc1QjM3i4sjsnfXnMHpLXdMECR8jPQBLcR__8Y1qZ58ZBsImBQN0GwRl3Z5nRmSdWTg8T3JNvIVy0j-V4pss5neMVP9wBCPNxnuwU_JqcRg9Go4miqMy55mCsdUMAJ4X-C8cUR1CMLEgrgIh7TMZLB1gmHc0PXcvpNkVINwG3r6cuIYLyNnkkvinMoCK790C9TnwuTDLEZz0iiadrMzC5nWuC-ho3VJoT_FxVNXckCvC7SysZbru3x1mpNSp2KbIIXPKUS_eUQjrC-MtqrdVS7JA8-Yd0LfzO8TYFJLL2t0-A-4SHvQouNa0nW8G5Pg9UU-4XVMisvKbz.vdGb_8ZH9DoEnBasb7goQw
  • 省略した値のサンプル: eyJhbGciOiJkaXIiLCJlbmMiOiJBMTI

このJSON形式のデータを簡単に説明していくと、

キー名 説明
auth_req_id
認証を要求するリクエストを識別するための一意の識別子です。この値はCIBAの仕様上必須で返ってきます。
expires_in
auth_req_id の有効期限を秒単位で示す、正の整数値です。この値はCIBAの仕様上必須で返ってきます。
interval
クライアントがトークンエンドポイントへのポーリング要求を再度行うまでに待機しなければならない最小時間を、秒単位で示す正の整数値です。指定がない場合のデフォルト値はCIBAの仕様上は5秒です。この値はCIBAの仕様上オプショナルです。

auth_req_id は 後の ⑪ Token Request のリクエストに使いますので、任意の媒体にメモしておいてください。

より詳しい詳細はOpen ID Foundation の CIBAのドキュメントの 7.3. Successful Authentication Request Acknowledgement をご確認ください。

⑦ コマンドラインツールからプッシュ通知を呼び出し

プッシュ通知をiOSのシミュレーターで試す方法は下記の2つがありますが、今回は後者のパターンで試していきます。
  • apnsファイルをシミュレーターにドラッグ&ドロップ
  • コマンドラインツールで呼び出す
プッシュ通知をコマンドラインツールでシミュレーターに呼び出すコマンドのフォーマットは下記です。
xcrun simctl push <SIMULATOR_DEVICE_ID> <APP_BUNDLE_ID> <APNS_FILE_NAME>
各項目を簡単に説明すると、
名前
説明
備考
SIMULATOR_DEVICE_ID
シミュレーターのデバイスのIDです。
APP_BUNDLE_ID
アプリケーションを識別するためのIDです
apnsファイルに Simulator Target Bundle の情報を入れれば、この情報をコマンドから省略可能
APNS_FILE_NAME
apnsファイルのパスです

次にそれぞれの情報の確認方法ですが、シミュレーターのデバイスのIDは、GUIからも確認できますが、コマンドからもデバイスのIDのリストを取得できるので、 iphone SE2 2nd generation なら次のようなコマンドで調べることも可能です。

$ xcrun simctl list | grep 'iPhone SE (2nd generation)'

iPhone SE (2nd generation) (com.apple.CoreSimulator.SimDeviceType.iPhone-SE--2nd-generation-)
    iPhone SE (2nd generation) (61B58938-F168-4B43-A515-FF5D0D53315F) (Booted)

次に、Xcodeで作ったアプリケーションのバンドルIDの確認方法ですが、デフォルトから変更していない場合は Organization IdentifierとProduct Nameをつなげたものになります。例えば、Organization Identifierが com.cl-sample で Product Name が Test の場合は、com.cl-sample.Test となります。

変更している時は、下記の画像のようにXcode上からでも確認は可能です。

今回のコマンドのサンプルではtest.apnsの中身は ③プッシュ通知検証用のapnsファイルを作成 の手順で作成した下記であるものとします。
{
    "aps": {
        "alert": {
            "title": "ユーザーの購入の許可の確認について",
            "subtitle": "cl-taro さんの A Shop で購入の許可の申請",
            "body": "識別コードは hogefuga です。"
        },
        "sound": "default",
        "badge": 1,
        "category": "TEST_CATEGORY"
    },
    "PUSH_TEST_MESSAGE_ID": "0000000001",
    "AUTHORIZATION": "Bearer eyJhbGciOiJSUzI1NiIsInR5"
}

また、 APNS_FILE_NAME は /Users/cl-taro/ciba-test/apns/test.apns にあるものとした場合、コマンドは下記のようになります。

※ サンプル実装のアプリケーションをそのまま使用している場合は、APNS_FILE_NAME は 任意の場所に設置した ciba-test-keycloak リポジトリの中の /docker/authn-server/tmp/apns/test.apns になります。

$ xcrun simctl push 61B58938-F168-4B43-A515-FF5D0D53315F com.cl-sample.Test /Users/sample/ciba-test/tmp/test.apns

また、apnsに Simulator Target Bundle の情報を入れれば、APP_BUNDLE_ID の情報をコマンドから省略することもできます

apnsファイルを例示すると下記のような形になります。

{
    "Simulator Target Bundle": "com.cl-sample.Test",
    "aps": {
        "alert": {
            "title": "ユーザーの購入の許可の確認について",
            "subtitle": "cl-taro さんの A Shop で購入の許可の申請",
            "body": "識別コードは hogefuga です。"
        },
        "sound": "default",
        "badge": 1,
        "category": "TEST_CATEGORY"
    },
    "PUSH_TEST_MESSAGE_ID": "0000000001",
    "AUTHORIZATION": "Bearer eyJhbGciOiJSUzI1NiIsInR5"
}
上記のようにapnsファイルに Simulator Target Bundle の情報 を含んでいる場合は下記のコマンドで、呼び出し可能とです。

※ サンプル実装のアプリケーションをそのまま使用している場合は、APNS_FILE_NAME は 任意の場所に設置した ciba-test-keycloak リポジトリの中の /docker/authn-server/tmp/apns/test.apns になります。

$ xcrun simctl push 61B58938-F168-4B43-A515-FF5D0D53315F /Users/sample/ciba-test/tmp/test.apns

⑧ プッシュ通知の表示

⑦で行ったコマンドを実行すると、下記のように画面にプッシュ通知が表示されます。

ただし、プッシュ通知を受信する場合は事前にアプリの起動して、下記のようなプッシュ通知に対するアクセスを許可をしている必要があります。(下記は英語表記)

⑨ "許可" を選択

先程の通知だけではまだイメージがしづらいです。なので許可するかどうかのアクションを設けてみることとします。例えば、先程のプッシュ通知を下にドラッグしてみると下記のようなアクションが選択できるケースを考えます。

ではここで 許可 を押してみましょう。

⑩ Authentication Result Notification

プッシュ通知から 許可 が選択されたらシミュレーターから、認証及び、認可されたとして Keycloakに対しての認証結果の通知である Authentication Result Notification を送信してみます。

このリクエストのヘッダーは下記になります。

ヘッダー名 説明
Content-Type
application/json
Authorization
Bearer eyJhbGciOiJSUzI1NiIsInR5

※ 省略した値です

この値は③ Authentication Delegation Requestで送信されてきた Authorizationヘッダーと同じ値を使います
Bodyに含ませるJSONは下記になります。
キー名 説明
status
SUCCEED
必須。この値は SUCCEED (認証が正常に完了), UNAUTHORIZED (認証が完了していない), CANCELLED(認証がユーザーによってキャンセルされた) のいずれかである必要があります。

このリクエストは認証が正常に完了したということを伝えています。

また、Authorizationヘッダーが なぜ ② Authentication Request や ⑪ Token Request に用いる値と違うのか、と気になる方も一部居られるかもしれませんが、リクエスト元・リクエスト先・用途 を思い出してください。基本的なイメージとしては 誰が 何を識別・認証したいのか によって決めるものだと考えてください。
リクエスト元 リクエスト先 Authorizationヘッダーの用途
② Authentication Request クライアント
(今回は任意のコンソール)
Keycloak クライアント を認証するため
③ Authentication Delegation Request Keycloak Authentication entity Authentication entity を識別するため
⑩ Authentication Result Notification Authentication entity Keycloak Authentication entity を認証するため
⑪ Token Request クライアント
(今回は任意のコンソール)
Keycloak クライアント を認証するため

また、実際に運用を考慮すると直接Keycloakのエンドポイントを叩かせるわけには行かないケースが多いので、中間になんらかのサーバーの設置が必要があるかもしれません。

⑪ Token Request

このリクエストはトークンを要求するリクエストです。今回は poll モードで行う想定ですすめていきます。

また、このポーリングの処理は ① Authentication Request の レスポンス(②) にある、 interval のタイミング(デフォルトは5秒)より短い間隔でポーリングを行うとエラーが返ってくるため注意が必要です。

このリクエストのヘッダーは下記になります。

ヘッダー名 説明
Content-Type
application/x-www-form-urlencoded
Authorization
Basic dGVzdC1jbGllbnQ6OTk2MzM0YmEtYjc5Ni00ZDE0LWEzYmEtMDFlMzQyMDk1M2I4 client_secret_basic 形式時の渡し方です。①のリクエストで設定した Authorization ヘッダーの値と同じ値です。

Bodyに含ませる値は下記になります。

名前 説明
grant_type
urn:openid:params:grant-type:ciba
この値は必須で、urn:openid:params:grant-type:ciba である必要があります
auth_req_id
eyJhbGciOiJkaXIiLCJlbmMiOiJBMTI

※ 省略した値です

認証を要求するリクエストを識別するための一意の識別子です。具体値としては① Authentication Requestのレスポンスである⑥に含まれている auth_req_id の値です。この値は必須です。

サンプルの cURL文としては下記になります。

curl -i -X POST \
   -H "Content-Type:application/x-www-form-urlencoded" \
   -H "Authorization:Basic dGVzdC1jbGllbnQ6OTk2MzM0YmEtYjc5Ni00ZDE0LWEzYmEtMDFlMzQyMDk1M2I4" \
   -d "grant_type=urn:openid:params:grant-type:ciba" \
   -d "auth_req_id=eyJhbGciOiJkaXIiLCJlbmMiOiJBMTI" \
 'http://localhost:8088/auth/realms/SampleRealm/protocol/openid-connect/token'

より詳しい詳細はOpen ID Foundation の CIBAのドキュメントの 10.1. Token Request Using CIBA Grant Type をご確認ください。

⑫ Successful Token Response

認証が正常に完了している場合のトークンレスポンスを見てみましょう。

レスポンスはHTTPステータスコード200で application/json 形式で送られてきます。レスポンスを整形すると下記のようになります。このレスポンスでクライアントがアクセストークンやリフレッシュトークンを手に入れることができました。

{
    "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJVdVBUQUF6bGZZN1Y1c2Y3eExQeHVObmhYUTZXRUM0emRDZEM2eldUQWVvIn0.eyJleHAiOjE2MzIyMjAyNjUsImlhdCI6MTYzMjIxOTk2NSwiYXV0aF90aW1lIjoxNjMyMjE5OTY1LCJqdGkiOiIzYzYwMzg0OC0zYzdhLTRkYWEtOGU3Ny1mOWMxNWYxMzU5YzEiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgwODgvYXV0aC9yZWFsbXMvU2FtcGxlUmVhbG0iLCJhdWQiOiJhY2NvdW50Iiwic3ViIjoiY2NhYjIyMDEtZmVhYy00NDhiLWI0YTItYTdhZDZlOTFkMjY1IiwidHlwIjoiQmVhcmVyIiwiYXpwIjoidGVzdC1jbGllbnQiLCJzZXNzaW9uX3N0YXRlIjoiZTkxOGZjZTktZGQ5ZS00Njc5LWEyZWItNDkxNDE3OGFlNWQ2IiwiYWNyIjoiMSIsInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJvZmZsaW5lX2FjY2VzcyIsInVtYV9hdXRob3JpemF0aW9uIiwiZGVmYXVsdC1yb2xlcy1zYW1wbGVyZWFsbSJdfSwicmVzb3VyY2VfYWNjZXNzIjp7ImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoib3BlbmlkIGVtYWlsIHByb2ZpbGUiLCJzaWQiOiJlOTE4ZmNlOS1kZDllLTQ2NzktYTJlYi00OTE0MTc4YWU1ZDYiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsInByZWZlcnJlZF91c2VybmFtZSI6ImNsLXRhcm8ifQ.gEawYLrKBBE6FIQHQ-1E7SbbyhXfnCvt0qga5nUYOxmv25QcVx16rBXDKQLzo2T4OKwNbTglVrAv62bLK2471kCIOf-qocImdu02X4RZiy-l1trgcndrT6ElbWaTGG2QWWaCUJlm4lccCMY6y32k3CaPKMy18SEu6akYUf5-UzZNfNDTA90Ld32JTcUfiTc2gNWqXEOnSIU9SAGAGjjQNoOPGswkOuDsXybn6ux38nlUONQP62FLRBNtmvwZMzdGuP0IUMT7NDs5JybWRenAj9nT1YjlPSE_T46DHgoHpkEb4ines9DOgbc-4nDQ1vSp6FOfr2fXKFAYq7hm4aE1lA",
    "expires_in": 300,
    "refresh_expires_in": 1800,
    "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJiMzljMTkxZi1lNjhjLTQ4MDUtOTg1MC02YWRmMTdhYjAzODMifQ.eyJleHAiOjE2MzIyMjE3NjUsImlhdCI6MTYzMjIxOTk2NSwianRpIjoiZGFiMjFjYzEtNTlkYy00NWIxLTg4MWItZjRlMDVkMmNhMzBhIiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDg4L2F1dGgvcmVhbG1zL1NhbXBsZVJlYWxtIiwiYXVkIjoiaHR0cDovL2xvY2FsaG9zdDo4MDg4L2F1dGgvcmVhbG1zL1NhbXBsZVJlYWxtIiwic3ViIjoiY2NhYjIyMDEtZmVhYy00NDhiLWI0YTItYTdhZDZlOTFkMjY1IiwidHlwIjoiUmVmcmVzaCIsImF6cCI6InRlc3QtY2xpZW50Iiwic2Vzc2lvbl9zdGF0ZSI6ImU5MThmY2U5LWRkOWUtNDY3OS1hMmViLTQ5MTQxNzhhZTVkNiIsInNjb3BlIjoib3BlbmlkIGVtYWlsIHByb2ZpbGUiLCJzaWQiOiJlOTE4ZmNlOS1kZDllLTQ2NzktYTJlYi00OTE0MTc4YWU1ZDYifQ.ZR1dcVJzdrhje_rqm-X76_qCCqec4ekO7pd7PD2i-gA",
    "token_type": "Bearer",
    "id_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJVdVBUQUF6bGZZN1Y1c2Y3eExQeHVObmhYUTZXRUM0emRDZEM2eldUQWVvIn0.eyJleHAiOjE2MzIyMjAyNjUsImlhdCI6MTYzMjIxOTk2NSwiYXV0aF90aW1lIjoxNjMyMjE5OTY1LCJqdGkiOiJjNmEyYjllYS00NGYzLTQ5MWYtODgxZC0yOTk2NDg5ZDJhNWEiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgwODgvYXV0aC9yZWFsbXMvU2FtcGxlUmVhbG0iLCJhdWQiOiJ0ZXN0LWNsaWVudCIsInN1YiI6ImNjYWIyMjAxLWZlYWMtNDQ4Yi1iNGEyLWE3YWQ2ZTkxZDI2NSIsInR5cCI6IklEIiwiYXpwIjoidGVzdC1jbGllbnQiLCJzZXNzaW9uX3N0YXRlIjoiZTkxOGZjZTktZGQ5ZS00Njc5LWEyZWItNDkxNDE3OGFlNWQ2IiwiYXRfaGFzaCI6Ilc5Mjc5ZUNpY29rWDR4N0VzWWFmSlEiLCJhY3IiOiIxIiwic2lkIjoiZTkxOGZjZTktZGQ5ZS00Njc5LWEyZWItNDkxNDE3OGFlNWQ2IiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJjbC10YXJvIn0.gnn4M6XCyQ_sHHOCO2s-oQmNbj7Y_tKMeiAk2vIbRqmvMIbJmT-0ngdtfzY6SYOit5YbRnr3UEMpx1w4AKa5r7tmQbGlr5DPMnQomuUqdiMCVlwgvY5CVLn1A5BmRo8ytjwf6PQ6HQ9bU7YcrG7jQGI3EIxVZQ8POHLVjcvPVBVMxjYFlUrvANo3-Xu4X4qwVH9m9D5sk4XdFZ6RYETn_61z8prGyXf5Cfu8Gp-PdCiP7t2DXdVcXw1azMcqlDaE1sNveait7g3KUr8MtRzPpSOsScaWfKsiJoVlyiTN7ma2Qer8AkqM6CgdW10Vd_j_iRpNeN9U9Eu3UHBgmC0ytw",
    "not-before-policy": 0,
    "session_state": "e918fce9-dd9e-4679-a2eb-4914178ae5d6",
    "scope": "openid email profile"
}

より詳しい詳細はOpen ID Foundation の CIBAのドキュメントの 10.1.1. Successful Token Response をご確認ください。

9. 後書き


今回の記事では簡単にプッシュ通知を踏まえたCIBAの流れを見てみました。

iOSのプッシュ通知を含んだフローを考えるとAPNsの特性もあり、
Authentication entity 側でいくつかの配慮が必要そうな気がしました。

例えば下記などです。

  • iOSではプッシュ通知が許可されていないケースの場合にどうするか
    (Androidはデフォルトで有効であるため、許可画面はでません)
  • プッシュ通知の再送信(リトライなど)に関してはどうするか
    基本的には届くが、仕様としては配信が保証されているわけではない。
    >  The system makes every attempt to deliver local and remote notifications in a timely manner, but delivery isn’t guaranteed.
    参照: developer.apple.com User Notifications

この資料を読むことで、CIBA自身についてや、CIBAのプッシュ通知を交えた
フローのイメージを掴むことに対して少しでも参考になれば幸いです。

 

CL LAB Mail Magazine

CL LABの情報を逃さずチェックしよう!

メールアドレスを登録すると記事が投稿されるとメールで通知します。

メールアドレス: 登録

※登録後メールに記載しているリンクをクリックして認証してください。

Related post

新規CTA