Nextcloud + KeycloakでSAML認証する in ラズパイ4

# はじめに
株式会社ピー・アール・オーのアドベントカレンダー22日目は、
表題の通り、NextcloudとKeycloakを組み合わせてファイルサーバを構築したのでその手順を書き残す。

## 背景
ツーリング中にドラレコやGoProで撮影した動画を友人内で共有するにあたり、
SFTPを使用してファイル共有をしていた。
(1ファイル4GBくらいあるので、GoogleDriveはあっという間に容量超過した)
さすがにパスワード認証は論外なので公開鍵認証を使用していたところ、以下の運用上の問題が発生した。

- 秘密鍵を失くすケース(まあわかる)
- 接続手順を忘れるケース(彼は本当にエンジニアなのだろうか)

失くされる度に公開鍵を登録し直すのは億劫かつ、こんなことのために手順書を用意するのも馬鹿らしいので、
簡単に使えるファイルサーバを立ち上げることにした。

## 目的

### SAML認証の目的
普段よく使うサービス、それこそGoogleアカウントなら忘れないだろうし、
忘れても自分でパスワード再発行してよしなにやってもらえるため。
~~本当はopenid connectでやりたかったが、外部idp(google)を挟むと認証後Nextcloudが例外吐くので断念。Keycloak単体だと普通に動くんだけど。。。~~

### Nextcloudを使用する目的
オープンソースのオンラインストレージを検索すると引っかかったのはownCloudとNextcloudの2つ。どちらもDropboxライク。
残念なことにownCloudは有償版のみSAML対応であったため、Nextcloudを使用することにした。
[詳しくは公式サイトでも見て下さい。](https://nextcloud.com/)

### Keycloakを使用する目的
- 今後、思い付きで別サービスを立ち上げるかもしれないため、認証を統合したかった。
- Nextcloud上でID管理をしたくなかった。
[Keycloakの詳細はこれとかを見てください。](https://www.atmarkit.co.jp/ait/series/7363/)

# 環境

| 名称       | バージョンなど                 | 備考                     |
| ---------- | ------------------------------ | ------------------------ |
| OS         | Raspbian GNU/Linux 10 (buster) | ラズパイ4                |
| Keycloak   | 10.0.1                          | ID管理/認証              |
| Nextcloud  | 18.0.4                         | ファイルサーバ           |
| Postgresql | 12.1                           | Nextcloud & Keycloak用DB |
| nginx      | 1.14.2                         | リバプロ                 |

# 流れ

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531974/3c17a073-bddd-d646-348f-b09fd2b04c30.png)

# 構築

## 各種ミドルウェアの準備
docker-composeはpipで入れた。

```bash
$ sudo apt-get install -y nginx
$ sudo curl -fsSL https://get.docker.com/ | sh
$ sudo apt-get -y install python3-dev python3-pip
$ sudo pip3 install docker-compose
```

Nextcloud、Keycloak、PostgresqlはDockerで入れた。
Keycloakはarmで動くいい感じのイメージが見つからなかったので、Dockerfile書いてビルドした。

```bash
$ docker build -t me/keycloak .
```

```dockerfile:Dockerfile
FROM balenalib/raspberry-pi-openjdk:8-stretch

ENV KEYCLOAK_VERSION 10.0.1
ENV JDBC_POSTGRES_VERSION 42.2.12
ENV JBOSS_HOME /data/keycloak

USER root

RUN mkdir /data
WORKDIR /data

RUN curl -LO https://downloads.jboss.org/keycloak/${KEYCLOAK_VERSION}/keycloak-${KEYCLOAK_VERSION}.tar.gz
RUN tar xfp keycloak-${KEYCLOAK_VERSION}.tar.gz --no-same-owner
RUN mv ${JBOSS_HOME}-${KEYCLOAK_VERSION} ${JBOSS_HOME}
RUN rm ${JBOSS_HOME}-${KEYCLOAK_VERSION}.tar.gz
RUN chmod +x ${JBOSS_HOME}/bin/standalone.sh

RUN mkdir ${JBOSS_HOME}/cli
RUN mkdir -p ${JBOSS_HOME}/standalone/deployments/

RUN curl -LO https://jdbc.postgresql.org/download/postgresql-${JDBC_POSTGRES_VERSION}.jar
COPY keycloak-add-user.json ${JBOSS_HOME}/standalone/configuration/keycloak-add-user.json
COPY config.cli ${JBOSS_HOME}/cli/config.cli
RUN sed -i -e "s/{{JDBC_POSTGRES_VERSION}}/${JDBC_POSTGRES_VERSION}/g" ${JBOSS_HOME}/cli/config.cli

RUN ${JBOSS_HOME}/bin/jboss-cli.sh --file=${JBOSS_HOME}/cli/config.cli
RUN rm -rf ${JBOSS_HOME}/standalone/configuration/standalone_xml_history

EXPOSE 8080

CMD [ "/bin/bash", "/data/keycloak/bin/standalone.sh", "-b", "0.0.0.0" ]
```

- 設定ファイルについて

| 設定ファイル名         | 設定内容                                                |
| ---------------------- | ------------------------------------------------------- |
| config.cli         | jboss-cliのコマンドを記述したファイル                                        |
| keycloak-add-user.json | adminユーザの作成                                       |

- config.cli

jboss-cliを実行し、各設定を行っている。
やっていることは以下。
1. JDBCドライバのインストール
2. DataSourceの設定
3. Keycloakのhttpsリダイレクト設定(リバプロをかませるため)
4. Keycloak管理コンソールのアクセス制御

:information_source: **ちなみに**
CLIで`module add`を実施するのは非推奨らしいが、今回はやっちゃう。

```cli:config.cli
embed-server --server-config=standalone.xml --std-out=echo
module add --name=org.postgresql --resources=postgresql-{{JDBC_POSTGRES_VERSION}}.jar --dependencies=javax.api,javax.transaction.api
/subsystem=undertow/server=default-server/http-listener=default: write-attribute(name=proxy-address-forwarding, value=true)
/subsystem=datasources/data-source=KeycloakDS: remove()
/subsystem=datasources/data-source=KeycloakDS: add(jndi-name=java:jboss/datasources/KeycloakDS,enabled=true,use-java-context=true,use-ccm=true, connection-url=jdbc:postgresql://${env.DB_ADDR:postgres}:5432/${env.DB_DATABASE:keycloak}${env.JDBC_PARAMS:}, driver-name=postgresql)
/subsystem=datasources/data-source=KeycloakDS: write-attribute(name=user-name, value=${env.DB_USER:keycloak})
/subsystem=datasources/data-source=KeycloakDS: write-attribute(name=password, value=${env.DB_PASSWORD:password})
/subsystem=datasources/data-source=KeycloakDS: write-attribute(name=check-valid-connection-sql, value="SELECT 1")
/subsystem=datasources/data-source=KeycloakDS: write-attribute(name=background-validation, value=true)
/subsystem=datasources/data-source=KeycloakDS: write-attribute(name=background-validation-millis, value=60000)
/subsystem=datasources/data-source=KeycloakDS: write-attribute(name=flush-strategy, value=IdleConnections)
/subsystem=datasources/jdbc-driver=postgresql:add(driver-name=postgresql, driver-module-name=org.postgresql, driver-xa-datasource-class-name=org.postgresql.xa.PGXADataSource)
/subsystem=keycloak-server/spi=connectionsJpa/provider=default:write-attribute(name=properties.schema,value=${env.DB_SCHEMA:public})
/subsystem=undertow/configuration=filter/expression-filter=ipAccess:add(,expression="path-prefix[/auth/admin] -> ip-access-control(acl={'192.168.0.0/24 allow'})")
/subsystem=undertow/server=default-server/host=default-host/filter-ref=ipAccess:add()
stop-embedded-server
```

- keycloak-add-user.json

```json:keycloak-add-user.json
[ {
  "realm" : "master",
  "users" : [ {
    "username" : "admin",
    "enabled" : true,
    "credentials" : [ {
      "type" : "password",
      "secretData" : "{\"value\":\"3MLtOqxjN3tKhaXmIwSQhTLBCdWHf1FvsdeHibw49glSj5Gj7L/8Dac7P399mJlpsOJ/20Wr3OmWBA+E/BzqhA==\",\"salt\":\"eLxa1xVzxfsGu7qlYsZosg==\"}",
      "credentialData" : "{\"hashIterations\":100000,\"algorithm\":\"pbkdf2-sha256\"}"
    } ],
    "realmRoles" : [ "admin" ]
  } ]
} ]
```

起動はdocker-composeを使用する。
全部同じネットワークに乗っけて、ローカルからのみアクセスOKとしている。
また、コンテナ立ち上げ時にNextcloud、KeycloakのDBをそれぞれ作成するため、`initdb`ディレクトリにSQLを放り込んでおく。
見よう見まねで書いているので、質についてはなんともいえない。

```yml:docker-compose.yml
version: '3'

services:
  postgres:
      image: postgres
      container_name: 'postgres'
      volumes:
        - ./postgres_data:/var/lib/postgresql/data
        - ./initdb:/docker-entrypoint-initdb.d
      ports:
      - "127.0.0.1:5432:5432"
      environment:
        POSTGRES_USER: postgres
        POSTGRES_PASSWORD: password
      restart: always
      network_mode: bridge
  nextcloud:
      image: nextcloud
      container_name: 'nextcloud'
      volumes:
        - ./nextcloud:/var/www/html
      ports:
      - "127.0.0.1:10443:443"
      - "127.0.0.1:10080:80"
      restart: always
      environment:
      - POSTGRES_HOST=postgres
      - POSTGRES_DB=nextcloud
      - POSTGRES_USER=nextcloud
      - POSTGRES_PASSWORD=password
      - NEXTCLOUD_ADMIN_USER=admin
      - NEXTCLOUD_ADMIN_PASSWORD=password
      - NEXTCLOUD_TRUSTED_DOMAINS=cloud.example.com
      - OVERWRITEHOST=cloud.example.com
      - OVERWRITEPROTOCOL=https
      links:
      - postgres
      network_mode: bridge
  keycloak:
      image: me/keycloak
      container_name: 'keycloak'
      volumes:
          - ./deployments:/data/keycloak/standalone/deployments/
      restart: always
      links:
      - postgres
      ports:
      - 127.0.0.1:8080:8080
      network_mode: bridge
```

- initdbに格納するSQL

```sql:init.sql
CREATE USER keycloak WITH PASSWORD 'password';
CREATE DATABASE keycloak OWNER keycloak;
CREATE USER nextcloud WITH PASSWORD 'password';
CREATE DATABASE nextcloud OWNER nextcloud;
```

## リバースプロキシの設定
Dockerにプロキシする設定。
以下のようなconfファイルを作成し、`/etc/nginx/conf.d`に放り込む。
http ⇒ httpsのリダイレクトは省略。

```conf:proxy.conf
### nextcloud ###
server {
    listen 443;
    client_max_body_size 10G;
    server_name cloud.example.com;
    ssl on;
    ssl_certificate /etc/nginx/cert/privatekey.pem;
    ssl_certificate_key /etc/nginx/cert/server.crt;

    location / {
        proxy_pass http://localhost:10080;
        proxy_redirect http:// https://;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-Proto https;
        proxy_set_header X-Forwarded-Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}
### keycloak ###
server {
    listen 443;
    server_name sso.example.com;
    ssl on;
    ssl_certificate /etc/nginx/cert/privatekey.pem;
    ssl_certificate_key /etc/nginx/cert/server.crt;

    location / {
        proxy_pass http://localhost:8080;
        proxy_redirect http:// https://;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-Proto https;
        proxy_set_header X-Forwarded-Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}
```

## SAML署名鍵 & 証明書の作成
SAMLで使用する鍵と証明書を作成しておく。
DN項目はCN含めすべて空でよい。

```
$ openssl req  -nodes -new -x509 --days 10000  -keyout private.key -out public.crt
```

## 起動
この後の設定は各システムのGUIで行うので立ち上げる。

```bash
$ docker-compose up -d
```

# Keycloakの設定 - レルム作成

`https://sso.example.com/auth`にアクセスすると、ウェルカムページが表示される。
`Administration Console`を選択し、管理コンソールにログインする。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531974/70818616-68da-52da-54ef-2f78c738e405.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531974/a92cad0e-4b30-18cd-1076-8c5a0436f17a.png)

## レルムの作成
レルムの追加から、`nextcloud`レルムを作成する。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531974/9311b316-f3b3-e5f7-8b57-54b10f0ae769.png)

## レルム証明書の取得  
レルムの設定 ⇒ `鍵`から、アルゴリズム`RS256`の証明書を表示し、コピーしておく。  
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531974/065693f0-ddb2-9ffa-584c-f1f2c734c864.png)

# Nextcloudの設定
AdminユーザでNextcloudにログインし、右上のアイコンから`アプリ` ⇒ `連携` から `SSO & SAML authentication`をインストールする。  
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531974/371cc17a-cee2-dcf6-d97b-fa614374a1c6.png)

## SSOとSAML認証の設定
右上のアイコンから`設定` ⇒ 左メニューの`SSOとSAML認証`から設定画面に入り、
`Identity providerを追加する`を押して設定を作成する。
設定内容は以下。  

- 一般

| 設定項目                       | 設定内容 |
| ------------------------------ | -------- |
| UIDをマップする属性            | username |
| IPプロバイダオプションの表示名 | お好みで |

- Service Providerデータ

| 設定項目                                             | 設定内容                                                       |
| ---------------------------------------------------- | -------------------------------------------------------------- |
| 名前IDの形式                              | 指定なし               |
| サービスプロバイダのX.509 証明書                              | 先ほど作った`public.crt`をそのまま貼り付ける               |
| サービスプロバイダーの秘密鍵 | 先ほど作った`private.key`をそのまま貼り付ける |

- Identity Providerデータ

| 設定項目                                             | 設定内容                                                       |
| ---------------------------------------------------- | -------------------------------------------------------------- |
| IdPエンティティの識別子                              | `https://Keycloakのホスト名/auth/realms/レルム名`               |
| SPが認証要求メッセージをを送信するIdPのURIターゲット | `https://Keycloakのホスト名/auth/realms/レルム名/protocol/saml` |

- オプションのIdentity Provider設定

| 設定項目                                             | 設定内容                                                                   |
| ---------------------------------------------------- | -------------------------------------------------------------------------- |
| SPが認証要求メッセージをを送信するIdPのURIターゲット | `https://Keycloakのホスト名/auth/realms/レルム名/protocol/saml`             |
| IdPの公開X.509証明書                                 | `-----BEGIN CERTIFICATE-----`さっきKeycloak上で控えた証明書`-----END CERTIFICATE-----` |

- 属性マッピング

| 設定項目                           | 設定内容 |
| ---------------------------------- | -------- |
| 表示名をにマップする属性(原文ママ) | username |
| 電子メールアドレスをマップする属性 | email    |

- セキュリティ設定

以下にチェックを入れる。  

| 設定項目                           |
| ---------------------------------- |
| このSPによって送信された <samlp:AuthnRequest> メッセージが署名されるかどうかを示します。[SPのメタデータがこの情報を提供する] |
| このSPによって送信された  <samlp:logoutRequest> メッセージが署名されるかどうかを示します。 |
| このSPによって送信された <samlp:logoutResponse> メッセージが署名されるかどうかを示します。 |
| このSPが受信した<samlp:Response>、<samlp:LogoutRequest>、および<samlp:LogoutResponse>要素が署名されるための要件を示します。 |
| ここのSPによって受信された<saml:Assertion>要素が署名されるための要件を示します。 [SPのメタデータはこの情報を提供する] |

設定完了後、最下部のボタンから、`メタデータXML`をダウンロードする。

- 設定例
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531974/4f2e5b27-6792-6e2e-e4c7-929736d47a0a.png)


# Keycloakの設定 - クライアント設定

## クライアントの作成
SAML用クライアントを新規作成する。
ここで、`インポート`を選択し、先ほどNextcloud上でダウンロードした`メタデータXML`を選択する。
すると、以下画面のように設定項目が自動的に投入される。

- 設定例
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531974/8491e49f-d54b-55c7-b0e0-4442d84fbcaa.png)

## クライアントの設定
クライアントを保存すると設定画面に移動するため、そのまま設定する。

### 設定
以下の設定項目のみ修正する。

| 設定項目                                            | 設定内容                                                 |
| --------------------------------------------------- | -------------------------------------------------------- |
| ルートURL                                           | https\://Nextcloudのホストネーム                         |
| 有効なリダイレクトURL                               | https\://Nextcloudのホストネーム/*                       |

- 設定例
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531974/cd82c47b-ba45-e075-9f94-dbe87b58e5f3.png)

### マッパー
Nextcloudユーザーは`username`と`email`が必須属性なのでマッパーも設定する。

- マッパー

| 名前     | マッパータイプ | プロパティ | Friendly Name | SAML Attribute Name | SAML Attribute NameFormat |
| -------- | -------------- | ---------- | ------------- | ------------------- | ------------------------- |
| username | User Property  | username   | username      | username            | Basic                     |
| email    | User Property  | email      | email         | email               | Basic                     |

- 設定例
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531974/955206f5-1c3b-f0d5-73b3-c587d58dc242.png)

## クライアントスコープの設定
左メニューの`クライアントスコープ`から`role_list`を選択 ⇒ `マッパー`から`role_list`を選択 ⇒ `Single Role Attribute`を`オン`にしておく。
この設定を入れないと、SAML認証時、Nextcloudが謎の例外を吐いてしまい認証に失敗する。

- 設定例
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531974/d56c9fd2-e61f-35fd-2bef-43b946cdbe91.png)

- 散り様
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531974/6f2af026-788a-8117-02d5-9a3657da3584.png)

## 外部idp設定

### Googleの設定
Googleアカウントによる認証を行うには、まず、[GoogleのAPIコンソール](https://console.developers.google.com)から認証情報を作成する。  

- 新しいプロジェクトを作成
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531974/e7c7f465-57f2-3f5e-2a5d-881b899e3537.png)

- OAuth同意画面を作成(G Suiteは私用しないのでUserTypeは外部とする)  
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531974/9e1c4c99-fbfd-157d-dcc6-bd98b353a49b.png)

- **承認済みドメインには最上位のドメイン名を登録する。**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531974/1545eb16-a539-b599-13de-aa049950182a.png)

- 保存後
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531974/be85f447-a2ca-8729-07d2-869571b2f98a.png)

- 認証情報を作成する。(`OAuth クライアントID`を選択)  
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531974/582dd6e4-98be-f3eb-4f49-4a7f045e7569.png)

- 認証情報の設定内容
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531974/834ef28b-189d-172b-4850-ffa3694ea3ab.png)

- 登録後(**クライアントIDとシークレットが発行されるので控えておく**)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531974/bb52f379-2a59-893e-4bda-c63ef075ff5b.png)

### Keycloakの設定
左メニューからアイデンティティプロバイダーを選択し、`Google`を選択し、設定画面に入る。

- 設定内容(差分のみ)

| 設定項目                 | 設定内容                             |
| ------------------------ | ------------------------------------ |
| クライアントID           | さっき控えたクライアントID           |
| クライアントシークレット | さっき控えたクライアントシークレット |
| デフォルトスコープ       | openid profile email                 |

- 設定例
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531974/b66a48da-dee9-ac9c-8a2c-0a91b20de58d.png)

# 拡張モジュール作成/設定  
実はここまでの設定でもうNextcloudにGoogleアカウントでログインすることができる。
ただ、このままではGoogleアカウント持ってれば誰でもウェルカム状態なので、
Keycloakに拡張認証モジュールを投入し、Keycloak上にアカウントが存在しない人をたたき出すようにする。
今回拡張したいのは[First Broker Login](https://www.keycloak.org/docs/latest/server_development/#modifying-first-broker-login-flow)なので、`AbstractIdpAuthenticator`を継承してモジュールを作成した。  

## 認証モジュールの流れ
1. Google経由のログインが初めてなら拡張モジュールに入る
1. Googleから送られてきたユーザー情報のメールアドレスで、Keycloakにアカウントが作られているかチェック
1. 作られていなければ認証NGにしてたたき出す
1. 作られていたらGoogleとアカウントリンクして認証OKにする

## ファイル構成

認証モジュールの作成には、少なくとも認証モジュール本体、ファクトリー、クラス登録用定義ファイルの3種が必要。

- ファイル構成例
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531974/abaa87e4-bf1a-1adb-4086-a978c61247d2.png)

## コード

- 認証モジュール本体

```java:IpdAccountExistsAuthenticator.java
package local.example;

import org.jboss.logging.Logger;
import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.authentication.AuthenticationFlowError;
import org.keycloak.authentication.AuthenticationFlowException;
import org.keycloak.authentication.authenticators.broker.AbstractIdpAuthenticator;
import org.keycloak.authentication.authenticators.broker.util.SerializedBrokeredIdentityContext;
import org.keycloak.broker.provider.BrokeredIdentityContext;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;

import java.util.Objects;

public class IpdAccountExistsAuthenticator extends AbstractIdpAuthenticator {

    private static Logger logger = Logger.getLogger(IpdAccountExistsAuthenticator.class);

    /**
     * 認証処理
     *
     * @param context
     * @param serializedCtx
     * @param brokerContext
     */
    @Override
    protected void authenticateImpl(AuthenticationFlowContext context, SerializedBrokeredIdentityContext serializedCtx, BrokeredIdentityContext brokerContext) {

        logger.info("Start IpdAccountExistsAuthenticator");

        // ユーザがKeycloakに登録済みであればOK
        UserModel user = context.getSession().users().getUserByUsername(brokerContext.getEmail(), context.getRealm());

        if (Objects.isNull(user)) {
            // ユーザが取れなければ認証失敗にする
            throw new AuthenticationFlowException("そんなユーザいません : " + brokerContext.getEmail(), AuthenticationFlowError.UNKNOWN_USER);
        }

        user.setEnabled(true);
        user.setEmail(brokerContext.getEmail());
        user.setFirstName(brokerContext.getFirstName());
        user.setLastName(brokerContext.getLastName());

        logger.info("認証OK : " + user.getUsername());

        // 認証成功
        context.setUser(user);
        context.getAuthenticationSession().setAuthNote(BROKER_REGISTERED_NEW_USER, "true");
        context.getAuthenticationSession().setAuthenticatedUser(user);
        context.success();
    }

    @Override
    protected void actionImpl(AuthenticationFlowContext context, SerializedBrokeredIdentityContext
            serializedCtx, BrokeredIdentityContext brokerContext) {

    }

    @Override
    public boolean requiresUser() {
        return false;
    }

    @Override
    public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
        return false;
    }
}
```

- ファクトリー
  
```java:IpdAccountExistsAuthenticatorFactory.java
package local.example;

import org.keycloak.authentication.Authenticator;
import org.keycloak.authentication.AuthenticatorFactory;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.provider.ProviderConfigProperty;

import java.util.List;

public class IpdAccountExistsAuthenticatorFactory implements AuthenticatorFactory {

    public static final String ID = "exists-account-from-keycloak";
    public static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
            AuthenticationExecutionModel.Requirement.ALTERNATIVE,
            AuthenticationExecutionModel.Requirement.REQUIRED,
            AuthenticationExecutionModel.Requirement.DISABLED};

    @Override
    public Authenticator create(KeycloakSession keycloakSession) {
        return new IpdAccountExistsAuthenticator();
    }

    @Override
    public void init(org.keycloak.Config.Scope scope) {

    }

    @Override
    public void postInit(KeycloakSessionFactory keycloakSessionFactory) {

    }

    @Override
    public void close() {

    }

    @Override
    public String getId() {
        return ID;
    }

    @Override
    public String getReferenceCategory() {
        return "exists-account";
    }

    @Override
    public boolean isConfigurable() {
        return true;
    }

    @Override
    public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {
        return REQUIREMENT_CHOICES;
    }

    @Override
    public String getDisplayType() {
        return ID;
    }

    @Override
    public String getHelpText() {
        return "ipd exists accoount";
    }

    public boolean isUserSetupAllowed() {
        return true;
    }

    public List<ProviderConfigProperty> getConfigProperties() {
        return null;
    }
}
```

- クラス登録用定義ファイル

```:org.keycloak.authentication.AuthenticatorFactory
local.example.IpdAccountExistsAuthenticatorFactory
```

## ビルド
上記ファイルをビルドしてjarを生成する。
pomはこんな感じにして`mvn install`した。

```xml:pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns="http://maven.apache.org/POM/4.0.0"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>club.honatsugi-ol.sso</groupId>
    <version>1.0-SNAPSHOT</version>
    <name>IDP Exists</name>
    <description/>
    <artifactId>idp_account_exists</artifactId>
    <packaging>jar</packaging>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.8</java.version>
        <maven.compiler.source>${java.version}</maven.compiler.source>
        <maven.compiler.target>${java.version}</maven.compiler.target>
        <keycloak.version>10.0.1</keycloak.version>
        <version.wildfly.maven.plugin>1.1.0.Final</version.wildfly.maven.plugin>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.keycloak</groupId>
            <artifactId>keycloak-core</artifactId>
            <version>${keycloak.version}</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.keycloak</groupId>
            <artifactId>keycloak-server-spi</artifactId>
            <version>${keycloak.version}</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.keycloak</groupId>
            <artifactId>keycloak-server-spi-private</artifactId>
            <version>${keycloak.version}</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.keycloak</groupId>
            <artifactId>keycloak-services</artifactId>
            <version>${keycloak.version}</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.jboss.logging</groupId>
            <artifactId>jboss-logging</artifactId>
            <version>3.4.1.Final</version>
        </dependency>
    </dependencies>

    <build>
        <finalName>idp-account-exists</finalName>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <configuration>
                    <archive>
                        <manifestEntries>
                            <Dependencies>org.keycloak.keycloak-services</Dependencies>
                        </manifestEntries>
                    </archive>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>
```

## デプロイ
`Keycloakのホームディレクトリ/standalone/deployments`に産み出されたjarを放り込むと、ホットデプロイしてくれる。

## 認証フローの作成
左メニューの`認証`から、以下のような認証フローを作成する。  
設定が複雑なので、デフォルトで存在するフローをコピー ⇒ `アクション`からExecution全削除 ⇒ `Executionを追加`から拡張モジュールを追加とすると楽。 

- 設定例 
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531974/de93778b-773d-903d-b0a8-891486e18fb6.png)

## 作成したフローの設定
左メニューの`アイデンティティプロバイダー`からさっき作った`google`を選択し、
`初回ログインフロー`にフローを設定する。

- 設定例
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531974/1ce54120-fe3f-2fe3-f2e1-869e86e77db4.png)

# 動作確認

## ログイン成功(登録済みユーザ)
- Keycloakにユーザを登録
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531974/d548319f-5924-f8c4-93cd-641c0fada570.png)

- `SSO & SAMLログイン`を選択
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531974/7ed8a5d5-8bb5-db3d-fb8f-3dce0366b523.png)

- Keycloakのログイン画面に飛ぶのでGoogleを選択
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531974/3aa6ebec-7864-ab4e-8e04-3806e0401700.png)

- テストユーザを選択してログイン
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531974/9585de34-2b34-1690-2a85-0fd53a7752d7.png)

- ログイン成功
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531974/295b50bd-7670-6dfd-b097-59b6c9c25fb5.png)

## ログイン失敗(未登録ユーザ)
- Keycloak上からテストユーザを抹殺して再度ログイン
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531974/c57fc334-d7a0-7e73-eb23-35be6aebfcf5.png)

- 弾いた
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531974/352eb10a-fdc1-6529-6853-a25d375c5217.png)

- ログ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531974/81742f45-d748-320a-cd9a-211a390eed24.png)

# メモ

## Nextcloudの`SSOとSAML認証`の設定の保存場所
設定はNextcloudDBの`oc_appconfig`テーブルに存在する。
`appid`カラムを`user_saml`で絞ると見つけるのが楽。
ガチャガチャいじってたらデータが壊れ、設定が保存されなくなってしまったが、レコード修正したら治った。

## Keycloakのリモートデバッグ
`standalone.conf`にこんな設定があるので、コメント解除してKeycloakを再起動してやればOK。
ポート開けてやればIDEから接続可能。

```conf:standalone.conf
# Sample JPDA settings for remote socket debugging
#JAVA_OPTS="$JAVA_OPTS -agentlib:jdwp=transport=dt_socket,address=8787,server=y,suspend=n"
```

```yml:docker-compose.yml
      ports:
      - 127.0.0.1:8080:8080
      - 8787:8787
```

# 終わりに
ラズパイ4でもなかなか機敏に動く。感動した。
もともとこのシステムはラズパイ2で無理矢理動かしていたが、サムネ生成の速度とか雲泥の差。
記事書くついでに移植したが、やってよかった。。。