📑

KeycloakでDevice AuthZ Grantが実装されたので動かしてみた!

2021/05/10に公開

2021/05/06にリリースされたKeycloak 13.0.0で、OAuth 2.0 Device Authorization Grant (RFC 8628) が実装されました🎉

自分が2年前にプロト実装してデザインドキュメントを出したものの長らく塩漬けにしてしまい、その後 Łukasz Dywicki 氏によるプルリクMichito Okai 氏によるプルリクを経てついにマージされました!以下は2年前のプロト実装の時のデモをTweetしたときのやつ。

あれから2年・・・ちょっと時間かかってしまいましたがこうやって引き継がれて組み込まれるのもOSSの良いところだなぁと思いました。

というわけで早速、動かし方を紹介しておきます。なお、Device Authorization Grant そのものについては今回特に解説していないので、プロトコルの詳細やそもそもこれって何のため?という話は以下の記事を参考にされるとよいでしょう。

Keycloakの起動

Dockerが使える人はさくっとDockerで起動するのが手っ取り早いです。環境変数で管理者アカウントの登録も同時にできます。

docker run --rm -p 8080:8080  \                          
  -e KEYCLOAK_ADMIN=admin \
  -e KEYCLOAK_ADMIN_PASSWORD=admin \
  quay.io/keycloak/keycloak:13.0.0

Dockerではなくてバイナリをインストールする場合は、KeycloakのDownloadページよりZIPまたはTAR.GZをダウンロードして任意の場所に解凍するだけです。また、Java 8 or 11が必要なので別途インストールしておいてください。ローカルで検証用に動かすだけであれば、Linuxの場合は (インストール先)/bin/standalone.sh を、Windowsの場合は (インストール先)/bin/standalone.bat を実行するだけですぐに起動します(組み込みDBが使われるでDBのセットアップも不要です)。なお、これで起動した場合は http://localhost:8080 にアクセスして、管理者アカウントを作成しておきます。

テスト用レルムの作成

http://localhost:8080 にアクセスしてAdministration Consoleのリンクから管理コンソールを開き、管理者アカウントでログインします。

管理コンソールに入れたら、まずはテスト用にtestレルムを作成しておきます。レルムのメニューからAdd realmをクリックして作成します。

OAuthクライント登録

管理コンソールの画面左メニューのClientsページにアクセスしてCreateをクリックし、新規クライアント登録を行います(OAuthの場合はClient Protocolopenid-connectのままでOK)。Client IDsampleとしました。

作成するとそのままクライアントの詳細ページが開きます。ここでDevice Authorization Grantを有効にしていきます。今回はパブリッククライアントとして設定します。

Access Typeを一度confidentialにします。

そうすると、OAuth 2.0 Device Authorization Grant Enabledのスイッチが表示されるので、ONにします。また、デフォルトでONになっていたStandard Flow EnabledDirect Access Grants EnabledOFFにしておきます。

Access Typeを再度confidentialに戻して、最後にページ末にあるSaveをクリックして保存します。

最低限の設定としては以上になります。なお、関連の設定としてはRealm SettingsTokensタブを開くと、デバイスコードの有効期間とポーリングインターバル(秒)の設定が可能になっています。

また、これらの値はクライアント設定のAdvanced Settingsからクライアント単位に上書きすることもできます。

テスト用ユーザの登録

管理コンソールの画面左メニューのUsersページにアクセスしてAdd userをクリックし、テスト用のユーザを追加します。

Saveクリック後、このアカウントでログインできるようにCredentialsタブにアクセスしてパスワードを設定しておきます。

OAuthクライアントの実装

OAuthクライアントは今回、Bashシェルスクリプトでさくっと実装しています。なのでWindowsな方はWSL2とかでやってみてください。curlコマンドとJSONパースに jqを使っているので必要に応じてインストールしておいてください。

以下、このOAuthクライアントの処理内容です。アクセストークンを取得するところまで実装しています。

  1. KeycloakのOpenID Provider Metadataを取得し、Device Authorization Grant用であるデバイス認可エンドポイントURLと、トークンエンドポイントURLを取得する。
  2. 取得したデバイス認可エンドポイントURLに対してデバイス認可リクエストを送信する。
  3. デバイス認可リクエストのレスポンスで得たintervalを使って、一旦待つ。
  4. デバイス認可リクエストのレスポンスで得たdevice_codeを使って、トークンエンドポイントに対してトークンリクエストを送信する。
  5. トークンリクエストのレスポンスがエラーの場合は3に戻る。成功(アクセストークンが取得できたら)の場合は終了。

そんなに行数ないので、以下にソースを全部載せておきます。

#!/bin/bash -eu

REALM=test
BASE_URL=http://localhost:8080/auth/realms/$REALM
CLIENT_ID=sample
SCOPE="profile"

CONFIGURATION_URL=$BASE_URL/.well-known/openid-configuration 

RES=`curl -s \
    $CONFIGURATION_URL`

DEVICE_AUTHZ_ENDPOINT=`echo $RES | jq -r .device_authorization_endpoint`
TOKEN_ENDPOINT=`echo $RES | jq -r .token_endpoint`


RES=`curl -s \
    -H "application/x-www-form-urlencoded" \
    -d "client_id=$CLIENT_ID" \
    -d "scope=$SCOPE" \
    $DEVICE_AUTHZ_ENDPOINT`

echo "========== DEVICE_AUTHZ_RESPONSE =========="
echo $RES | jq
echo "==========================================="

DEVICE_CODE=`echo $RES | jq -r .device_code`
INTERVAL=`echo $RES | jq -r .interval`

while true
do
  sleep $INTERVAL

  RES=`curl -s \
    -H "application/x-www-form-urlencoded" \
    -d "grant_type=urn:ietf:params:oauth:grant-type:device_code" \
    -d "device_code=$DEVICE_CODE" \
    -d "client_id=$CLIENT_ID" \
    $TOKEN_ENDPOINT`

  echo "========== TOKEN_RESPONSE =========="
  echo $RES | jq
  echo "===================================="

  ACCESS_TOKEN=`echo $RES | jq -r '.access_token | values'`

  if [ ! -z $ACCESS_TOKEN ]; then
    break
  fi
done

echo "========== ACCESS_TOKEN =========="
echo $ACCESS_TOKEN
echo "=================================="

実行

作成したシェルスクリプトを実行します。

./device-flow.sh

コンソールに以下のように最初にデバイス認可レスポンスが出力され、その後はintervalの時間(デフォルト5秒)スリープしてひたすらトークンリクエストのレスポンスを出力し続けます。まだアクセストークンは取得できないため、トークンリクエストのレスポンスはエラーが返り続けます。

========== DEVICE_AUTHZ_RESPONSE ==========
{
  "device_code": "47GS1V-hCld16yctvPabQBgHNPXvbH_-IcIOP0NoGKg",
  "user_code": "ZOXB-GOVM",
  "verification_uri": "http://localhost:8080/auth/realms/test/device",
  "verification_uri_complete": "http://localhost:8080/auth/realms/test/device?user_code=ZOXB-GOVM",
  "expires_in": 600,
  "interval": 5
}
===========================================
========== TOKEN_RESPONSE ==========
{
  "error": "authorization_pending",
  "error_description": "The authorization request is still pending"
}
====================================
========== TOKEN_RESPONSE ==========
{
  "error": "authorization_pending",
  "error_description": "The authorization request is still pending"
}
====================================

ここからブラウザを使って認証し、OAuthクライアントがアクセストークンを取得できるようにします。デバイス認可レスポンスに含まれるverification_uri_completeのURLをブラウザで開きます。最初にログイン画面が表示されるので作成しておいたテスト用アカウントでログインします。

sampleデバイスに対して、リソースオーナのアクセス権限を渡してよいか同意を求められます。

Yesをクリックすると、Device Login Successfulのページが表示されてブラウザ側の操作は終わりです。

シェルスクリプト実行側のコンソールを見てみると、無事にアクセスートークンがとれています。

========== TOKEN_RESPONSE ==========
{
  "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJILXJfN3plbUF1ZjVqbmNYWm50QTVqY0tMbmphaEpFaGktYVdCM05jWkVBIn0.eyJleHAiOjE2MjA1NzE5MTQsImlhdCI6MTYyMDU3MTYxNCwiYXV0aF90aW1lIjoxNjIwNTcxNjA5LCJqdGkiOiIxMmE5OWU0Zi0xZTlhLTQ4M2ItYWU5YS0wN2ZiNzViNWRmNTAiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgwODAvYXV0aC9yZWFsbXMvdGVzdCIsImF1ZCI6ImFjY291bnQiLCJzdWIiOiJiNTkxMTg1OS0zNWIzLTQzMDQtYWQ3Zi01ZTNmZjZlMjY0ZmUiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJzYW1wbGUiLCJzZXNzaW9uX3N0YXRlIjoiNzU4ODZjMzEtOTA2YS00MTFhLWJiYzctMGE5NmI4ZDRjZWIwIiwiYWNyIjoiMSIsInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJkZWZhdWx0LXJvbGVzLXRlc3QiLCJvZmZsaW5lX2FjY2VzcyIsInVtYV9hdXRob3JpemF0aW9uIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19fSwic2NvcGUiOiJwcm9maWxlIGVtYWlsIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJuYW1lIjoiVGVzdCBUZXN0IiwicHJlZmVycmVkX3VzZXJuYW1lIjoidGVzdCIsImdpdmVuX25hbWUiOiJUZXN0IiwiZmFtaWx5X25hbWUiOiJUZXN0IiwiZW1haWwiOiJ0ZXN0QGV4YW1wbGUuY29tIn0.QMAgTRs1YKUr5ZPkTy6wfy-JtOyO294RRvEvRECYJyZrfOv4ClDx5P5ILhLiPSgiCUFi7O1qau8GR67g6v1y96PTcgoWblYMfpdZj4gymvMbA7J1xPQiiJYj1QFZN8fi04_0_qP4hKawIXwIrgquZCTNysGzUGg3-lgleEItJ7WUas5ShSKnJlQLMtI3d5bS7mIBqzCixQ7vAG3EibUXf-f-ZD3Ie76hLEx4_HwGPe-p6ZMIt6Fre52L_2fOqcvTGindtMBokCU6_6oT_dxXcC-hqg8i6Lpc5MhJSbUbiYpi_1xv7kZozynrjsbBaqgv6iJdzgtEYURyUEvZjTBejA",
  "expires_in": 300,
  "refresh_expires_in": 1800,
  "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICIxOTM4ZWFhYS0yZWVjLTQxZjctYjU2NS1jMDQ3YjNmZDA2ZTAifQ.eyJleHAiOjE2MjA1NzM0MTQsImlhdCI6MTYyMDU3MTYxNCwianRpIjoiYjdkNDBjY2YtNGM4Zi00NjYyLTg1OGQtYWE0ZGY4ZTY5MDY2IiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwL2F1dGgvcmVhbG1zL3Rlc3QiLCJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjgwODAvYXV0aC9yZWFsbXMvdGVzdCIsInN1YiI6ImI1OTExODU5LTM1YjMtNDMwNC1hZDdmLTVlM2ZmNmUyNjRmZSIsInR5cCI6IlJlZnJlc2giLCJhenAiOiJzYW1wbGUiLCJzZXNzaW9uX3N0YXRlIjoiNzU4ODZjMzEtOTA2YS00MTFhLWJiYzctMGE5NmI4ZDRjZWIwIiwic2NvcGUiOiJwcm9maWxlIGVtYWlsIn0.zmvk1TJMRl5Vu5MZuy6qVlocrvVqofPcdAx0nAshIAk",
  "token_type": "Bearer",
  "not-before-policy": 0,
  "session_state": "75886c31-906a-411a-bbc7-0a96b8d4ceb0",
  "scope": "profile email"
}
====================================
========== ACCESS_TOKEN ==========
eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJILXJfN3plbUF1ZjVqbmNYWm50QTVqY0tMbmphaEpFaGktYVdCM05jWkVBIn0.eyJleHAiOjE2MjA1NzE5MTQsImlhdCI6MTYyMDU3MTYxNCwiYXV0aF90aW1lIjoxNjIwNTcxNjA5LCJqdGkiOiIxMmE5OWU0Zi0xZTlhLTQ4M2ItYWU5YS0wN2ZiNzViNWRmNTAiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgwODAvYXV0aC9yZWFsbXMvdGVzdCIsImF1ZCI6ImFjY291bnQiLCJzdWIiOiJiNTkxMTg1OS0zNWIzLTQzMDQtYWQ3Zi01ZTNmZjZlMjY0ZmUiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJzYW1wbGUiLCJzZXNzaW9uX3N0YXRlIjoiNzU4ODZjMzEtOTA2YS00MTFhLWJiYzctMGE5NmI4ZDRjZWIwIiwiYWNyIjoiMSIsInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJkZWZhdWx0LXJvbGVzLXRlc3QiLCJvZmZsaW5lX2FjY2VzcyIsInVtYV9hdXRob3JpemF0aW9uIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19fSwic2NvcGUiOiJwcm9maWxlIGVtYWlsIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJuYW1lIjoiVGVzdCBUZXN0IiwicHJlZmVycmVkX3VzZXJuYW1lIjoidGVzdCIsImdpdmVuX25hbWUiOiJUZXN0IiwiZmFtaWx5X25hbWUiOiJUZXN0IiwiZW1haWwiOiJ0ZXN0QGV4YW1wbGUuY29tIn0.QMAgTRs1YKUr5ZPkTy6wfy-JtOyO294RRvEvRECYJyZrfOv4ClDx5P5ILhLiPSgiCUFi7O1qau8GR67g6v1y96PTcgoWblYMfpdZj4gymvMbA7J1xPQiiJYj1QFZN8fi04_0_qP4hKawIXwIrgquZCTNysGzUGg3-lgleEItJ7WUas5ShSKnJlQLMtI3d5bS7mIBqzCixQ7vAG3EibUXf-f-ZD3Ie76hLEx4_HwGPe-p6ZMIt6Fre52L_2fOqcvTGindtMBokCU6_6oT_dxXcC-hqg8i6Lpc5MhJSbUbiYpi_1xv7kZozynrjsbBaqgv6iJdzgtEYURyUEvZjTBejA
==================================

おまけ: IDトークンの取得

KeycloakのDevice Authorization Grantの実装では、他ベンダーが実装しているようにIDトークンの発行もサポートしています。ついでにこれも試してみましょう。変更するのは作成したシェルスクリプトのSCOPE変数に、openidを追加して要求スコープに追加するだけです。

#!/bin/bash -eu

REALM=test
BASE_URL=http://localhost:8080/auth/realms/$REALM
CLIENT_ID=sample
SCOPE="profile openid"

これでシェルスクリプトを実行してまたブラウザからアクセスを行い同意を行うと、以下のようにトークンレスポンスにid_tokenが含まれるようになります。

========== TOKEN_RESPONSE ==========
{
  "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJILXJfN3plbUF1ZjVqbmNYWm50QTVqY0tMbmphaEpFaGktYVdCM05jWkVBIn0.eyJleHAiOjE2MjA1NzI2NTgsImlhdCI6MTYyMDU3MjM1OCwiYXV0aF90aW1lIjoxNjIwNTcyMzU2LCJqdGkiOiJjOTJlMzdhNy1lMjMxLTQwNTQtOTAyNC0zYzk4NTUwY2YwZDkiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgwODAvYXV0aC9yZWFsbXMvdGVzdCIsImF1ZCI6ImFjY291bnQiLCJzdWIiOiJiNTkxMTg1OS0zNWIzLTQzMDQtYWQ3Zi01ZTNmZjZlMjY0ZmUiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJzYW1wbGUiLCJzZXNzaW9uX3N0YXRlIjoiNzU4ODZjMzEtOTA2YS00MTFhLWJiYzctMGE5NmI4ZDRjZWIwIiwiYWNyIjoiMSIsInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJkZWZhdWx0LXJvbGVzLXRlc3QiLCJvZmZsaW5lX2FjY2VzcyIsInVtYV9hdXRob3JpemF0aW9uIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19fSwic2NvcGUiOiJwcm9maWxlIGVtYWlsIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJuYW1lIjoiVGVzdCBUZXN0IiwicHJlZmVycmVkX3VzZXJuYW1lIjoidGVzdCIsImdpdmVuX25hbWUiOiJUZXN0IiwiZmFtaWx5X25hbWUiOiJUZXN0IiwiZW1haWwiOiJ0ZXN0QGV4YW1wbGUuY29tIn0.Ls4n8xeZ4Gzr-qB1xGRH6RKLkpgDMZUUtH-u60x4aMQYWkEv0IBxQq0cTm2ajeFFsUdoUxHvvTpX4ZjU-lNer4APRfqkeGutoF75qIlNT3G6ZNQQAkY2vFHd8XG4H_Pud6NOEOG4zwomlyqWU808f09i2Ez8rsW738dTzbeqxwssfFbjyj2J3tvMJJmu6EYgrwCeludoAeiCBbxjKP343AY3-x1U35Sgj8BYTJpbn5E6WiPEhtzaB7Vsg1HQVSIzi0LIGm2yWF7dCGjaWlh-U_qAmdPKwF_VDsMFYs34hlllIErjOEmH3VN916gwCghf3VO1f5nvHTT3Lgetml2UYw",
  "expires_in": 300,
  "refresh_expires_in": 1800,
  "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICIxOTM4ZWFhYS0yZWVjLTQxZjctYjU2NS1jMDQ3YjNmZDA2ZTAifQ.eyJleHAiOjE2MjA1NzQxNTgsImlhdCI6MTYyMDU3MjM1OCwianRpIjoiOGNlNGFlYjgtYTZlZS00ZGJiLThiMzMtY2QwNDIxNmRiOTRkIiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwL2F1dGgvcmVhbG1zL3Rlc3QiLCJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjgwODAvYXV0aC9yZWFsbXMvdGVzdCIsInN1YiI6ImI1OTExODU5LTM1YjMtNDMwNC1hZDdmLTVlM2ZmNmUyNjRmZSIsInR5cCI6IlJlZnJlc2giLCJhenAiOiJzYW1wbGUiLCJzZXNzaW9uX3N0YXRlIjoiNzU4ODZjMzEtOTA2YS00MTFhLWJiYzctMGE5NmI4ZDRjZWIwIiwic2NvcGUiOiJwcm9maWxlIGVtYWlsIn0._TIKSIeHB2O7-cak0NCzQ9GWyOL5ZnGaAz0u-U5v6xY",
  "token_type": "Bearer",
  "id_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJILXJfN3plbUF1ZjVqbmNYWm50QTVqY0tMbmphaEpFaGktYVdCM05jWkVBIn0.eyJleHAiOjE2MjA1NzI2NTgsImlhdCI6MTYyMDU3MjM1OCwiYXV0aF90aW1lIjoxNjIwNTcyMzU2LCJqdGkiOiI3ZTJhZGFmNS01ZTU3LTRkNjAtYWM1Ny03NTc5ODNmM2FkNmIiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgwODAvYXV0aC9yZWFsbXMvdGVzdCIsImF1ZCI6InNhbXBsZSIsInN1YiI6ImI1OTExODU5LTM1YjMtNDMwNC1hZDdmLTVlM2ZmNmUyNjRmZSIsInR5cCI6IklEIiwiYXpwIjoic2FtcGxlIiwic2Vzc2lvbl9zdGF0ZSI6Ijc1ODg2YzMxLTkwNmEtNDExYS1iYmM3LTBhOTZiOGQ0Y2ViMCIsImF0X2hhc2giOiJhV0RfWFdsUUNRTmp2V2VPN0s2aHlRIiwiYWNyIjoiMSIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwibmFtZSI6IlRlc3QgVGVzdCIsInByZWZlcnJlZF91c2VybmFtZSI6InRlc3QiLCJnaXZlbl9uYW1lIjoiVGVzdCIsImZhbWlseV9uYW1lIjoiVGVzdCIsImVtYWlsIjoidGVzdEBleGFtcGxlLmNvbSJ9.HPE150JnASfZzQb3NQHsVK07TzB-TTqp5cCT86NONeFj2kneunWbxf9vokrzQ7Bg7K0Dy-N8nwOYzm-p40OuO0p91hAmzmHKBZ9OFP3nLteOx1LhCsGCIlQZ6sWfztxqN946B9U36s1oMNej6ozrcUJmuKv0v6Fmh-T-F6-nVxiVCXff7c8nknYVlm1DpFCT1rVXePd3ltZoifkO6rzdwqh-d5lt8QqIrQGo3BqK_niPVM1ZyWgMSsxWGhRtTiJPS2YyZekC7wWA2Kxv2wpnL6vKkA_EmdBw6n2Nd1CbvG3DSxuQcmoDSkGJnFCRcFo4tvBMUq46VOfKvT8Qkhuz6A",
  "not-before-policy": 0,
  "session_state": "75886c31-906a-411a-bbc7-0a96b8d4ceb0",
  "scope": "profile email"
}

おわりに

というわけでKeycloak 13.0.0でついに実装されたDevice Authorization Grantの使い方を簡単に紹介しました。13.0.0ではこの他にもCIBA (Client Initiated Backchannel Authentication) も実装されているので、是非ためしてみてください!

Discussion