KeycloakとSpringBoot/SecurityでOpenID Connect(SSO編)

以下の記事のつづきです。
[KeycloakとSpringBoot/SecurityでOpenID Connect(リソースサーバー編)](https://qiita.com/kasa_le/items/259792d3ae4df7f1d2b2)
[KeycloakとSpringBoot/SecurityでOpenID Connect(クライアント編)](https://qiita.com/kasa_le/items/8db9f1da1895cee74aa9)

# 目標

Keycloakの同じレルムにクライアントアプリケーションと、リソースサーバーが出来たので、SSO(シングルサインオン)して相互に連携できることを目指します。

# 環境など

|ツールなど|バージョンなど|
|:--|:--|
| 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|

# 簡易フロー図

やろうとしていることのフローは、このようなものです。

![OIDC Flow.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103886/92d69ccd-9a30-48bb-2e1b-fc625ce286fe.png)


今、必要があって作っているのはリソースサーバーにあたる(?)RESTfulAPIサービスです。
ただ、この認証と認可のフローって、微妙に言葉が違うだけで解説を読んでいても混乱してしまうもの。やっぱり全体を作って動かして、理解するのがなんぼ。
ということで、手を動かしています。

# 用語

微妙に用語が異なっていてそれだけで混乱すると書いたばかりですが、ここでは、以下のように言い表します。

- クライアントアプリ=Relying Party(RP)
  - ユーザーが直接触るアプリです。または、その裏にあるサーバー側プログラム。
- IDプロバイダー
  - Keycloakですね。
- リソースサーバー
  - RESTfulAPIサービス。 RPとは現実には同じサービスかもしれないし、別のサービスかも知れない。ただ一応、分けて作ります。

# リソースサーバーの設定変更

Keycloak上では、どちらも(クライアントアプリもリソースサーバーも)Clientとして登録するのですが、リソースサーバーの方は、"bearer-only"に変更します。（ここは要件次第かな？)

|<img width="755" alt="bearer-only.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103886/a1429f05-e865-6b06-ce93-b815c18e95da.png">|
|:--|

# クライアントアプリを作る
[前回](https://qiita.com/kasa_le/items/259792d3ae4df7f1d2b2)のプロジェクトを編集するか、コピーしてきて名前を変えるなどすればいいでしょう。
私は`sso_demo`としました。
ディレクトリ名や、`pom.xml`の`artifactId`,`name`など忘れずに変更しましょう。 

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

前のクライアントアプリと同じでもいいですし、変えてもいいです。同時に動かすことはないので同じでもいいでしょう。
私は念の為変えて`9010`にしました。

```application.properties
server.port=9010
```

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

## 2.ページを追加

[リソースサーバー]()には、`/hello`ページがあるので、それに対応するページを作成します。

### (1)helloページの作成

```java
    @GetMapping("/hello")
    public String hello(Model model) {
        model.addAttribute("message", "Greeting for you!");
        return "hello";
    }
```

とりあえず固定の文字列を表示します。

```resource/templates/hello.html
<!doctype html>
<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
<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">
        <H1 th:text="${message}"></H1>
        </p>
    </main>
</div>

</body>
```

### (2)indexページにリンクを追加

毎回手動で`/hello`って入れるのも面倒なので、indexページにリンクを追加しましょう。

```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>
        <!-- Helloページ追加ここから -->
        <p class="lead">
            <a href="/hello" class="btn btn-lg btn-secondary">Greeting</a>
        </p>
        <!-- Helloページ追加ここまで -->

        <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>

</body>
```

### (3)Keycloak用の設定を追加

realmなどの設定を入れます。コピーしてきたなら入っているはずですが、`keycloak.resource=`の箇所を変えるのを忘れないでください。後でKeycloakに登録するクライアント名になります。

```application.properties
server.port=9010

spring.thymeleaf.mode=HTML

keycloak.enabled=true
keycloak.auth-server-url=http://localhost:8088/auth
keycloak.realm=myrealm
keycloak.resource=client2
keycloak.public-client=true

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=
```

### (4)ロールアクセス制御を追加

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

これで実行して、http://localhost:9010 から[Greeting]にアクセスすると、ログイン後、以下のようなページが開きます。

|<img width="671" alt="new_index_page.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103886/bb16c2fb-3f12-7d1a-1d68-66dc91d838c7.png">|
|:--|


|<img width="790" alt="hello_page.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103886/03d50169-8dca-8054-8e3e-a4d7a36cd6f3.png">|
|:--|

ページの準備ができました。
次は、リソースサーバーのエンドポイントにGETアクセスして、レスポンスを表示できるようにします。

## 2.REST API アクセス

### (1)コントローラーの変更

固定値を返しているのを、リソースサーバーの各エンドポイントを叩いてレスポンスをもらって入れるように変更します。

```SamplePageController.java

    RestTemplate restTemplate;

    private final String base_url = "http://127.0.0.1:8080";

    public ProductController(RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
    }

    @GetMapping("/hello")
    public String hello(Model model) {
        String url = base_url + "/hello";
        String response = restTemplate.getForObject(url, String.class);
        model.addAttribute("message", response);
        return "hello";
    }
    
    @GetMapping(path = "/products")
    public String getProducts(Model model){
        String url = base_url + "/list";
        ResponseEntity<List<String>> response = restTemplate.exchange(
                url,
                HttpMethod.GET,
                null,
                new ParameterizedTypeReference<>() {
                });
        List<String> products = response.getBody();
        model.addAttribute("products", products);
        return "products";
    }
    ...
```

- `RestTemplate restTemplate;`
  - SpringでRESTアクセスするのに使用するオブジェクト。コンストラクタインジェクションしています。
- `final String base_url`
  - リソースサーバーのurl。
  - `localhost`を使うと、KeycloackがDockerで動いているせいかうまく動かなかったので、IPアドレス指定にしています。
- `String response = restTemplate.getForObject(url, String.class);`
  - リソースサーバーの`/hello`エンドポイントからレスポンスをもらっています。
- `getProducts`
  - こちらは`List<String>`が返ってくるので、`restTemplate.getForObject`ではType指定が困難です。そのため、レスポンス全体をいったん直接取得し、`getBody`で値を取得しています。

### (2)KeycloakRestTemplateを使えるように設定

`KeycloakRestTemplate`はアクセストークンなどを自動的にヘッダーにつけてくれるそうです。
`RestTemplate`に代わってインジェクトされるようにします。

```SecurityConfig.java
@ComponentScan(basePackageClasses = KeycloakSecurityComponents.class)
@KeycloakConfiguration
public class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter {

    public final KeycloakClientRequestFactory keycloakClientRequestFactory;
    
    public SecurityConfig(KeycloakClientRequestFactory keycloakClientRequestFactory) {
        this.keycloakClientRequestFactory = keycloakClientRequestFactory;
    }

    // RestTemplateにかわってインジェクトされるようにする
    @Bean
    @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    public RestTemplate restTemplate(){
        return new KeycloakRestTemplate(keycloakClientRequestFactory);
    }
```

- `@ComponentScan(basePackageClasses = KeycloakSecurityComponents.class)`
  - インジェクションのために検索するパッケージを追加しています。

まだアクセスは出来ません。Keycloakに`client2`を追加しないと。

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

## 1.クライアントの登録
Keycloakの管理画面で、クライアントを追加します。
設定はほぼ前回のclient1と同じです。

|<img width="707" alt="client2.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103886/d02d767c-13cd-ea3b-c042-2a60b04a99ee.png">|
|:--|

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

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

|<img width="886" alt="client2_detail.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103886/460d026a-f020-6e36-4c1f-39cc8e8dae38.png">|
|:--|

[Save]を忘れずに。

## 2.クライアントにデフォルトスコープを追加

`hello`と`user`のスコープをデフォルトスコープに追加します。

|<img width="796" alt="client2_add_scopes.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103886/dd9025a6-a5b4-0a77-be27-e8951471157e.png">|
|:--|

# リソースサーバーにアクセス！

リソースサーバーを立ち上げます。
今回作ったクライアントを立ち上げて、`/hello`にアクセスします。
ログイン後、次のように表示されれば通信が成功です。

|<img width="872" alt="hello_rest.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103886/3263ebc9-334f-beb1-5aa4-64c53acfdb34.png">|
|:--|

`/products`にアクセスすると、ちゃんとリストが表示されるはず。

クライアント<->リソースサーバー<->IDプロバイダー(Keycloak)が実装できました！
最初の方に貼ったフロー図の動きになっているはずです(あれを作ったつもり^^;)


# 感想

これで、OpenId Connectの認証・認可のフローを全部実装できました。
やっぱり説明を読んでいるだけでは、概要やイメージは分かっても、「このパラメータをこうしたい」という話になったときに「あのフローのこの部分のアレだな」というのがとっさにわかりません。

ところで・・・これだけのことをやってくれるKeycloak。
IDプロバイダーを自前実装するというのは、やっぱりかなりハードル高いですよねえ・・・
Keycloakのまだ深い設定などいじってないですが、それでも、Keycloak自身をフォークしてどうこうするのも、そうとう大変そうな印象です。

途中途中でなにか特殊な仕様でどうにかしたい場合は、リバースプロキシのようなことで対応するほうが楽かもしれません。
それから、APIゲートウェイというのもありますよね。
これらもうまく組み合わせることで、自前で実装しなければならない部分は局所的にしていくほうが、今の時代に合ったアプローチかもしれません。

それと、しょっぱなから連携するものを作ろうとして、結局動かせず、一つずつ作ることにしてやっとうまくいきました。
何事も、ミニマムからやらないとね。

# サンプルプロジェクト

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

# 参考サイト

RestTemplateのBean登録方法の参考に。
https://stackoverflow.com/questions/36151421/could-not-autowire-fieldresttemplate-in-spring-boot-application

Keycloak 4とSpring Boot 2アプリの連携 - リソースサーバー
https://www.yo1000.com/keycloak4-springboot2-resource/

リソースサーバーのurlに`localhost`を使ってエラーになっていたときの対処の参考になりました。
https://github.com/jenkinsci/oic-auth-plugin/issues/35

RestTemplateを使ってオブジェクトのリストを取得および投稿する
https://www.codeflow.site/ja/article/spring-rest-template-list