今日やること
Keycloakアドベント 19日目は暗黙フロー(Implicit Flow)、Resource Owner Password Credentials、Client Credentialsを欲張ってやります。
暗黙フロー(Implicit Flow)編
暗黙フロー(Implicit Flow)とは何か?
認可コードフローとは何が違うの?
- OpenID Provider(OP)はIDトークンとアクセストークンを直接発行する
認可コードフローでは、認可コードをRelying Partyに渡して、認可コードと引き換えにトークンをもらう。暗黙フロー(Implicit Flow)では直接トークンを発行する。
- Implicit Flow はネイティブアプリやスクリプト言語を用いて実装されブラウザ上で動作するクライアントによって使用される
サーバーサイドへ渡すことを想定していないため、トークンはハッシュフラグメント(URLの#より後ろの部分)につく。
- リフレッシュトークンが発行されない
そういう仕様。
事前準備
- Keycloakは、2日目の記事などを参考にしてインストールしておいてください
- Relying Partyとテストユーザーは前日の認可コードフローを試してみると同じものを使います
環境
認可コードフローのときと同じです。
- Relying Party(RP)の環境
項目 | 値 |
---|---|
OS | CentOS 7.4 |
URL | https://172.26.22.25 |
クライアントID (client_id) | apache24 |
リダイレクトURI | https://172.26.22.25/private/callback |
- OpenID Provider(OP)の環境
項目 | 値 |
---|---|
URL | https://172.26.22.5 |
エンドポイント | https://172.26.22.5/auth/realms/master/.well-known/openid-configuration |
設定する
Keycloakに設定する
認可コードフローのときに作ったクライアント設定を使いまわします。「クライアント」から「apache24」を選択します。
「Implicit Flowの有効」があるので、これを有効して保存します。ちなみにすぐ上にある「Standard Flow」とは認可コードフローのことです。
mod_auth_openidc(Apache)を設定する
認可コードフローのときとほとんど同じですが、response_typeを変えて暗黙フロー(Implicit Flow)を要求するようにします。
LoadModule auth_openidc_module modules/mod_auth_openidc.so
OIDCProviderMetadataURL https://172.26.22.5/auth/realms/master/.well-known/openid-configuration
OIDCClientID apache24
OIDCClientSecret f6757521-f4e9-4f5e-9ed1-0bf2a6d06cfe
OIDCResponseType "token id_token"
OIDCScope "openid"
OIDCSSLValidateServer Off
OIDCProviderTokenEndpointAuth client_secret_basic
OIDCRedirectURI https://172.26.22.25/private/callback
OIDCCryptoPassphrase passphrase
OIDCPreservePost On
<Location /private>
AuthType openid-connect
Require valid-user
</Location>
<Location /public>
OIDCUnAuthAction pass
AuthType openid-connect
Require valid-user
</Location>
パラメータ名 | 説明 |
---|---|
OIDCProviderMetadataURL | OpenID Provider(OP)の.well-knownのURL |
OIDCClientID | クライアントID |
OIDCClientSecret | クライアントシークレット。さっきメモっておいたやつ。 |
OIDCResponseType | response_typeの値。暗黙フロー(Implicit Flow)なので token id_token を指定する。 |
OIDCScope | スコープ。OpenID Connectを使いたいのでopenidを指定する。 |
OIDCSSLValidateServer | OpenID Provider(OP)にアクセスするとき、SSL証明書を検証するか? |
OIDCProviderTokenEndpointAuth | クライアント認証のやり方。クライアントID&シークレットを指定。 |
OIDCRedirectURI | リダイレクトURI |
OIDCCryptoPassphrase | なんだこれは? 多分IDトークンの暗号化かな |
OIDCPreservePost | POSTのときリクエストパラメータを保存しておくか? |
やってみる
これで設定は全部完了です。OpenID Connectでシングルサインオンしましょう。Relying Party (https://172.26.22.25/private/headers.pl) にアクセスすると、認可コードフローのときと同じように勝手に進みます。
できました! が、認可コードフローと見た目が全く同じで違いが分からない…
通信内容を見てみる
Fiddlerを使ってブラウザが行っている通信を見てみましょう。
- 認証・認可リクエスト
リクエスト
GET http://172.26.22.5/auth/realms/master/protocol/openid-connect/auth?response_type=token%20id_token&scope=openid&client_id=apache24&state=W7xGhp8yQm5bdo9rT-Vy1aqf-oE&redirect_uri=https%3A%2F%2F172.26.22.25%2Fprivate%2Fcallback&nonce=6lDXsu08pmbHtlaNyMFqCoGoYADtDGF5klosXKB6ywU HTTP/1.1
Host: 172.26.22.5
User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:51.0) Gecko/20100101 Firefox/51.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: ja,en-US;q=0.7,en;q=0.3
Accept-Encoding: gzip, deflate
Connection: keep-alive
Upgrade-Insecure-Requests: 1
response_type=token%20id_token
になっているので、ちゃんと暗黙フロー(Implicit Flow)の要求になっています。
- ログインID&パスワードをKeycloakに送信
リクエスト
POST http://172.26.22.5/auth/realms/master/login-actions/authenticate?code=Kq9Fq6bKC4w1fgtO03HKOIBEue6I_wz7Z-NJtuMKGGs&execution=c20d26d6-424e-46ba-8269-70c5e3666546&client_id=apache24 HTTP/1.1
Host: 172.26.22.5
User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:51.0) Gecko/20100101 Firefox/51.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: ja,en-US;q=0.7,en;q=0.3
Accept-Encoding: gzip, deflate
Referer: http://172.26.22.5/auth/realms/master/protocol/openid-connect/auth?response_type=token%20id_token&scope=openid&client_id=apache24&state=W7xGhp8yQm5bdo9rT-Vy1aqf-oE&redirect_uri=https%3A%2F%2F172.26.22.25%2Fprivate%2Fcallback&nonce=6lDXsu08pmbHtlaNyMFqCoGoYADtDGF5klosXKB6ywU
Cookie: AUTH_SESSION_ID=1faf6106-5a5b-415c-a344-685c14ebd65d.ip-172-26-22-5; KC_RESTART=eyJhbGciOiJIUzI1NiIsImtpZCIgOiAiZTQyZmYxNzYtMTFmMS00YmY3LTg4YTItYmM3NTk2OTQ1ZDIzIn0.eyJjaWQiOiJhcGFjaGUyNCIsInB0eSI6Im9wZW5pZC1jb25uZWN0IiwicnVyaSI6Imh0dHBzOi8vMTcyLjI2LjIyLjI1L3ByaXZhdGUvY2FsbGJhY2siLCJhY3QiOiJBVVRIRU5USUNBVEUiLCJub3RlcyI6eyJzY29wZSI6Im9wZW5pZCIsImlzcyI6Imh0dHA6Ly8xNzIuMjYuMjIuNS9hdXRoL3JlYWxtcy9tYXN0ZXIiLCJyZXNwb25zZV90eXBlIjoidG9rZW4gaWRfdG9rZW4iLCJjb2RlX2NoYWxsZW5nZV9tZXRob2QiOiJwbGFpbiIsInJlZGlyZWN0X3VyaSI6Imh0dHBzOi8vMTcyLjI2LjIyLjI1L3ByaXZhdGUvY2FsbGJhY2siLCJzdGF0ZSI6Ilc3eEdocDh5UW01YmRvOXJULVZ5MWFxZi1vRSIsIm5vbmNlIjoiNmxEWHN1MDhwbWJIdGxhTnlNRnFDb0dvWUFEdERHRjVrbG9zWEtCNnl3VSJ9fQ.U6vTS71SFYlxzgX6RCQZbS0hpJfwkviVnYfDs1quUv0
Connection: keep-alive
Upgrade-Insecure-Requests: 1
Content-Type: application/x-www-form-urlencoded
Content-Length: 74
username=u111&password=password&login=%E3%83%AD%E3%82%B0%E3%82%A4%E3%83%B3
レスポンス
HTTP/1.1 302 Found
Date: Mon, 30 Oct 2017 06:35:22 GMT
Server: WildFly/11
Cache-Control: no-store, must-revalidate, max-age=0
X-Powered-By: Undertow/1
P3P: CP="This is not a P3P policy!"
Location: https://172.26.22.25/private/callback#state=W7xGhp8yQm5bdo9rT-Vy1aqf-oE&id_token=eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJWa1hGdXFRYldjUnJzbEduZGwzYWtnMkxsdjk0dzlZWHl0aW1qM24tX25JIn0.eyJqdGkiOiJmYTkwOGJhMS1kNjdiLTRjYWItOTQ3MS03OTQwZTMyMWM4YTQiLCJleHAiOjE1MDkzNDYyMjIsIm5iZiI6MCwiaWF0IjoxNTA5MzQ1MzIyLCJpc3MiOiJodHRwOi8vMTcyLjI2LjIyLjUvYXV0aC9yZWFsbXMvbWFzdGVyIiwiYXVkIjoiYXBhY2hlMjQiLCJzdWIiOiI5YjUzNTIwMC1lZDRjLTQ0NjEtYjM4My1jMTI5ZmY1NzUxMDAiLCJ0eXAiOiJJRCIsImF6cCI6ImFwYWNoZTI0Iiwibm9uY2UiOiI2bERYc3UwOHBtYkh0bGFOeU1GcUNvR29ZQUR0REdGNWtsb3NYS0I2eXdVIiwiYXV0aF90aW1lIjoxNTA5MzQ1MzIyLCJzZXNzaW9uX3N0YXRlIjoiMWZhZjYxMDYtNWE1Yi00MTVjLWEzNDQtNjg1YzE0ZWJkNjVkIiwiYXRfaGFzaCI6IkctWmRzaUlPN3VTSndCZ3V0ckstamciLCJhY3IiOiIxIiwicHJlZmVycmVkX3VzZXJuYW1lIjoidTExMSIsImdpdmVuX25hbWUiOiJ1MTExIiwiZmFtaWx5X25hbWUiOiJ0ZXN0In0.AFaplC4Hlc0gp604ml53nedUMwY38TpkmVcnSL9bgJpug-FW2Zk9RAEscufmQysBlG8_1P75hPe8RD_v8NoJBVOu40iUSliFvYf_bbIxnTchBGnjvD2hRp51F5enRcGwTtv8eYBw3AKvS4jTRLd8gp1pMxe1KmILTYE-kpx9kZms4r0XA29qnapIiuTzHXyqMoFOraaKV1BL2Q6T1Gj2WaVA4FMnRlR8IuBZeDF89JTxyVge7PsIhilNZyRko9JDmKa8ldpAH9A57XX7CO7M_T61bWS-2gg-42Ob9xItRu1ZJoJBndsYulZi0J4qDibG8K29wdUqGNdZk4nmqtqc3A&access_token=eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJWa1hGdXFRYldjUnJzbEduZGwzYWtnMkxsdjk0dzlZWHl0aW1qM24tX25JIn0.eyJqdGkiOiIyOWQxYjhlZC00ZmQ5LTQ1NjgtYmUzZC0xNTBlYmVkYWM4NWUiLCJleHAiOjE1MDkzNDYyMjIsIm5iZiI6MCwiaWF0IjoxNTA5MzQ1MzIyLCJpc3MiOiJodHRwOi8vMTcyLjI2LjIyLjUvYXV0aC9yZWFsbXMvbWFzdGVyIiwiYXVkIjoiYXBhY2hlMjQiLCJzdWIiOiI5YjUzNTIwMC1lZDRjLTQ0NjEtYjM4My1jMTI5ZmY1NzUxMDAiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJhcGFjaGUyNCIsIm5vbmNlIjoiNmxEWHN1MDhwbWJIdGxhTnlNRnFDb0dvWUFEdERHRjVrbG9zWEtCNnl3VSIsImF1dGhfdGltZSI6MTUwOTM0NTMyMiwic2Vzc2lvbl9zdGF0ZSI6IjFmYWY2MTA2LTVhNWItNDE1Yy1hMzQ0LTY4NWMxNGViZDY1ZCIsImFjciI6IjEiLCJhbGxvd2VkLW9yaWdpbnMiOltdLCJyZXNvdXJjZV9hY2Nlc3MiOnt9LCJuYW1lIjoidTExMSB0ZXN0IiwicHJlZmVycmVkX3VzZXJuYW1lIjoidTExMSIsImdpdmVuX25hbWUiOiJ1MTExIiwiZmFtaWx5X25hbWUiOiJ0ZXN0In0.Jex4NF2MLUXaGjnQ3vqUPKI1Iyndhc-toHA6hh88l9MdUriHTSq9bKyKHMDeaeBekIMOGCcO4VdmECPcedztGwR0JOf39aa5vVpPL9S16t5ISx4WEibbjzwhYpxMaypLjszScXCi8jD_P-BTUeq9GrIiRi1EXnoA2PoVxUYx-ZFm96ddy2FjrzTSDS3LiL_KmaRlWcdHDVB1pPXeR8qH1VuFCKqqlXubhSnX0VA8Mhv-O_j7eCrIfAlKKfV6Fa0659zdesl46YohSNpFg7qxR_K6LCRaT8LNwr4Rz0VDVjQd1Hn0DdrWcro5We23GpHJ1sIOi3RdfoFeb7pGv7aeAQ&token_type=bearer&session_state=1faf6106-5a5b-415c-a344-685c14ebd65d&expires_in=900¬-before-policy=0
Content-Length: 0
Set-Cookie: KC_RESTART=; Version=1; Expires=Thu, 01-Jan-1970 00:00:10 GMT; Max-Age=0; Path=/auth/realms/master; HttpOnly
Set-Cookie: KEYCLOAK_IDENTITY=eyJhbGciOiJIUzI1NiIsImtpZCIgOiAiZTQyZmYxNzYtMTFmMS00YmY3LTg4YTItYmM3NTk2OTQ1ZDIzIn0.eyJqdGkiOiJlMjk3NTg1Ni0zMzRhLTRhOTItOGRjZi01NTU1ZDIwZTUwMDYiLCJleHAiOjE1MDkzODEzMjIsIm5iZiI6MCwiaWF0IjoxNTA5MzQ1MzIyLCJpc3MiOiJodHRwOi8vMTcyLjI2LjIyLjUvYXV0aC9yZWFsbXMvbWFzdGVyIiwic3ViIjoiOWI1MzUyMDAtZWQ0Yy00NDYxLWIzODMtYzEyOWZmNTc1MTAwIiwiYXV0aF90aW1lIjowLCJzZXNzaW9uX3N0YXRlIjoiMWZhZjYxMDYtNWE1Yi00MTVjLWEzNDQtNjg1YzE0ZWJkNjVkIiwicmVzb3VyY2VfYWNjZXNzIjp7fX0.1lVcKjpOgDq-j1a3d0_OsyRPVYIAL2zZ_nnIXCjEGjU; Version=1; Path=/auth/realms/master; HttpOnly
Set-Cookie: KEYCLOAK_SESSION=master/9b535200-ed4c-4461-b383-c129ff575100/1faf6106-5a5b-415c-a344-685c14ebd65d; Version=1; Expires=Mon, 30-Oct-2017 16:35:22 GMT; Max-Age=36000; Path=/auth/realms/master
Set-Cookie: KEYCLOAK_REMEMBER_ME=; Version=1; Comment=Expiring cookie; Expires=Thu, 01-Jan-1970 00:00:10 GMT; Max-Age=0; Path=/auth/realms/master; HttpOnly
Keep-Alive: timeout=5, max=96
Connection: Keep-Alive
レスポンスヘッダ:Location
が https://172.26.22.25/private/callback#state=... となっているので、ちゃんとハッシュフラグメントで返って来ていました。
- トークンをRelying Partyに送信
リクエスト
GET https://172.26.22.25/private/callback HTTP/1.1
Host: 172.26.22.25
User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:51.0) Gecko/20100101 Firefox/51.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: ja,en-US;q=0.7,en;q=0.3
Accept-Encoding: gzip, deflate, br
Referer: http://172.26.22.5/auth/realms/master/protocol/openid-connect/auth?response_type=token%20id_token&scope=openid&client_id=apache24&state=W7xGhp8yQm5bdo9rT-Vy1aqf-oE&redirect_uri=https%3A%2F%2F172.26.22.25%2Fprivate%2Fcallback&nonce=6lDXsu08pmbHtlaNyMFqCoGoYADtDGF5klosXKB6ywU
Cookie: mod_auth_openidc_state_W7xGhp8yQm5bdo9rT-Vy1aqf-oE=eyJhbGciOiAiZGlyIiwgImVuYyI6ICJBMjU2R0NNIn0..1NQsc2d6jHSbkP0f.Xyq2f_UPsjNWbDu9a5wH0GkW2Hm8DGdBjh4GKaM_hkz00qMDGLZuC1awYlNsuCacgMsmGEh4FsnII5Zd7vSk1bOCZWex-81Cber7VMU3OwcBiHv6PlogLa3nMYT6ORDsYxOPoH_nU-6zUZVRDBvMmthDh_dUzyOKB5DEiAePhVgitdi0wxaLjWN-LGOnu-DEFlXhltV6YnG7iijETDq4hExcKc0SJYIq2NOGHv6K-CraJ_sULf6rbSgNifamX-psQ6PlsYdAcjw2j5nVZuQqGR5yJaWEDQ4dzNNMSPayRAFjFkeIvRh5CqOOnM2d66jBMmdSM-9nZOOMxKhI3xAaYztxAcvWFl1JgnR8tlhtGvvNnvcy-yH9u3iQVdT0CSvxPZ0NxB906eoahxV0gRl6o4N_DOZ8tNR3hLc-OosZU-2Z.ESias1z8gJ9APO6PvIbVCA
Connection: keep-alive
Upgrade-Insecure-Requests: 1
レスポンス
HTTP/1.1 200 OK
Date: Mon, 30 Oct 2017 06:35:22 GMT
Server: Apache/2.4.25 (Unix) OpenSSL/1.0.2k-fips
Content-Length: 1141
Keep-Alive: timeout=5, max=99
Connection: Keep-Alive
Content-Type: text/html
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Submitting...</title>
<script type="text/javascript">
function postOnLoad() {
encoded = location.hash.substring(1).split('&');
for (i = 0; i < encoded.length; i++) {
encoded[i].replace(/\+/g, ' ');
var n = encoded[i].indexOf('=');
var input = document.createElement('input');
input.type = 'hidden';
input.name = decodeURIComponent(encoded[i].substring(0, n));
input.value = decodeURIComponent(encoded[i].substring(n+1));
document.forms[0].appendChild(input);
}
document.forms[0].action = window.location.href.substr(0, window.location.href.indexOf('#'));
document.forms[0].submit();
}
</script>
</head>
<body onload="postOnLoad()">
<p>Submitting...</p>
<form method="post" action="">
<p>
<input type="hidden" name="response_mode" value="fragment">
</p>
</form>
</body>
</html>
mod_auth_openidc
は、トークンを送信するHTMLを生成して返していました(このHTMLを返すCGIや何かを配置している訳ではありません)。もしRelying Partyを自分で作る場合は、jQuery(Ajax)などを使ってトークンを送信するようにする必要があります。
Resource Owner Password Credentials編
Resource Owner Password Credentialsとは?
続いて、Resource Owner Password Credentialsです。Resource Owner(=エンドユーザー)のユーザー名&パスワードで認証しながらトークンも発行する方法です。多分試してみたのを見たほうが早いでしょう。
環境
認可コードフローのときと同じです。
項目 | 値 |
---|---|
OS | CentOS 7.4 |
URL | https://172.26.22.25 |
クライアントID (client_id) | apache24 |
リダイレクトURI | (いらない) |
設定する
Keycloakの設定
いつものように「クライアント」から前に作った「apache24」を選択します。
なぜか「ダイレクトアクセスグラントの有効」という名前になっているので、これを有効にします。それとこっそり同意を不要の設定にしておきます。
Relying Party(RP)の設定
ないです。リダイレクトURI設定してないし。
やってみる
これで設定は全部完了です。Resource Owner Password Credentialsでトークンを発行してみましょう。クライアントはcurlを使います。
> curl -k --request POST --header "Authorization: Basic YXBhY2hlMjQ6ZjY3NTc1MjEtZjRlOS00ZjVlLTllZDEtMGJmMmE2ZDA2Y2Zl" --data "username=u001&password=password&grant_type=password&scope=openid" "https://172.26.22.5/auth/realms/master/protocol/openid-connect/token"
パラメータを簡単に説明しておきましょう。
- リクエストヘッダ
ヘッダ名 | 値 |
---|---|
Authorization | クライアントID&シークレットのBasic認証 |
- リクエストパラメータ
パラメータ名 | 値 |
---|---|
grant_type | "password"を指定してResource Ownerなんとかを要求する |
username | エンドユーザーのID |
password | エンドユーザーのパスワード |
scope | スコープ |
実行結果。あまりにも長いので、トークンの値は省略しています。
{
"access_token": "eyJhbGciOi..(略)...",
"expires_in": 60,
"refresh_expires_in": 1800,
"refresh_token": "eyJhbGciOi..(略)...",
"token_type": "bearer",
"id_token": "eyJhbGciOi..(略)...",
"not-before-policy": 0,
"session_state": "bfbe9ca9-1ccb-4ff7-8857-14b862f2e682"
}
なんかものすごく簡単にトークンを発行できてしまいまいた。
改めて、Resource Owner Password Credentialsとは何なのか?
見ての通り、curl
1回で、つまりリクエスト1回でトークンを発行できてしまいました。もう認可コードフローとかいらないし、Relying Party(RP)用なんとかライブラリなんていらないな、というようにも見えます。今までのフローと全く違うため、不思議な感じがします。そのため、改めてResource Owner(長い)が何なのか、どういう意味なのかを説明しようと思います。
Resource Owner Password Credentialsで注意しなければならないこと
- エンドユーザーのID&パスワードでしか、ユーザーの認証ができない
これはそういう仕様(RFC)です、としか言いようがないです。これが意味するのは、ユーザーID&パスワード以外の認証はできない(つまりパラメータを追加できない)ということであり、多要素認証できないということでもあります。仕様と実装は別物、と言われればその通りだし、実際パラメータを1つ追加する実装をするくらいは簡単でしょう。しかし、その実装はもはやResource Owner Password Credentialsではありません。
この項目については、もう一つ重要なこととして「エンドユーザーやクライアントが認証を選択できない」というのがあるのですが、この話をするとアドベンド1日分になりそうなので、今回は割愛させていただきます。
- OAuth2.0の仕様(RFC)である
シングルサインオンおたくなら、すぐに「OAuth2.0は認可のプロトコルじゃん。なんで認証やってるの」という指摘が来そうです。なぜOAuth2.0の仕様(RFC)なのか、私にも分かりません。ただこれが意味するのは、認証について何も仕様(RFC)に書かれていないOAuth2.0が認証をやると、いろいろ仕様上矛盾が生じることになります。
例えば、ちょっと前に書いた「エンドユーザーやクライアントが認証を選択できる」という、ぶっちゃけ言ってしまうとOpenID Connectのacr_values、SAMLのAuthentication Context(認証コンテキスト)のことなのですが、OAuth2.0は認証をしないので、このことについては仕様(RFC)に何も書かれていません。だからOpenID Provider(OP)側で認証手段を用意して多要素認証をやっても、Resource Owner Password Credentialsを使われると、全部ガン無視でユーザーID&パスワードの認証に固定されてしまう訳です。
- エンドユーザーのID&パスワードを直接送信している
シングルサインオンのコンセプトの1つに「サーバー(RP)はユーザーのパスワードを知る必要が無い」というのがあるのですが、これに真っ向から矛盾しています。
- クライアントID&シークレットを送信している
じゃぁ、RESTfulでできるしjQuery使えばいいじゃん、と思うかもしれませんが、クライアントシークレットという、いわゆるシステムのパスワードを送っているため、ユーザーにクライアントシークレットが筒抜けスカスカになります。これはご法度です。(Publicクライアントの話はしませんよ)
- 同意が求められない
エンドユーザーに画面が出せないので、同意が必要な場合、トークンが発行できません。先ほど「やってみる」でやってみたとき、設定をこっそり同意不要にしたのは、このためです。
で、結局Resource Owner Password Credentialsって何なの(怒)
実はOAuth2.0の仕様(RFC)をよく読むと書いてあります。
ユーザーID&パスワードのレガシー認証(←一部の人は心穏やかじゃない表現かもしれませんが、仕様にこう書いてある)をやっているサーバーが、とりあえず手っとり早くトークンを発行できるようにするための、下位互換のための仕様。
つまり、将来的に認可コードフローとか暗黙フロー(Implicit Flow)を実装するんだけど、それまではResource Owner Password Credentialsで我慢してね、という仕様のようです。
Client Credentials編
Client Credentialsとは
通常Relying Party(RP)やResource Server(RS)がIDトークンやアクセストークンが欲しいのは、ユーザーが誰か(認証)やユーザーに権限があるか(認可)といった、ユーザーに紐付く情報が欲しいからで、そのような場合は認可コードフローや暗黙フロー(Implicit Flow)を使ってきました。しかし、あまり良い例ではないですが、ユーザーCRUD操作を提供するRESTfulアプリケーションがあったとします。このAPIは自分自身についてはCRUDができるとします。しかしこれとは別に、とあるシステムからはすべてのユーザーについてCRUDができるシステム管理者のような権限が欲しいとします。この場合、システム管理者がエンドユーザーになり変わってアクセストークンを… とかやり出すといきなり複雑になるので、通常はスーパー管理者権限を持ったアクセストークンを発行します。このスーパー管理者を1エンドユーザーとして暗黙フロー(Implicit Flow)でアクセストークンをもらってもいいですが、欲しいのはCRUD操作できるアクセストークンだけで良いことを考慮すると、Client Credentialsでさくっとアクセストークンをもらった方がずっと早そうです。
Client Credentialsは、技術的には認可コードフローの「クライアント認証」の部分だけ取り出した処理となっています。
やってみる
前置きが長くなってしまいましたが、やってみましょう。
- 設定する
いつものように「クライアント」から設定します。「サービスアカウントのの有効」を有効にします。Keycloakは各フローが ON/OFF ワンクリックで設定できるので、簡単です。
- 試してみる
curl
を使います。パラメータにgrant_type=client_credentials
を指定します。
> curl -k --request POST --header "Authorization: Basic YXBhY2hlMjQ6ZjY3NTc1MjEtZjRlOS00ZjVlLTllZDEtMGJmMmE2ZDA2Y2Zl" --data "grant_type=client_credentials&scope=openid" "https://172.26.22.5/auth/realms/master/protocol/openid-connect/token"
{
"access_token": "eyJhbGciOiJS..(略)..",
"expires_in": 60,
"refresh_expires_in": 1800,
"refresh_token": "eyJhbGciOiJSU..(略)..",
"token_type": "bearer",
"id_token": "eyJhbGciOiJSU..(略)..",
"not-before-policy": 0,
"session_state": "5efaf7fa-f9a2-46a5-aacb-d1b3185b541f"
}
トークンを取得できましたが、気になるのは、エンドユーザーを認証していないので、IDトークンの内容はどうなっているかでしょう。IDトークンのClaim
をデコードしてみます。
{
"jti": "43e665bb-9202-4288-a580-f89fe89b7d48",
"exp": 1509944839,
"nbf": 0,
"iat": 1509944779,
"iss": "http://172.26.22.5/auth/realms/master",
"aud": "apache24",
"sub": "9ef04d16-ca87-425c-b19d-e56c0427c78b",
"typ": "ID",
"azp": "apache24",
"auth_time": 0,
"session_state": "5efaf7fa-f9a2-46a5-aacb-d1b3185b541f",
"acr": "1",
"clientHost": "127.0.0.1",
"clientId": "apache24",
"preferred_username": "service-account-apache24",
"clientAddress": "127.0.0.1",
"email": "service-account-apache24@placeholder.org"
}
いくつか気になるところがありますが… iss
, aud
, azp
はまぁいいでしょう。sub
はKeycloak内部で持っているなにかのIDになっているようです(つまり何にも使えない)。そしてpreferred_username
とemail
が、変な値になっています。Client Credentialsのときは、使う属性はaud
, azp
, sub
(と他検証する属性)しか使わないので、この2つの値は何でもいいのですが、なんと言うか無理やり値を突っ込んだ印象を受けます。
クライアント認証の種類
今までは、クライアント認証には何も考えずにリクエストヘッダ(Basic認証)に付けていましたが、リクエストヘッダしか手段が無い、という訳ではありません。大きく分けて2種類、細かく分けると3種類あります。
- クライアントID&シークレット
- リクエストヘッダ(Basic認証)
- リクエストボディ(リクエストパラメータ)
- JWTベアラトークン
「クライアントID&シークレット」の場合、ヘッダだけではなくボディ(パラメータ)として渡すケースもあります。どちらが使えるか(または両方使えるか)は、OpenID Provider(OP)次第です。Keycloakでは両方可能でした。
「JWTベアラトークン」は、クライアントシークレットの代わりに署名を使う方法です。クライアント証明書を使ったクライアント認証に近い感じです、というかコレそのものです。Keycloakでは「クライアント」の「クレデンシャル」タブから「クライアント認証」で設定可能です。
まとめ
今日は暗黙フロー(Implicit Flow)、Resource Owner Password Credentials、最後にクライアント認証(Client Credentials)をやりました。表題に「試してみる」とありますが、内容はほとんど仕様の説明(=眠くなる話)になってしまいました。本当はこういったコンセプトの話は、こま切れになってしまうのでピンポイントのテーマとして書くのではなく、まとまって書くべきなのですが、OpenID ConnectやSAMLに詳しい人が書くでしょう。むしろ誰か書いて欲しい…。
Keycloakに関しては、結構細かい部分まで設定が調整できる印象を受けました。ただ、細かい調整ができるからと言っても、SAMLやOpenID Connectの仕様に反することは(当たり前ですが)できません。仕様はそうなっているのは分かるが、現実は… と言いたくなる気持ちは分かりますが、仕様(RFC)は仕様を書いた人が趣味で作っている訳ではなく(いや、そういう部分もあるけど)、セキュリティ面も考慮しての仕様(RFC)なので、まずは仕様に反しない設計から考えて欲しいと、私は思うのです。