Keycloak・リソースサーバー・Javascript クライアントの連携例

概要

Keycloak と、リソースサーバー、リソースクライアント間の SSO 検証メモ。(Javascript クライアント版)

すでに検証部分の大半は過去のポストで 完了しているため、ここでは主に Javascript 版での差分と、 Javascript クライアントからの利用方法を中心に書いていきます。

この手順で使用したコードは、以下に公開しているので、こちらも参考にしてください。
https://github.com/yo1000/kc-resource/tree/e09c1dd987/kc-resource-client-js

要件

環境

今回の作業環境は以下のとおりです。

  • Java 1.8.0_131
  • Spring Boot 1.5.9.RELEASE
  • Keycloak 3.4.1.Final
  • NodeJS v8.4.0
  • NPM 5.6.0
$ sw_vers
ProductName:	Mac OS X
ProductVersion:	10.12.5
BuildVersion:	16F2073

$ java -version
java version "1.8.0_131"
Java(TM) SE Runtime Environment (build 1.8.0_131-b11)
Java HotSpot(TM) 64-Bit Server VM (build 25.131-b11, mixed mode)

$ node -v
v8.4.0

$ npm -v
5.6.0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

認証認可フロー

過去のポストで掲示しているものと同じですが、全体を把握するのに役立つため再掲します。 認証認可フローの概要図は、以下のとおりです。

img/2017-12-18_kc-resource-flow.svg

各アクターの役割は、以下のとおりです。

  • User
    • Browser を操作して、リソースを表示するサイトを要求します
    • 認証を必要とした場合に、ログインします
  • Keycloak
    • SSO 基盤です
    • ユーザーを認証認可します
    • クライアント (認証認可情報を要求してきたアプリケーション) を認証認可します
  • Resource Server
    • Resource Store からリソースを取得して、クライアントへ提供します
      • 今回のサンプルでは、Resource Store からのリソース取得については実装しません
      • 今回のサンプルでは、ロールに応じた静的な値をリソースとして返却させます
  • Resource Client
    • Resource Server からリソースを取得して、画面に結果を表示します

備考

以降、一連の流れを実施するにあたり、複数のディレクトリで作業するため、 便宜上、${BASE_DIR} をディレクトリの基点として使用します。 また、ディレクトリ全体の構成は以下のようになります。

$ BASE_DIR=`pwd`
$ tree -d -L 2
.
├── kc-resource
│   ├── kc-resource-client-js
│   └── kc-resource-server
└── keycloak-3.4.1.Final
    ├── bin
    ├── docs
    ├── domain
    ├── modules
    ├── standalone
    ├── themes
    └── welcome-content
1
2
3
4
5
6
7
8
9
10
11
12
13
14

Keycloak のセットアップ (SSO Server)

過去のポストで構築したものをそのまま使用します。

構築済みの Keycloak がない場合は、このポストを参考に準備してください。

Keycloak ヘログイン

以降、kcadm.sh を使用する上で、ログイン状態が必要になるため、ログインします。 kcadm.sh 実行時に、以下のようなメッセージが出力された場合は、 ログインセッションが期限切れとなっているため、改めてログインします。

Session has expired. Login again with ‘kcadm.sh config credentials’

$ # Login to Keycloak
$ ${BASE_DIR}/keycloak-3.4.1.Final/bin/kcadm.sh config credentials \
  --server http://127.0.0.1:8080/auth \
  --realm master \
  --user keycloak \
  --password keycloak1234
Logging into http://127.0.0.1:8080/auth as user admin of realm master
1
2
3
4
5
6
7

SSO クライアントの登録

SSO 基盤を使用するアプリケーション (SSO サーバーに対する、クライアント) を登録します。 リソースサーバーとして kc-resource-server は既に作成されているものとします。 ここではリソースクライアントとして、新たに kc-resource-client-js を作成します。

$ # Add realm client for Resource client (for Javascript)
$ RES_CLI_ID=`\
  ${BASE_DIR}/keycloak-3.4.1.Final/bin/kcadm.sh create clients \
  -r kc-resource \
  -s clientId=kc-resource-client-js \
  -s publicClient=true \
  -s 'redirectUris=["http://127.0.0.1:28081/*"]' \
  -s 'webOrigins=["http://127.0.0.1:28081"]' \
  -i\
  `; echo $RES_CLI_ID
f970945c-67dc-4c09-8126-423158ff1248
1
2
3
4
5
6
7
8
9
10
11

リソースサーバーの実装 (SSO Client - A)

過去のポストで構築したものに、必要な変更を加えていきます。

構築済みのリソースサーバーがない場合は、このポストを参考にするか、 ポストの冒頭で挙げた GitHub リポジトリから、プロジェクトをクローンして準備してください。

セキュリティ構成の実装

コード例の後に、要点をまとめます。

package com.yo1000.keycloak.resource.server

import org.keycloak.adapters.KeycloakConfigResolver
import org.keycloak.adapters.springboot.KeycloakSpringBootConfigResolver
import org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationProvider
import org.keycloak.adapters.springsecurity.config.KeycloakWebSecurityConfigurerAdapter
import org.keycloak.adapters.springsecurity.filter.KeycloakAuthenticationProcessingFilter
import org.keycloak.adapters.springsecurity.filter.KeycloakPreAuthActionsFilter
import org.springframework.boot.web.servlet.FilterRegistrationBean
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper
import org.springframework.security.core.authority.mapping.SimpleAuthorityMapper
import org.springframework.security.web.authentication.session.NullAuthenticatedSessionStrategy
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy
import org.springframework.web.cors.CorsConfiguration
import org.springframework.web.cors.UrlBasedCorsConfigurationSource

@Configuration
@EnableWebSecurity
class KcSecurityConfiguration: KeycloakWebSecurityConfigurerAdapter() {
    @Bean
    fun grantedAuthoritiesMapper(): GrantedAuthoritiesMapper {
        val mapper = SimpleAuthorityMapper()
        mapper.setConvertToUpperCase(true)
        return mapper
    }

    @Bean
    fun keycloakConfigResolver(): KeycloakConfigResolver {
        return KeycloakSpringBootConfigResolver()
    }

    @Bean
    fun keycloakAuthenticationProcessingFilterRegistrationBean(
            filter: KeycloakAuthenticationProcessingFilter): FilterRegistrationBean {
        val registrationBean = FilterRegistrationBean(filter)
        registrationBean.isEnabled = false
        return registrationBean
    }

    @Bean
    fun keycloakPreAuthActionsFilterRegistrationBean(
            filter: KeycloakPreAuthActionsFilter): FilterRegistrationBean {
        val registrationBean = FilterRegistrationBean(filter)
        registrationBean.isEnabled = false
        return registrationBean
    }

    override fun sessionAuthenticationStrategy(): SessionAuthenticationStrategy {
        return NullAuthenticatedSessionStrategy()
    }

    override fun keycloakAuthenticationProvider(): KeycloakAuthenticationProvider {
        val provider = super.keycloakAuthenticationProvider()
        provider.setGrantedAuthoritiesMapper(grantedAuthoritiesMapper())
        return provider
    }

    override fun configure(auth: AuthenticationManagerBuilder?) {
        auth!!.authenticationProvider(keycloakAuthenticationProvider())
    }

    override fun configure(http: HttpSecurity) {
        super.configure(http)
        http
                .authorizeRequests()
                .antMatchers("/kc/resource/server/admin").hasRole("ADMIN")
                .antMatchers("/kc/resource/server/user").hasRole("USER")
                .anyRequest().permitAll()
                .and()
                .cors()
                .configurationSource(UrlBasedCorsConfigurationSource().apply {
                    registerCorsConfiguration("/**", CorsConfiguration().apply {
                        addAllowedHeader(CorsConfiguration.ALL)
                        addAllowedMethod("GET")
                        addAllowedOrigin("http://127.0.0.1:28081")
                        allowCredentials = true
                    })
                })
    }
}
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

configure(http: HttpSecurity)

認証で保護したい URL のパターンと、許可するロールの組み合わせを設定します。 また、Javascript クライアント向けに、CORS 設定を追加しています。

CORS 設定を適用する URL のパターン、およびクロスオリジンリクエストを許可するリクエストのパターンを設定します。 ここでは主に以下の3点を設定します。

  • Header: CorsConfiguration.ALL で、任意のヘッダーを持ったリクエストを許可
  • Meathod: "GET" で、HTTP GET メソッドによるリクエストを許可
  • Origin: 指定したドメインからのリクエストを許可

設定された すべての条件を満たすリクエストのみ が、クロスオリジンリクエストを許可されるようになります。

ビルド・起動

リソースサーバー用アプリケーションを起動します。

$ cd ${BASE_DIR}/kc-resource/kc-resource-server
$ ./mvnw clean spring-boot:run &
1
2

リソースクライアントの実装 (SSO Client - B)

プロジェクトの作成

アプリケーションサーバーは必要ありませんが、 HTTP Server を起動しなければならないため、ここでは npm を使用します。 以下のように プロジェクト用ディレクトリの作成と、package.json を準備してください。

$ mkdir ${BASE_DIR/kc-resource/}kc-resource-client-js
$ cd ${BASE_DIR/kc-resource/}kc-resource-client-js
1
2
{
  "name": "kc-resource-client",
  "version": "1.0.0",
  "author": "",
  "license": "MIT",
  "description": "",
  "scripts": {
    "http": "http-server -o -p 28081"
  },
  "devDependencies": {
    "http-server": "^0.10.0"
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

npm を使用しない場合でも、HTML ファイルを HTTP に公開可能であれば、 Apache や Nginx へ公開するような形をとっても良いです。

設定ファイルの配置

リソースクライアント用の、構成ファイルをセットアップします。

Set up Clients で $RES_CLI_ID 変数に取っておいた、 クライアント ID を使用して、keycloak.json を出力します。 keycloak.json は、Keycloak の認証を受けるスクリプトが実行されるパスと、同じ場所に配置する必要があります。 index.html 内に認証スクリプトを実装する場合、以下のような構成になります。

$ cd ${BASE_DIR}/kc-resource/kc-resource-client-js
$ tree -I node_*
.
├── index.html
├── keycloak.json
└── package.json
1
2
3
4
5
6

keycloak.json は、kcadm.sh を使用して、 以下のように Keycloak サーバーから取得します。

$ ${BASE_DIR}/keycloak-3.4.1.Final/bin/kcadm.sh \
  get clients/${RES_CLI_ID}/installation/providers/keycloak-oidc-keycloak-json \
  -r kc-resource \
  > ${BASE_DIR}/kc-resource/kc-resource-client-js/keycloak.json 
1
2
3
4

認証とリソース取得スクリプトの実装

ここまでできたら、Javascript から SSO を利用する準備は整いました。 スクリプトを実装して、実際にリソースサーバーからリソースを取得してみます。

コード例の後に、要点をまとめます。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>kc-resource-client</title>
</head>
<body>
<div>
    <button id="req-admin-btn">Request admin resource</button>
</div>
<div>
    <button id="req-user-btn">Request user resource</button>
</div>
<pre id="display-username"></pre>
<pre id="display-resource"></pre>
<script type="text/javascript" src="http://127.0.0.1:8080/auth/js/keycloak.js"></script>
<script type="text/javascript">
(function () {
    /**
     * @param keycloak
     * @param role
     */
    function getResource(keycloak, role) {
        if (!keycloak || !keycloak.token) {
            console.error('Require Authentication.');
            return;
        }

        if (!role) {
            console.error('Require Role in arguments.');
            return;
        }

        var url = 'http://localhost:18080/kc/resource/server/' + role;
        var xhr = new XMLHttpRequest();

        xhr.open('GET', url, true);
        xhr.setRequestHeader('Accept', 'application/json');
        xhr.setRequestHeader('Authorization', 'Bearer ' + keycloak.token);
        xhr.onreadystatechange = function () {
            var displayResource = document.getElementById('display-resource');

            if (xhr.readyState === 4) {
                if (xhr.status === 200) {
                    console.info('OK');
                    displayResource.innerText = xhr.responseText;
                } else if (xhr.status === 403) {
                    console.error('Forbidden');
                    displayResource.innerText = '[Forbidden]';
                } else {
                    console.error(xhr);
                }
            }
        };

        xhr.send();
    }

    /*
     * Will be run when page loading.
     */
    var keycloak = Keycloak();

    keycloak
        .init({
            "onLoad" : "login-required"
        })
        .success(function(authenticated) {
            console.info(authenticated ? 'Authenticated' : 'Not authenticated');
            console.debug(keycloak);

            var displayUsername = document.getElementById('display-username');
            displayUsername.innerText = keycloak.tokenParsed.preferred_username;
        }).error(function() {
        console.error('Failed to initialize');
    });

    var reqAdminBtn = document.getElementById('req-admin-btn');
    reqAdminBtn.addEventListener('click', function (e) {
        getResource(keycloak, 'admin');
    }, false);

    var reqUserBtn = document.getElementById('req-user-btn');
    reqUserBtn.addEventListener('click', function (e) {
        getResource(keycloak, 'user');
    }, false);
})();
</script>
</body>
</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
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

keycloak.js

<script type="text/javascript" src="http://127.0.0.1:8080/auth/js/keycloak.js"></script> からも分かるように、 Javascript 用 Keycloak アダプターライブラリは、Keycloak サーバー内にホストされています。 アプリケーションが使用する Keycloak サーバー上のライブラリを参照する方法が推奨されており、 この参照方法に従っておくと、Keycloak サーバーのバージョンアップ時などにスクリプトも合わせて最新化されるため、 バージョンを追従できるようになります。

keycloak.token

Keycloak オブジェクトは SSO による認証を受ける前と、受けた後でフィールドの状態が大きく変化します。 認証前の状態では、keycloak.token を参照することはできません。 認証後、keycloak.token が参照可能となり、リソースサーバーへの認証情報として使用可能になります。

keycloak.tokenParsed

keycloak.token 同様に、認証後に参照可能になるフィールドです。 このフィールドには認証を受けたユーザーや、そのロール、認証トークンの有効期限などが設定されています。

ビルド・起動

リソースクライアント用ページを表示します。

$ cd ${BASE_DIR}/kc-resource/kc-resource-client-js
$ npm install
$ npm run http
1
2
3

デモ

参考までに、実際に動かした結果を、以下キャプチャに残しておきます。

以下 npm コマンドを実行すると、公開されたページが表示されます。 表示されなかった場合は、コマンド結果に従って、 ブラウザで http://127.0.0.1:28081 を表示してください。

$ npm run http

> kc-resource-client@1.0.0 http ${BASE_DIR}/kc-resource/kc-resource-client-js
> http-server -o -p 28081

Starting up http-server, serving ./
Available on:
  http://127.0.0.1:28081
  http://10.0.0.7:28081
  http://10.211.55.2:28081
  http://10.37.129.2:28081
Hit CTRL-C to stop the server
1
2
3
4
5
6
7
8
9
10
11
12

初回表示では、Keycloak へリダイレクトされ、ログインを要求されます。
img/2017-12-18_kc-resource-demo-1.png

ログインすると、画面にユーザー名が表示され、ボタンをクリックすると要求に応じたメッセージが表示されます。
img/2018-01-07_kc-resource-demo-3.png

参考