KeycloakとSpringBoot/SecurityでOpenID Connect(クライアント編)

前回の続きです。

[KeycloakとSpringBoot/SecurityでOpenID Connect(リソースサーバー編)](https://qiita.com/kasa_le/items/259792d3ae4df7f1d2b2)

# 目標
KeycloakをID Providerとして、クライアントアプリケーションを作成する(Webアプリ)。

本当はSpring Framework(not Boot)で作りたかったのですが、スピード重視で一旦Bootでやります。
（検索するとBootの情報しかない気がします・・・)

# 環境など

|ツールなど|バージョンなど|
|:--|:--|
| MacbookPro |macOS Mojave 10.14.5|
|IntelliJ IDEA |Ultimate 2019.3.3|
|Java |AdoptOpenJDK 11|
|apache maven |3.6.3|
|JUnit|5.6.0|
|Postman|7.19.1|
|Spring Boot|2.2.6.RELEASE|
|Spring Security|5.2.2.RELEASE|
|Keycloak|9.0.3|
|Docker|19.03.8, build afacb8b|
|Thymeleaf|3.0.11.RELEASE|

# クライアントアプリを作る
Spring Bootでちゃちゃっと作ります。
[前回](https://qiita.com/kasa_le/items/259792d3ae4df7f1d2b2)の雛形の作成までを参考にプロジェクトを作ってください。

## 1.ポート番号の設定など

このまま作ると`8080`になってリソースサーバーとかぶるので、変更します。
Keycloakが`8088`なので、どうしようかな。
`9000`くらいにしときますか。空いてるか心配な方は`Portscan`(Macの場合)などで調べるといいかもしれませんね。

```application.properties
server.port=9000
```

これで実行すれば、`localhost:9000`でアクセスできます。

## 2.ページを作成

htmlを表示するのに、なんかおすすめらしい[Thymeleaf(タイムリーフ)](https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf_ja.html)を使ってみます

```xml:pom.xml
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
  </dependency>
```

### (1)indexページ

indexページはnon-secureでいいでしょう。このページは固定なのでThymeleafは使っていません。

```resource/templates/index.html
<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <title>Home</title>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css" integrity="sha384-WskhaSGFgHYWDcbwN70/dfYBj47jz9qbsMId/iRN3ewGhXQFZCSftd1LZCfmhktB" crossorigin="anonymous">
</head>

<body class="text-center">

<div class="cover-container d-flex w-100 h-100 p-3 mx-auto flex-column">
    <main role="main" class="inner cover">
        <h1 class="cover-heading">Apache Keycloak SSO demonstration</h1>
        <p class="lead">
        </p>
        <p class="lead">
            <a href="/products" class="btn btn-lg btn-secondary">Search products</a>
        </p>
    </main>
</div>

</body>
```

これで実行して、http://127.0.0.1:9000/ にアクセスすると、以下のようなページが開きます。

|<img width="687" alt="index.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103886/00bbe203-5cd2-b558-6313-ff08c1b4233b.png">|
|:--|

### (2)productsページ

後でセキュアなページにする予定のページを作ります。

```ProductController.java
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;

@Controller
public class ProductController {

    private final ProductService productService;

    public ProductController(ProductService productService) {
        this.productService = productService;
    }

    @GetMapping(path = "/products")
    public String getProducts(Model model){
        model.addAttribute("products", productService.getProducts());
        return "products";
    }
}
```

```ProductService.java
import org.springframework.stereotype.Service;

import java.util.Arrays;
import java.util.List;

@Service
public class ProductService{

    public List getProducts(){
        return Arrays.asList("Mazda","Toyota","Audi");
    }
}
```

```resource/templates/products.html
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <title>Keycloak</title>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css"
          integrity="sha384-WskhaSGFgHYWDcbwN70/dfYBj47jz9qbsMId/iRN3ewGhXQFZCSftd1LZCfmhktB" crossorigin="anonymous">

</head>

<body class="text-center">
<div class="cover-container d-flex w-100 h-100 p-3 mx-auto flex-column">

    <main role="main" class="inner cover">
        <h1 class="cover-heading">Keycloak SSO demonstration</h1>

        <div style="width: 18rem; padding: 10px; display: block; margin-left: auto;margin-right: auto">
            <ul class="list-group">
                <li th:each="element : ${products}" class="list-group-item">[[${element}]]</li>
            </ul>
        </div>
    </main>
</div>

</body>
</html>
```

実行するとこうなります。

|<img width="550" alt="products.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103886/2730f5ae-6044-23e8-d0eb-1a20f3e1bbcd.png">|
|:--|

## 3.Keycloak関係の設定を追加
### (1)依存関係の追加

Keycloakと、SpringSecurityを追加します。

```xml:pom.xml
    <properties>
        <java.version>11</java.version>
        <keycloak.version>9.0.3</keycloak.version>
    </properties>
    
   <dependencies>
       ...
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.keycloak</groupId>
            <artifactId>keycloak-spring-boot-starter</artifactId>
        </dependency>
        ...
   </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.keycloak.bom</groupId>
                <artifactId>keycloak-adapter-bom</artifactId>
                <version>${keycloak.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
```

### (2)セキュリティ設定クラスを追加

```SecurityConfig.java
package com.example.client_demo;

import org.keycloak.adapters.KeycloakConfigResolver;
import org.keycloak.adapters.springboot.KeycloakSpringBootConfigResolver;
import org.keycloak.adapters.springsecurity.KeycloakConfiguration;
import org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationProvider;
import org.keycloak.adapters.springsecurity.config.KeycloakWebSecurityConfigurerAdapter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.authority.mapping.SimpleAuthorityMapper;
import org.springframework.security.core.session.SessionRegistryImpl;
import org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;

@KeycloakConfiguration
public class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter {

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        KeycloakAuthenticationProvider keycloakAuthenticationProvider = keycloakAuthenticationProvider();
        keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(new SimpleAuthorityMapper());
        auth.authenticationProvider(keycloakAuthenticationProvider);
    }

    @Bean
    @Override
    protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
        return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl());
    }

    @Bean
    public KeycloakConfigResolver keycloakConfigResolver() {
        return new KeycloakSpringBootConfigResolver();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        super.configure(http);
        http.authorizeRequests()
                .antMatchers("/products*").hasRole("user")
                .anyRequest().permitAll();
    }
}
```

### (3)application.properites

```application.properties
keycloak.enabled=true
keycloak.auth-server-url=http://localhost:8088/auth
keycloak.realm=myrealm
keycloak.resource=client1
keycloak.public-client=true
```

ポート番号、レルム名は自分でつけたものに合わせてください。
`keycloak.resource`は、これからKeycloakに作成するクライアントIDです。

この状態で起動すると、`/products`ページは`401`になる・・・

というか、Keycloakの画面に飛んで、「そんなクライアントいないよ」と言われますね。

|<img width="565" alt="keycloak_noclient.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103886/d71324bf-d5c5-7ef8-4ec2-11e86f9c7654.png">|
|:--|

# Keycloakにクライアントを追加する

前回やったようにクライアントを追加します。

|<img width="786" alt="create_client1.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103886/29571dc0-0ed9-f868-d2b5-08531c75ed2d.png">|
|:--|

- **AccessType**
  - **public**にします
- **Valid Redirect URIs**
  - `http://localhost:9000*`とします

上記以外はデフォルトでいいかと思います。
ポート番号は自分で設定したのと合わせてください。

|<img width="758" alt="detail_client1.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103886/8055120a-c06f-b4d2-3f17-8cf8cf3505b0.png">|
|:--|

リダイレクトURLに、`localhost`パターンとローカルIP指定パターンの両方登録しておきました。

[Save]を忘れずに。

これで接続してみます。
(127.0.0.1:9000ではなく、localhost:9000でアクセスしてください。リダイレクトがうまくかかりません)
クライアントアプリを再起動して`/products`ページにアクセス！

|<img width="589" alt="keycloak_login.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103886/b3e874df-a53b-47f9-1f5c-3d8d3b82b570.png">|
|:--|

ログイン画面が出ました！
作成済みの任意のユーザー名とパスワードでログインしてみましょう！

|<img width="698" alt="403.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103886/dc04e8df-2e53-8e00-6057-a9056cd43802.png">|
|:--|

あ・・・あれ、Forbidden(403)・・・

当たり前です。ユーザーにロールを設定していませんでした(テヘペロ)

# ユーザーにロールを割り当てる

## 1.ロールを追加
Keycloakの管理画面で、`Roles`から[Add Role]をクリックします。

|<img width="1015" alt="add_role.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103886/6361d071-12ec-fdc8-ef18-44c609c80a64.png">|
|:--|

**Role Name**に`user`と入力し、[Save]。

|<img width="578" alt="user_role.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103886/020094b8-08d3-6695-ae10-321fd4886f94.png">|
|:--|

## 2.ユーザーにロールを割り当てる

- `Users`-[View all users]で、任意のユーザーを選ぶ
- [Role Mappings]タブをクリック
- **Available Roles**にある`user`を選択して、[Add selefcted]とする

|<img width="720" alt="user_role_mapping.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103886/04ac2e91-6d96-33c4-c4d2-45ac8b457d17.png">|
|:--|

# アクセス！

|<img width="550" alt="products.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103886/2730f5ae-6044-23e8-d0eb-1a20f3e1bbcd.png">|
|:--|

きました！


# ユーザー情報を取得する

といってもユーザー情報APIを叩くのではなくて、アクセストークンを解析して取ってみようと思います。

## 1.ユーザー情報閲覧ページを追加

```ProductController.java
    @GetMapping(path = "userinfo")
    public String userInfo(HttpServletRequest request, Model model){
        KeycloakAuthenticationToken token = (KeycloakAuthenticationToken) request.getUserPrincipal();
        KeycloakPrincipal principal=(KeycloakPrincipal)token.getPrincipal();
        KeycloakSecurityContext session = principal.getKeycloakSecurityContext();
        AccessToken accessToken = session.getToken();
        UserInfo info = new UserInfo();
        info.username = accessToken.getPreferredUsername();
        info.emailID = accessToken.getEmail();
        info.lastname = accessToken.getFamilyName();
        info.firstname = accessToken.getGivenName();
        info.realmName = accessToken.getIssuer();
        AccessToken.Access realmAccess = accessToken.getRealmAccess();
        info.roles = realmAccess.getRoles().toString();
        info.scopes = accessToken.getScope();
        model.addAttribute("userinfo", info);
        return "userinfo";
    }
    
    public static class UserInfo {
        public String username;
        public String emailID;
        public String lastname;
        public String firstname;
        public String realmName;
        public String roles;
        public String scopes;
    }
    
    @GetMapping(path = "/logout")
    public String logout(HttpServletRequest request) throws ServletException {
        request.logout();
        return "redirect:/";
    }
```

ついでにログアウトボタンも付けましょう。

```userinfo.html
<!doctype html>
<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="utf-8">
    <title>UserInfo</title>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css" integrity="sha384-WskhaSGFgHYWDcbwN70/dfYBj47jz9qbsMId/iRN3ewGhXQFZCSftd1LZCfmhktB" crossorigin="anonymous">
</head>

<body class="text-center">

<div class="cover-container d-flex w-100 h-100 p-3 mx-auto flex-column">
    <main role="main" class="inner cover">
        <h1 class="cover-heading">User Info</h1>
        <p class="lead">
        </p>
        <p class="lead">
            <div th:text="|user name = ${userinfo.username}|"></div>
            <div th:text="|email = ${userinfo.emailID}|"></div>
            <div th:text="|last name = ${userinfo.lastname}|"></div>
            <div th:text="|first name = ${userinfo.firstname}|"></div>
            <div th:text="|roles = ${userinfo.roles}|"></div>
            <div th:text="|scopes = ${userinfo.scopes}|"></div>
            <div th:text="|access token = ${userinfo.accessToken}|"></div>
            <div th:text="|id token = ${userinfo.idToken}|"></div>
        </p>

        <p class="lead">
            <a href="/logout" class="btn btn-lg btn-secondary">Logout</a>
        </p>
    </main>
</div>

</body>
```

デバッグ用にAccess TokenもID Tokenも晒しちゃいます。

## 2.indexページにユーザー情報ページへのリンクを追加

```index.html
<div class="cover-container d-flex w-100 h-100 p-3 mx-auto flex-column">
    <main role="main" class="inner cover">
        <h1 class="cover-heading">Apache Keycloak SSO demonstration</h1>
        <p class="lead">
        </p>
        <p class="lead">
            <a href="/products" class="btn btn-lg btn-secondary">Search products</a>
        </p>
        <p class="lead">
        </p>
        <p class="lead">
            <a href="/userinfo" class="btn btn-lg btn-secondary">User Information</a>
        </p>
    </main>
</div>
```

## 3.実行！

indexページ

|<img width="636" alt="link_userinfo.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103886/5990ee64-3b42-725e-7114-8711de2502ef.png">|
|:--|

ユーザー情報ページ

|<img width="691" alt="userinfo.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103886/4e01475c-ceeb-cceb-9389-6f4a8ce8aac4.png">|
|:--|

体裁は、まあ^^;

scopeですが、KeycloakのClient設定で、[Client Scopes]の[Default Client scopes]から、`hello`をセットしておいたので、`scopes`にいますね。

ログアウトをクリックすると、indexに戻ります。で、またログインから実行できます。

## 4.このアクセストークンをリソースサーバーに投げてみる

これで、[前回](https://qiita.com/kasa_le/items/259792d3ae4df7f1d2b2)作成したリソースサーバーと同じレルムで動くクライアントが出来ました。
SSOでリソースサーバーにもアクセスできるはずです。
とりあえず手動で投げます。CurlとかPostmanで。

以下はPostmanの例です。

|<img width="885" alt="hello.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103886/096f39f9-b8a0-98e1-2faf-92e7919921af.png">|
|:--|

`scope`に`hello`がいるので、`/hello`はアクセスできます。
`/list`はアクセスできません。
[Default scopes]に`user`も追加すれば、見られるようになります。

# 感想
次は、このクライアントから、リソースサーバーのAPIを叩いて表示出来るようにしたいですね。いわゆるSSO(シングルサインオン)の実現です。
トークンのやり取りどうなるかしら？

それと、サーバー(tomcat)のアクセスログが全然見えなくて困ったので、以下のような設定を追加しました。

```applicatin.properties
server.tomcat.accesslog.enabled=true
server.tomcat.basedir=/dev
server.tomcat.accesslog.directory=stdout
server.tomcat.accesslog.suffix=
server.tomcat.accesslog.prefix=
server.tomcat.accesslog.file-date-format=
```

# サンプルプロジェクト

ここまでのサンプルプロジェクトは、以下にアップしてあります。
https://github.com/le-kamba/SpringSecurityAndKeycloakSamples

# 参考ページ

このサンプルのベースコードはこちらです。
http://www.javafoundation.xyz/2018/07/integrate-keycloak-with-spring-security.html

Spring Boot で Thymeleaf 使い方メモ
https://qiita.com/opengl-8080/items/eb3bf3b5301bae398cc2

アクセストークンを解析する方法の参考になりました。
https://stackoverflow.com/questions/48432626/get-the-accesstoken-of-keycloak-in-spring-boot

ログアウト後にindexページへリダイレクトする方法の参考になりました。
https://teratail.com/questions/187906