オライリーの書籍「実践Keycloak」にあるJavaScriptアダプターについて試してみました。
環境)
Keycloak v21/Ubuntu 22.04 / 192.168.1.172
Keycloak v15, node.js App/Ubuntu 20.04 / 192.168.1.148
(これまで使ってきたもの。二つのバージョンで動作確認)
「7.6 JavaScriptアプリケーションとの統合」のJavaScriptコード取得
git clone https://github.com/PacktPublishing/Keycloak-Identity-and-Access-Management-for-Modern-Applications.git
node.js App インストールと実行
n@n07:~/Keycloak-Identity-and-Access-Management-for-Modern-Applications/ch7/keycloak-js-adapter$ sudo apt install npm
n@n07:~/Keycloak-Identity-and-Access-Management-for-Modern-Applications/ch7/keycloak-js-adapter$ npm install
npm WARN deprecated start@5.1.0: Deprecated in favor of https://github.com/deepsweet/start
npm notice created a lockfile as package-lock.json. You should commit this file.
npm WARN keycloak-js-app@0.0.1 No description
npm WARN keycloak-js-app@0.0.1 No repository field.
npm WARN keycloak-js-app@0.0.1 No license field.added 61 packages from 47 contributors and audited 61 packages in 2.329s
8 packages are looking for funding
runnpm fund
for detailsfound 0 vulnerabilities
n@n07:~/Keycloak-Identity-and-Access-Management-for-Modern-Applications/ch7/keycloak-js-adapter$ npm start
> keycloak-js-app@0.0.1 start /home/n/Keycloak-Identity-and-Access-Management-for-Modern-Applications/ch7/keycloak-js-adapter
> node app.js
app.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
var express = require('express'); var app = express(); var stringReplace = require('string-replace-middleware'); var fs = require('fs'); var KC_URL = process.env.KC_URL || "http://192.168.1.148:8080"; app.use(stringReplace({ 'KC_URL': KC_URL })); app.use(express.static('.')) app.get('/', function(req, res) { res.render('index.html'); }); /* app.get('/index', function(req, res){ fs.readFile('./index.html', 'UTF-8', function(err, data){ res.writeHead(200, {'Content-Type': 'text/html'}); res.header({ 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET,PUT,POST,DELETE' }); res.write(data); res.end(); }); }); */ app.listen(8082); |
※v21は、KC_URLをhttp://192.168.1.148:8080 に
(コメントは、オリジン間リソース共有 (Cross-Origin Resource Sharing, CORS) のエラーがでたときに追加。しかしNG。Keycloakの設定で解決できることがわかる。一応残しておく)
index.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
<!DOCTYPE html> <html> <head> <title>Keycloak Example Application</title> <script type="text/javascript" src="KC_URL/auth/js/keycloak.js"></script> <script type="text/javascript"> function output(content) { if (typeof content === 'object') { content = JSON.stringify(content, null, 2) } document.getElementById('output').textContent = content; } function profile() { if (keycloak.idTokenParsed.name) { document.getElementById('name').textContent = 'Hello ' + keycloak.idTokenParsed.name; } else { document.getElementById('name').textContent = 'Hello ' + keycloak.idTokenParsed.preferred_username; } document.getElementById('user').style.display = 'block'; } //keycloak = new Keycloak({ realm: 'apache', clientId: 'apache24' }); keycloak = new Keycloak(); keycloak.init({onLoad: 'login-required'}).then(function () { console.log('User is now authenticated.'); profile(); }).catch(function () { console.log('Error'); // window.location.reload(); }); /* keycloak.init({onLoad: 'login-required'}).then(function(authenticated) { alert(authenticated ? 'authenticated' : 'not authenticated'); }).catch(function() { alert('failed to initialize'); }); */ </script> </head> <body> <div id="user" style="display: none"> <button onclick="window.keycloak.logout()">Logout</button> <button onclick="output(keycloak.idTokenParsed)">Show ID Token</button> <button onclick="output(keycloak.tokenParsed)">Show Access Token</button> <button onclick="window.keycloak.updateToken(-1).then(function() { output(keycloak.idTokenParsed); profile() })">Refresh</button> <hr/> <h2 id="name"></h2> <pre id="output"></pre> </div> </body> </html> |
※v21は、KC_URL/auth を KC_URL に
n@n07:~/Keycloak-Identity-and-Access-Management-for-Modern-Applications/ch7/keycloak-js-adapter$ cat keycloak.json
1 2 3 4 5 6 7 8 |
{ "realm": "apache", "auth-server-url": "http://192.168.1.148:8080/auth/", "ssl-required": "external", "resource": "apache24", "public-client": true, "confidential-port": 0 } |
※v21は、http://192.168.1.148:8080/auth/ を http://192.168.1.172:8080/ に
設定メモ)最初Confidential Clientの設定で、動かしたところtokenの取得でclient_secretがないというエラー。考えてみれはシークレットキーをkeycloak.json(管理画面(install)からDownload)に置くことはセキュリティ上ないのかもと思い、publicにする。keycloak.jsonから設定を読むときは、new Keycloak()を使用。またPKCEは無効にする。
当然どちらも同じ画面ですが、v21はstep2.htmlをスキップしました。
これだけの機能を、JavaScriptライブラリとして提供されていることを便利に感じました。(SPAに簡単に導入できる)
最後に、step2.htmlをスキップしている部分の確認と、ブラウザで確認できなかったtokenの取得部分をパケットキャプチャを残しておきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 |
02:26:41.471359 IP 192.168.1.228.51004 > 192.168.1.172.http-alt: Flags [P.], seq 1058:1515, ack 40285, win 256, length 457: HTTP: GET /realms/apache/protocol/openid-connect/3p-cookies/step1.html HTTP/1.1 E....<@..............<.....V.X..P.......GET /realms/apache/protocol/openid-connect/3p-cookies/step1.html HTTP/1.1 Host: 192.168.1.172:8080 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/112.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8 Accept-Language: ja,en-US;q=0.7,en;q=0.3 Accept-Encoding: gzip, deflate Connection: keep-alive Referer: http://192.168.1.148:8082/ Upgrade-Insecure-Requests: 1 02:26:41.474361 IP 192.168.1.172.http-alt > 192.168.1.228.51004: Flags [P.], seq 40285:41467, ack 1515, win 501, length 1182: HTTP: HTTP/1.1 200 OK E.....@.@..s...........<.X......P...]d..HTTP/1.1 200 OK Referrer-Policy: no-referrer Strict-Transport-Security: max-age=31536000; includeSubDomains X-Robots-Tag: none Cache-Control: no-cache, must-revalidate, no-transform, no-store X-Content-Type-Options: nosniff Content-Security-Policy: frame-src 'self'; object-src 'none'; P3P: CP="This is not a P3P policy!" X-XSS-Protection: 1; mode=block Content-Type: text/html;charset=utf-8 content-length: 757 <!DOCTYPE html> <html> <head> <meta charset="utf-8" /> </head> <body> <script> if ("hasStorageAccess" in document) { checkStorageAccess(); } else { placeTestCookie(); } function checkStorageAccess() { document.hasStorageAccess().then(function (hasAccess) { window.parent.postMessage( hasAccess ? "supported" : "unsupported", "*" ); }); } function placeTestCookie() { document.cookie = "KEYCLOAK_3P_COOKIE_SAMESITE=supported; Max-Age=60; SameSite=None; Secure"; document.cookie = "KEYCLOAK_3P_COOKIE=supported; Max-Age=60"; window.location = "step2.html"; } </script> </body> </html> 02:26:41.482453 IP 192.168.1.228.51019 > n07.8082: Flags [.], ack 967, win 509, length 0 E..(Y.@....Z.........K...L.....tP............. 02:26:41.520615 IP 192.168.1.228.51004 > 192.168.1.172.http-alt: Flags [.], ack 41467, win 252, length 0 E..(.=@..............<.......X..P...v......... 02:26:41.576107 IP 192.168.1.228.51004 > 192.168.1.172.http-alt: Flags [P.], seq 1515:2164, ack 41467, win 252, length 649: HTTP: POST /realms/apache/protocol/openid-connect/token HTTP/1.1 E....>@....'.........<.......X..P.......POST /realms/apache/protocol/openid-connect/token HTTP/1.1 Host: 192.168.1.172:8080 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/112.0 Accept: */* Accept-Language: ja,en-US;q=0.7,en;q=0.3 Accept-Encoding: gzip, deflate Content-type: application/x-www-form-urlencoded Content-Length: 214 Origin: http://192.168.1.148:8082 Connection: keep-alive Referer: http://192.168.1.148:8082/ code=487a3c08-e7fe-4931-9108-29daaf1ff313.11856c64-25af-4e5c-a1c4-b7cebfa00164.3f9bac2f-4c46-4747-ae7d-eaf4b9a738e9&grant_type=authorization_code&client_id=apache24&redirect_uri=http%3A%2F%2F192.168.1.148%3A8082%2F 02:26:41.593178 IP 192.168.1.172.http-alt > 192.168.1.228.51004: Flags [P.], seq 41467:44387, ack 2164, win 501, length 2920: HTTP: HTTP/1.1 200 OK E.....@.@..............<.X......P....c..HTTP/1.1 200 OK Referrer-Policy: no-referrer X-Frame-Options: SAMEORIGIN Access-Control-Expose-Headers: Access-Control-Allow-Methods Strict-Transport-Security: max-age=31536000; includeSubDomains Cache-Control: no-store Access-Control-Allow-Origin: http://192.168.1.148:8082 Access-Control-Allow-Credentials: true X-Content-Type-Options: nosniff Pragma: no-cache X-XSS-Protection: 1; mode=block Content-Type: application/json content-length: 3748 {"access_token":"eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJtelcwdllVN25ZUElCRk42Y2NqX2dsSEROUHBub0NyckFoMWVLMklQN0hRIn0.eyJleHAiOjE2ODM1OTk1MDEsImlhdCI6MTY4MzU5OTIwMSwiYXV0aF90aW1lIjoxNjgzNTk5MTU5LCJqdGkiOiJhODZjYTM4MC02ZTYyLTRkMmEtOGFkZC01YTk1YmFiODdlYTciLCJpc3MiOiJodHRwOi8vMTkyLjE2OC4xLjE3Mjo4MDgwL3JlYWxtcy9hcGFjaGUiLCJhdWQiOiJhY2NvdW50Iiwic3ViIjoiYzQyMTM3YjktZTQ2OC00MGJmLThlNmQtMzg3OWMyNTFiMWE1IiwidHlwIjoiQmVhcmVyIiwiYXpwIjoiYXBhY2hlMjQiLCJub25jZSI6IjY4NmJhMGUzLTIxMzYtNDBlNi1hYTg1LTQyYWMzNjA0OGM2ZCIsInNlc3Npb25fc3RhdGUiOiIxMTg1NmM2NC0yNWFmLTRlNWMtYTFjNC1iN2NlYmZhMDAxNjQiLCJhY3IiOiIwIiwiYWxsb3dlZC1vcmlnaW5zIjpbIioiXSwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbImRlZmF1bHQtcm9sZXMtYXBhY2hlIiwib2ZmbGluZV9hY2Nlc3MiLCJ1bWFfYXV0aG9yaXphdGlvbiJdfSwicmVzb3VyY2VfYWNjZXNzIjp7ImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoib3BlbmlkIGVtYWlsIHByb2ZpbGUiLCJzaWQiOiIxMTg1NmM ..... |