KeycloakとSpringBoot/SecurityでOpenID Connect(リソースサーバー編)

# 目標
KeycloakをID Providerとして、RESTfulAPIが動くリソースサーバーを作る。

本当は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|

# Keycloakの準備

Keycloakを実行するのに、Dockerから使います。
こちらなどが参考になります。

公式のチュートリアル
https://www.keycloak.org/getting-started/getting-started-docker

DockerとKeycloakで世界最速OpenID Connect！！
https://tech-lab.sios.jp/archives/19319

ただ、localhostのポート番号がSpringのサンプルアプリとかぶると困るので、ポート番号を変えておきます。
コマンドは次のようになります。

```console
$ docker run -p 8088:8088 -e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD=admin quay.io/keycloak/keycloak:9.0.3 -Djboss.http.port=8088
```

`-p`オプションの後だけ変更するのでは足りません。`-Djboss.http.port`での指定も必要になります。
AdminLogin用のURLは以下のようになります。

http://localhost:8088/auth/admin

あとは、上記の参考ページに従って、クライアントアプリの登録と、ユーザーアカウントの登録を行っておきます。

最後にサンプルクライアントアプリの登録で、https://www.keycloak.org/app/ につなぎますが、その時もポート番号を変更するのを忘れないでください。その後の動きが変なことになります。

<img width="823" alt="sample_app.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103886/a9fe2896-ac3f-a493-835e-62a9390df1b8.png">

これから作るサンプルアプリケーションの登録はまた後ほどやります。


# Spring Bootアプリケーションの準備

## 1.雛形の作成
[Spring Initializer](https://start.spring.io/)のサイトで雛形を作成します。
Dependenciesにはいったん`Spring Web`だけ設定します。

|<img width="1003" alt="spring_initializer.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103886/46774831-9204-66cc-a46f-43c9c734d18a.png">|
|:--|

**Project Metadata**はお好みのものに変えてください。

[GENERATE]をクリックするとプロジェクトをダウンロードできます。
解凍し、IntelliJで[Import Project]します。

## 2.とりあえず実行

なにも考えずに、そのままデバッグボタンを押します。

```console
  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.2.6.RELEASE)

2020-04-23 19:03:06.048  INFO 57394 --- [           main] com.example.demo.DemoApplication         : Starting DemoApplication on kanedev.local with PID 57394 (/Users/myuser_name/Documents/workspace/boot_demo/target/classes started by myuser_name in /Users/myuser_name/Documents/workspace/boot_demo)
2020-04-23 19:03:06.050  INFO 57394 --- [           main] com.example.demo.DemoApplication         : No active profile set, falling back to default profiles: default
2020-04-23 19:03:06.845  INFO 57394 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)
2020-04-23 19:03:06.879  INFO 57394 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2020-04-23 19:03:06.880  INFO 57394 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet engine: [Apache Tomcat/9.0.33]
2020-04-23 19:03:06.974  INFO 57394 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2020-04-23 19:03:06.975  INFO 57394 --- [           main] o.s.web.context.ContextLoader            : Root WebApplicationContext: initialization completed in 893 ms
2020-04-23 19:03:07.190  INFO 57394 --- [           main] o.s.s.concurrent.ThreadPoolTaskExecutor  : Initializing ExecutorService 'applicationTaskExecutor'
2020-04-23 19:03:07.469  INFO 57394 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2020-04-23 19:03:07.474  INFO 57394 --- [           main] com.example.demo.DemoApplication         : Started DemoApplication in 1.685 seconds (JVM running for 7.407)
```

なんかこんなログが出ればOK.

## 3.REST APIを作成

とりあえずGETでなんか返すのを作ります。

```ProductController.java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
public class ProductController {
    private final ProductService productService;

    @Autowired
    public ProductController(ProductService productService) {
        this.productService = productService;
    }
    
    @GetMapping(path = "/list")
    public List<String> getProducts() {
        return productService.getProducts();
    }
}
```

```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");
    }
}
```

## 4.実行

実行して、ブラウザなどから`127.0.0.1:8080/list`にアクセスします。
以下のように表示されればOK.

|<img width="325" alt="first_run.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103886/d0e9f30b-b2df-7d9d-6a6f-82b4619e02a0.png">|
|:--|

# Spring Securityを導入

## 1.依存関係の追加

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

これだけで実行してみると・・・

<img width="567" alt="login.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103886/9b7889ab-aa13-b4f4-8d25-6455bdbabfc0.png">

何も作ってないのにログインページが表示されました:open_mouth:
今は何を入れてもエラーになります。
IDプロバイダーの設定をしていないので当然ですね。

## 2. application.propertiesの設定

以下のように設定します。

```application.properties
# 認可サーバーのIssuer Identifier
spring.security.oauth2.resourceserver.jwt.issuer-uri= http://localhost:8088/auth/realms/myrealm
```

この状態で再起動すると・・・

|<img width="656" alt="401.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103886/e0afc9e8-e06c-55f3-3044-12443de74f2f.png">|
|:--|

401が返ってきました。
`401=Unauthorized`ですね。だから今はこの状態で、正常な動作です。

リソースサーバーの設定はここまでです。

# 認証アクセス

認可してもらうようにするためにKeycloakにアプリを登録しアクセストークンを取得し、それを使ってアクセスを試します。

## 1.Keycloakにクライアントアプリを追加
Keycloakのadmin管理画面から、Clientを追加します。

<img width="741" alt="add_sclient_boot.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103886/6f712310-9dfc-63a2-e14a-c8840c032ce7.png">

- **[Access Type]**は**Confidential**にします。
- **[Authorization Enabled]**を**ON**にします。
  - 付随して他にもONになるものがあります。

|<img width="1039" alt="confidential.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103886/c4cda1ef-e9fc-ef8c-0f89-1c3a4d887568.png">|
|:--|

それ以外はデフォルトのままで大丈夫なはずです。
[Save]します。

## 2.Client Secretsの取得

Keycloakのadmin管理ページで、クライアント設定画面を開き、タブの[Credentials]をクリックします。

表示されている**Secret**をコピーしておきます。

|<img width="739" alt="credential.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103886/6d96f115-6385-7342-5ede-8bc951236239.png">|
|:--|

## 3.アクセストークンを取得
### (1)エンドポイントからアクセストークンを取得
クライアントアプリを作成するなら、ログイン画面を出すようにリダイレクトでいいのですが、今回、リソースサーバーはクライアントアプリとは別に動作するという前提で作っています。なので、ここはアクセストークンを受け取ることだけ考えます。

そこで、アクセストークン発行エンドポイントをcurlなどで叩いて、取得します。

curlで以下のコマンドを叩きます。

```console
$ curl -X POST "http://localhost:8088/auth/realms/[クライアントを作ったrealm名]/protocol/openid-connect/token" \
--data "grant_type=client_credentials&client_secret={さっきコピーしたsecret}&client_id=boot-sample"
```

Postmanだとこうします。

|<img width="992" alt="token_postman.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103886/5dd850d5-fd64-d136-4a49-699b3a4416b9.png">|
|:--|

こんなJsonが返ってくるかと思います。

```json
{
    "access_token": "eyJhbGciOiJSUzI1....",
    "expires_in": 600,
    "refresh_expires_in": 1800,
    "refresh_token": "....",
    "token_type": "bearer",
    "not-before-policy": 0,
    "session_state": "5b53443e-67fb-457d-9540-5ae58bb2ed1b",
    "scope": "profile email"
}
```

`"access_token"`の中身をコピーしておきます。(長いよ)

### (2)アクセストークンの中身を見る

https://jwt.io/ などでアクセストークンの中身を見ることが出来ます。コピーした`"access_token"`を貼り付け、ふむふむと思っておきましょう。

## 4.アクセストークンを指定して実行

curlコマンドなどで先ほどコピーしたアクセストークンを使います。

```console
$ curl -v -X GET -H "Authorization: Bearer 先ほどコピーしたアクセストークン" http://localhost:8088/list
```

Postmanの例。

|<img width="988" alt="postman_with_token.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103886/f9ff8b7d-d73c-d4e3-5f03-382737b66dac.png">|
|:--|

または、[Authorization]タブにアクセストークンをセットします。

|<img width="978" alt="postman_auth.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103886/af2b44b8-6c38-de50-0479-108e7223bc27.png">|
|:--|

jsonが取得できるようになりました！

ここまで、Keycloakとの連携は、`application.properties`に書いただけです。
それと、依存ライブラリに`spring-boot-starter-oauth2-resource-server`を追加しただけです。
これだけで、受け取ったアクセストークン(JWT)の検証したりいろいろしてくれちゃう(※)んですね。意外でした。

※トークンが期限切れで401になることがよくあったので、やっているものと認識しました。それとも、自前でJWTの検証もすべきなんでしょうか？やるならオフライン認証かなあと思っていますが・・・

# スコープによるアクセス制御
先程は、Client Secretでアクセストークンをもらいました。
つまり、登録されたアプリから発行されたアクセストークンならばなんでもどこでもアクセスが許可されます。
せっかくなので、アクセス制御方法も見てみましょう。

OpenID Connectではスコープを複数指定できますね。ユーザーの属性(Claimと言うのかな？)を任意でアクセストークンに含めることが出来ます。
このスコープを使って、アクセスできる/出来ないAPIを作ってみます。

## 1.スコープマッピングをリソースサーバーに設定する

Springプロジェクトのリソースサーバーに、以下のクラスを追加します。

```SecurityConfig.java
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers(HttpMethod.GET, "/hello").hasAuthority("SCOPE_hello")
                .antMatchers(HttpMethod.GET, "/list").hasAuthority("SCOPE_user")
                .antMatchers(HttpMethod.GET, "/").permitAll()
                .anyRequest().authenticated();
        http.oauth2ResourceServer()
                .jwt();
    }
}
```

エンドポイントを3つ用意して、それぞれスコープが許可されている場合、されていない場合としてみました。

- `/hello`パス
  - `hello`スコープを持ったアクセストークンでのみアクセス可能
- `/list`パス
  - `user`スコープを持ったアクセストークンでのみアクセス可能 
- `/`(index)パス
  - すべてのアクセスを許可 
  
スコープ名の指定ですが、`"SCOPE_"`というprefixが必要だそうです。
 
続いて、`HelloContoroller`と`IndexController`をそれぞれ適当に作成します。

```HelloController.java
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {

    @GetMapping("/hello")
    public String hello() {
        return "Hello!";
    }
}
```

```IndexController.java
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class IndexController {

    @GetMapping("/index")
    public String index() {
        return "Welcome to Sample Page!!";
    }
}
```

## 2.Keycloakでスコープを設定する

`hello`や`user`というスコープを追加する必要があります。
なお、追加するのが面倒な人は、定義済みのスコープを使ってもいいでしょう。

### (1)Client Scopeの追加
- Keycloakの管理画面で、[Client Scopes]を選びます。
- 右上の[create]をクリック

|<img width="861" alt="create_scope.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103886/b16f991a-7e81-f602-3f8e-5ab92d642864.png">|
|:--|

- **Name**などに任意の設定をする
- **Include In Token Scope**を**ON**にする

|<img width="593" alt="config_scope.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103886/cc840178-30f9-e099-9948-467e618e6faa.png">|
|:--|

上記の図では、`user`スコープを作っています。
最後に[Save]してください。

同様にして、`hello`スコープも作ります。

### (2)マッパーを作成

- 作成したスコープを[Client Scopes]からクリック
- [Mappers]タブをクリック
- [Create]をクリック

|<img width="869" alt="create_mapper.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103886/dd81b90f-6772-4627-fda9-da1fcecf8a15.png">|
|:--|

- **Name**は任意
- **Mapper Type**は**User Attribute**を選択
- **User Attribute**に入力
  - 任意のものでよいが後で使うので覚えておく
- **Token Claim Name**に入力
  - アクセストークンなどに含まれるときのJsonの名前です
- **Claim JSON Type**は任意
  - あとで各ユーザーにこのタイプのものを指定するので覚えておく

Hello Mapperはこうしました。

|<img width="717" alt="hello-mapper.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103886/cf9b4633-581a-e1cb-46e3-ff489b1b026b.png">|
|:--|

User Mapperはこうしました。

|<img width="672" alt="user_mapper.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103886/9bff83e5-c73a-903e-4d5f-a1f884d8837b.png">|
|:--|

[Save]を忘れないで！

### (3)クライアントにスコープを設定する

- [Clients]から対象のクライアントを選ぶ
- [Client Scopes]タブをクリック
- **Optional Client Scopes**にあるスコープを選択して、[Add Selected]をクリック

|<img width="738" alt="client_optional_scope.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103886/a09203fe-4557-8ba0-7f9a-d848b2efbbc6.png">|
|:--|


## 3.アクセストークンを再発行する

`scope`に`openid`と、`hello`または`user`を追加して、アクセストークンを再発行します。
※`openid`は必須。

Postmanだとこうです。

|<img width="914" alt="renew_access_token.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103886/07c07467-23b4-2a14-8ee5-df49302fed46.png">|
|:--|


## 4.各ページアクセス

### (1)ルートページへのアクセス

- `http://127.0.0.1:8080/`へブラウザからアクセスしてみます。
`/`へのマッピングは、`permitAll`ですから、アクセストークンがなくても誰でもアクセスできます。
だから、ブラウザに結果がちゃんと表示されます。

それ以外のページは、`401`(Unauthorized)になるはずです。

### (2)`hello`ページへのアクセス

以下のような結果になるはずです。

- ブラウザからのアクセス、アクセストークンを指定しない場合
  - `401`(Unauthorized)
- アクセストークンの`scope`に`hello`が含まれる場合
  - アクセス可能(`"Hello"`が表示される)
- アクセストークンの`scope`に`hello`が含まれ**ない**場合
  - `403`(Forbidden)

### (3)`/list`ページへのアクセス

- ブラウザからのアクセス、アクセストークンを指定しない場合
  - `401`(Unauthorized)
- アクセストークンの`scope`に`user`が含まれる場合
  - アクセス可能(Jsonが表示される)
- アクセストークンの`scope`に`user`が含まれ**ない**場合
  - `403`(Forbidden)

# ユーザーアクセス

ここまで、Client Secretによるアクセストークンを発行していましたが、ユーザーごとに発行してみます。
ユーザーのロール制御については今回は見送りますが、時間があればそのうちやります。

## 1.Keycloakにユーザーを追加する

もしKeycloakのチュートリアルで追加していなければ、adminでKeycloakにログインして、[Users]から[Add user]してください。
詳しくはこちらを参照してください。

https://www.keycloak.org/getting-started/getting-started-docker

パスワード設定では、**Temporary**を必ずOFFにして、右上の[Save]ボタンを押してください。

|<img width="855" alt="temporary_off.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103886/0c174ccd-768f-7769-b153-d7b74beecf70.png">|
|:--|

## 2.ユーザーに属性を追加する
先程、スコープを作ったときに、**User Attribute**というのを指定したと思います。
私の場合は、

- hello
- listuser

でした。これらの属性の値をユーザーにセットします。

- 作成したユーザーを選び、[Attributes]タブをクリック
- Key : 属性名
  - スコープ作成時に指定した**User Attribute**の値
- Value : `任意の文字列`
  - スコープ作成時に指定した**Claim JSON Type**で設定すること
- [Add]をクリック
- [Save]をクリック

`hello`と`listuser`をセットしました。

|<img width="756" alt="user_attr.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103886/75c243ba-0358-904d-6051-a1dbe024091d.png">|
|:--|

## 3.ユーザーのアクセストークンを発行する

先ほど作成したのはクライアントのサービスアカウントに対するアクセストークンでしたが、今回はユーザーごとのアクセストークンを取得して試します。

スコープの指定は、`openid hello`か、`openid scope`とします。

```console
$ curl -X POST "http://localhost:8088/auth/realms/[クライアントを作ったrealm名]/protocol/openid-connect/token" \
--data "grant_type=password&username={作ったユーザー名}&password={ユーザーのパスワード}client_secret={さっきコピーしたsecret}&client_id=boot-sample&scope=openid hello"
```

Postmanだとこうです。

|<img width="968" alt="user_access_token.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103886/5c6e2aa4-c867-e6d8-3290-39cbe94e26c8.png">|
|:--|

返されたJsonを眺めると、`scope`にちゃんと指定した物があるのがわかります。
`profile`と`email`はデフォルトで付いてきちゃうようです。

そして、アクセストークン(JWT)を検証すると、ちゃんとそこにも

```json
"scope": "openid hello profile email",
```

というのがいるのがわかります。

ユーザー(またはクライアント)がこのスコープでアクセスしてきたらGETはOK。別のスコープだったらCREATEは不可。
こんなアクセス制御に使えますね。

ちなみに、`Access Token`の中身は、Keycloakの管理画面でも見られます。

- [Clients]から該当のクライアントを選び、[Client Scopes]をクリック後、[Evaluate]をクリック
- **Client Scopes**を選ぶ'(任意)
- **User**を選ぶ
  - 追加済みユーザーのusernameを入れる必要あり(入力を始めると候補が出る)
- [Evaluate]をクリック
- [Generated Access Token]をクリック

|<img width="885" alt="evaluate.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103886/fab7b04c-4864-c765-6a9a-4a56fb356be8.png">|
|:--|

userinfoのエンドポイントを叩くと、ユーザー情報を取得でき、スコープに応じて、`hello`と`list_user`の項目があるはずです。

以下は、`scope=openid hello user`として得たアクセストークンでuserinfoエンドポイントを叩いた結果です。

|<img width="949" alt="userinfo.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103886/d9ea03a9-f9ca-8e84-e334-942471cca170.png">|
|:--|


# 感想
実装より、Keycloakの設定に苦労した感じです^^;
コードはほとんど書いてないですからね。

本当はレルムのロールによるユーザーアクセス制御もしたかったのですが、ちょっと長くなったので一旦区切ります。

それと、毎回アクセストークンを手動で発行してコピペするのも大変なので、クライアントアプリも作って結合した状態で流してみたいですね。リフレッシュトークンも対応しなきゃですが。

Bootを使わないで書く方法は、任せた（誰に)

# サンプルプロジェクト

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

# 参考サイト

Dockerで起動するKeycloakのポート番号変更の参考になりました。
https://stackoverflow.com/questions/57430811/changing-default-port-of-keycloak-in-docker

Spring Bootアプリ(クライアント)の設定の参考になりました。
http://www.javafoundation.xyz/2018/07/integrate-keycloak-with-spring-security.html

リソースサーバーの作り方の参考になりました。
https://qiita.com/suke_masa/items/0f75fa75a22a6551065b

アクセストークンの取得方法の参考にしました。
https://qiita.com/ryou56/items/4c6fa618d1f6b960ba71

スコープを追加する参考になりました。
https://www.janua.fr/using-client-scope-with-redhat-sso-keycloak/
https://qiita.com/rawr/items/d4f45e094c39ef43cbdf