KeycloakでDevice AuthZ Grantが実装されたので動かしてみた!
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 Protocol
はopenid-connect
のままでOK)。Client ID
はsample
としました。
作成するとそのままクライアントの詳細ページが開きます。ここでDevice Authorization Grantを有効にしていきます。今回はパブリッククライアントとして設定します。
Access Type
を一度confidential
にします。
そうすると、OAuth 2.0 Device Authorization Grant Enabled
のスイッチが表示されるので、ON
にします。また、デフォルトでON
になっていたStandard Flow Enabled
とDirect Access Grants Enabled
はOFF
にしておきます。
Access Type
を再度confidential
に戻して、最後にページ末にあるSave
をクリックして保存します。
最低限の設定としては以上になります。なお、関連の設定としてはRealm Settings
のTokens
タブを開くと、デバイスコードの有効期間とポーリングインターバル(秒)の設定が可能になっています。
また、これらの値はクライアント設定のAdvanced Settings
からクライアント単位に上書きすることもできます。
テスト用ユーザの登録
管理コンソールの画面左メニューのUsers
ページにアクセスしてAdd user
をクリックし、テスト用のユーザを追加します。
Save
クリック後、このアカウントでログインできるようにCredentials
タブにアクセスしてパスワードを設定しておきます。
OAuthクライアントの実装
OAuthクライアントは今回、Bashシェルスクリプトでさくっと実装しています。なのでWindowsな方はWSL2とかでやってみてください。curlコマンドとJSONパースに jqを使っているので必要に応じてインストールしておいてください。
以下、このOAuthクライアントの処理内容です。アクセストークンを取得するところまで実装しています。
- KeycloakのOpenID Provider Metadataを取得し、Device Authorization Grant用であるデバイス認可エンドポイントURLと、トークンエンドポイントURLを取得する。
- 取得したデバイス認可エンドポイントURLに対してデバイス認可リクエストを送信する。
- デバイス認可リクエストのレスポンスで得た
interval
を使って、一旦待つ。 - デバイス認可リクエストのレスポンスで得た
device_code
を使って、トークンエンドポイントに対してトークンリクエストを送信する。 - トークンリクエストのレスポンスがエラーの場合は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