React hooksとKeycloakを連携してみた

# はじめに

[React](https://www.keycloak.org/)と[Keycloak](https://www.keycloak.org/)を連携し、OIDCによるログインを実装しましたので、実装方法についてご紹介しようと思います。

![reactkeycloak.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2004450/8e8d9c82-c408-df77-892e-a99729f8fec8.png)





## Open ID Connect（OIDC）

OIDCは、OAuthを拡張した認証のためのプロトコルで、シングルサインオン（SSO）に主に使われます。
OIDCの用語は以下になります。

- OpenID Provider（OP）
    - ユーザの認証を行うサーバー。
- Relying Party（RP）
    - OPにIDトークンとアイデンティ情報を要求するサーバー。
- UserInfoエンドポイント
    -  アクセストークンを示したクライアントに対し、アイデンティ情報を提供する。
- IDトークン
    - 認証・認可の情報を含むJWT（JSON Web Token）形式のトークン。
- アクセストークン
    - `UserInfo`エンドポイントにアクセスするためのトークン。


今回作成したアプリケーションのフローを以下の図に示します。
![flow.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2004450/a530ed81-546d-34b2-0a4e-2c4aa3f17cb6.png)



# Keycloakの設定

今回、登録したKeycloakの設定を以下に示します。


- レルム：Keycloakでユーザなどの情報を管理するための単位。
    - realm：`demo`
- クライアント:OIDCにおけるRPを指定します。
    - Client ID:`demo-react-client`
    - RootURL:`http://localhost:3000/`
- ユーザー
    - FirstName:`Foo`
    - LastName:`Smith`
    - Email:`user001@dummy.com`
    - Password:`password`


## JavaScriptアダプター（keycloak.json）の設定

`keycloak.json`で、JavaScriptアダプターの設定を行います。
javaScriptアダプターはKeycloakの機能であるクライアントアダプターの一つで、アプリケーションにOIDCのRPなどの機能を持たせることが可能です。Keycloakの`Client`設定画面の`Installation`タブで「Format Option」に`Keycloak OIDC JSON`を設定することでJSONファイルをダウンロードすることができます。


```json
{
  "realm": "demo",
  "auth-server-url": "http://localhost:8080/auth/",
  "ssl-required": "external",
  "resource": "demo-react-client",
  "public-client": true,
  "confidential-port": 0
}
```

各設定内容については以下に示します。

- `realm`：レルム名
- `auth-server-url`：KeycloakのURL
- `ssl-required`：SSLの設定  
     -  `external`:プライベートIPアドレスに固定する限り、ユーザーはSSL無しでKeycloakと通信可能。  
     - `none`：SSLの設定なし。   
     - `all`：すべてのIPアドレスに対し、SSLを要求。
- `resource`：クライアントの「Client ID」
- `public-client`：パブリッククライアントである場合は`true`を設定
        

# Reactでの実装

`React 16.8`から追加された機能であるReact hooksを使い実装しました。

## プロジェクト作成

`Create React App`でプロジェクトを作成します。

```
$ npx create-react-app  [プロジェクト名]
```

## ディレクトリ構成

上記コマンドで作成されたディレクトリに追加したフォルダ・ファイルを示します。
`public`ディレクトリに前述した`keycloak.json`を配置します。
また、`src`ディレクトリに`components`ディレクトリを作成し、`Secured.js`、`Userinfo.js`、`Logout.js`を配置します。

```
.
├── README.md
├── node_modules
├── package.json
├── public
│   ├── keycloak.json　（追加）
│   ├── favicon.ico
│   ├── index.html
│   ├── manifest.json
│   └── robots.txt
├── src
│   ├──components（追加）
│   │   ├── Secured.js
│   │   ├── Userinfo.js
│   │   ├── Logout.js
│   ├── App.css
│   ├── App.js
　　・
　　・
　　・
　（省略）
```


## Keycloakとの連携

```react:Secured.js
import {React,useState,useEffect} from 'react'
import Keycloak from 'keycloak-js';
import UserInfo from './UserInfo';
import Logout from './Logout';

const Secured = () => {
    const [keycloakState, setKeycloakState] = useState({ keycloak: null, authenticated: false });

    useEffect(() => {
        const keycloak = Keycloak('./keycloak.json');
        //ページロード時の処理で、Keycloak.init関数を呼び出し、未認証の場合
        //認可コードフローを開始しログインページを表示
        keycloak.init({onLoad: 'login-required'}).then(authenticated => {
            setKeycloakState({ keycloak: keycloak, authenticated: authenticated });      
        })
    }, [])
    console.log(keycloakState)
    if(keycloakState.keycloak){

        if(keycloakState.authenticated)
        return (
            <div>
                <p>login success</p>
                <UserInfo keycloak={keycloakState} />
                <Logout keycloak={keycloakState} />
            </div>
            ) ; 
        else 
        return(<div> 認証できません</div>)
        }
    return (
        <div>Keycloakを初期化しています...</div>
    )
}

export default Secured
```

`http://localhost:3000/Secured`にアクセスし、確認します。

![login.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2004450/069bc39f-9d9c-51b3-2a26-f5f8b91cd9dc.png)


## UseInfoの表示


```react:Userinfo.js
import {useEffect,useState} from 'react'

const UserInfo = (props) => {

    const keycloakState = props.keycloak
    const [user, setUser] = useState({name:"",email:"",id:""});
    
    useEffect(() =>{
      //loadUserInfoから取得したユーザーデータを元にuserを更新
      keycloakState.keycloak.loadUserInfo().then(UserInfo => {
            console.log(UserInfo);
            setUser({ name: UserInfo.name, email: UserInfo.email, id: UserInfo.sub })
        });
    },[])
    return (
        <div className="UserInfo">
        <p>Name: {user.name}</p>
        <p>Email: {user.email}</p>
        <p>ID: {user.id}</p>
        </div>
    )
}
export default UserInfo
```


ここでは、Keycloakインスタンスから、`loadUserInfo`メソッドを使用しユーザーデータの抽出をしています。
ログインが成功した後の画面に名前、メールアドレス、クライアントIDが表示されます。

![userinfo.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2004450/b3b50c08-2b43-89ac-b737-13421029b65a.png)


## ログアウトの実装

ここも同様に、Keycloakインスタンスから`logout`メソッドを使用します。
`react-router`が`v6`から`useHistory`メソッドが使用できなくなったため、`useNavigate`メソッドを使用しログアウトを実装しました。
`react-router`の`v6`と`v5`の違いについては、以下を参考にしてください。

https://reactrouter.com/docs/en/v6/upgrading/v5


```react:Logout.js
import {React} from 'react'
import {useNavigate} from 'react-router-dom'

const Logout = (props) => {

    const keycloakState = props.keycloak
    // ログイン画面に遷移
    const nabigate = useNavigate()
    nabigate('/secured') 
    return (
        <div>
            <button onClick={ () => keycloakState.keycloak.logout()} >
            Logout 
            </button>
        </div>
    )
}
export default Logout
```

# まとめ

今回は、シンプルなReactアプリケーションとKeycloakを連携し、簡単にフロントエンドの認証を実装できることがわかりました。
Keycloakではシングルサインオンや外部プロバイダーを使った認証なども実装することができるので、色々活用してください。

#　参考

https://scalac.io/blog/user-authentication-keycloak-1/
https://blog.logrocket.com/implement-keycloak-authentication-react/