KeycloakのJavaScriptクライアントアダプターを使ってみる。

[keycloak公式](https://www.keycloak.org/downloads)で提供されているOIDCのクライアントアダプターを実際に利用してみる。
keycloakのOIDCのクライアントアダプターは、WildFly、Tomcat、Node.jsなど様々な形で提供されていたが、バージョンが上がるにつれて徐々にDEPRECATEDとなり、最新(21.0.2)ではJavaScript以外はすべてDEPRECATEDとなっている。
本記事では唯一の生き残りであるJavaScriptのクライアントアダプターを利用して、クライアントアプリからKeycloakサーバへ接続する方法を検証する。
※本記事は、Keycloakのバージョンアップに伴い、「[Keycloakのクライアント・アダプターを試してみる（Angular編）](https://qiita.com/mamomamo/items/cdde95feffbb5e524fd4)」の記事を最新化したものです。


## 本記事でやること

 1. Angularアプリケーションの作成
 1. Keycloakサーバの設定
 1. Keycloakクライアントアダプターの導入
 1. 動作検証

## Angularアプリケーションの作成
### 内容
Angularを使ったクライアントアプリケーションとして、
Keycloakによる認証が「必要な画面」と「不要な画面」の2つの簡単が画面を作成して検証する。

### 環境
Angularの開発環境は以下の通り

 - Node: 16.15.1
 - Angular: 14.3.0
 - Angular CLI: 14.0.7

## 
Angular CLIの`ng new`コマンドを利用して新規プロジェクトを作成し、
`ng generate component`コマンドを使って２つのページを作成した。

まず、appcomponentについては以下のようにルーターを使って画面を切り替えられるようにし、ルーティングの設定にも追加する画面のコンポーネントを追加しておく。
```html:src/app/app.componenthtml
<nav>
  <a routerLink="">認証不要</a>
  <a routerLink="/auth">要認証</a>
</nav>
<router-outlet></router-outlet>
```
```ts:src/app/app-routing.module.ts
import { RouterModule, Routes } from '@angular/router';
import { AuthPageComponent } from './auth-page/auth-page.component';
import { NonAuthPageComponent } from './non-auth-page/non-auth-page.component';

const routes: Routes = [
  { path: '', component: NonAuthPageComponent },
  { path: 'auth', component: AuthPageComponent },
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule],
})
export class AppRoutingModule {}
```

認証不要な画面は以下の通りテキストのみのシンプルなページとする。
また、Typescriptファイルは特にすることもないので自動生成された状態のままでOK。
```html:src/app/non-auth-page/non-auth-page.component.html
<h3>Keycloakによる認証が不要なページです。</h3>
```

認証が必要なページは以下のようにして、認証したユーザーの情報を表示できるようにする。

```html:src/app/auth-page/auth-page.component.html
<h3>Keycloakによる認証が必要なページ</h3>
<div>
  <span>User Name : {{ user.username }}</span>
  <span>First Name : {{ user.firstName }}</span>
  <span>Last Name : {{ user.lastName }}</span>
</div>
<div>
  <span>ID Token:</span>
  <div class="token">{{ user.idToken }}</div>
</div>
```
```ts:src/app/auth-page/auth-page.component.ts
import { Component, OnInit } from '@angular/core';
import { User } from '../user.model';

@Component({
  selector: 'app-auth-page',
  templateUrl: './auth-page.component.html',
  styleUrls: ['./auth-page.component.scss'],
})
export class AuthPageComponent implements OnInit {
  constructor() {}

  user!: User;

  public ngOnInit(): void {}
```
ユーザー情報を入れておく箱も合わせて作成しておく。
※ユーザー情報にはkeycloakから認証したユーザーの情報を取得して詰め込むがそちらは後で説明する。
```ts:user.model.ts
export class User {
  public username: string;
  public firstName: string;
  public lastName: string;
  public idToken: string;

  constructor(
    username: string,
    firstName: string,
    lastName: string,
    idToken: string
  ) {
    this.username = username;
    this.firstName = firstName;
    this.lastName = lastName;
    this.idToken = idToken;
  }
}
```

## keycoakサーバの設定

### keycloakサーバ構築
今回keycloakは公式ページの[こちら](https://www.keycloak.org/getting-started/getting-started-zip)を使ってローカルPC上に立ち上げて検証した。

### Realm作成
keycloakが起動したら管理コンソールへサインインして、Realmを作成する。
今回Realm名を`demo-realm`として作成した。

#### 手順
1.管理管理コンソールにサインイン
2.メニューバーのrealm選択リストにある「Create Realm」を選択
3.Realm nameを入力して「Create」で作成
![レルム作成.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3333320/42f6b823-f616-91ae-fb48-a6c65a0f6a35.png)


### クライアントの作成
クライアントアプリケーション用にkeycloakのクライアントを作成する。
クライアントは先ほど作成したRealmに作成していく。

#### 手順
1.メニューバーを開いて先ほど作成したrealmが選択されていることを確認する。
2.メニューバーの「Clients」を押下してクライアント一覧を表示する。
3.「Create client」ボタンを押下してクライアント作成画面を開く。
4.「OpenID Connect」を選択、Client IDを入力して「Next」ボタンを押下する。
![クライアント作成1.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3333320/c2308b8a-af4a-d5a4-facb-f1d3a6f34de7.png)

5.「Client authentication」をOffにすることでアクセスタイプをパブリックに設定し、「Next」ボタンを押下する。
![クライアント作成2.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3333320/8fe96a8a-0f60-ff20-6e2f-770ac30e7de3.png)

:::note info
クライアントサイドアプリケーションでは安全にクライアントの資格情報を保存する方法が存在しないため、アクセスタイプをパブリックに設定する必要があります。
:::
　
6.以下の2つを設定して「Save」ボタンを押下する。
 - Validate redirect URIs：http://localhost:4200/*
 - Web origins：http://localhost:4200

![クライアント作成3.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3333320/2145ac5f-e81c-bf84-8370-b34465824ec1.png)

:::note info
「Validate redirect URIs」はKeycloakで認証後にリダイレクト先として許可してもよいURLを設定する。
「Web origins」はCORS要求を許可するホストを設定する。
これら2点が実行元のクライアントアプリに沿った正しい値が設定されていないと、リクエストの拒否やCORSエラーが発生して認証に失敗する。
:::

### ユーザーの作成
認証を行うためのユーザーを作成する。

#### 手順
1.メニューバーを開いて先ほど作成したrealmが選択されていることを確認する。
2.メニューバーの「User」を押下してユーザー一覧を表示する。
3.「Add user」ボタンを押下してユーザー作成画面を開く。
4.Username、First name、Last nameを入力して「Create」ボタンでユーザーを作成する。
![ユーザー作成1.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3333320/26cd97d0-b9d1-90f0-a49b-f99f054ad220.png)

5.ユーザー一覧画面から作成したユーザーを選択する。
6.「Creadentials」タブを選択する。
7.「Set password」ボタンを押下してパスワードを登録する。
![ユーザー作成2.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3333320/c5b87d8f-b7f5-32a9-f7a8-ecc539c1edfd.png)
これでkeycloakサーバ側の必要な設定はすべて完了。

## keycloakクライアントアダプタの導入
まず、Javascriptのクライアントアダプタを利用するにはプロジェクト直下で以下のコマンドを実行する必要があります。

```
npm install keycloak-js --save
```
こちらでプロジェクトには導入されたため利用可能となります。
各コンポーネントでkeycloak-jsを適宜インポートして利用します。

### keycloak接続情報の設定
先ほどkeycloakサーバと通信できるようにするため、先ほど作成したクライアント情報を環境設定ファイルに設定します。
```ts:src/environments/environment.ts
export const environment = {
  production: false,
  KEYCLOAK_URL: 'http://localhost:8080/',
  KEYCLOAK_REALM: 'demo-realm',
  KEYCLOAK_CLIENTID: 'demo-client',
};
```


### keycloakサービス作成
このサービスでは、サービスの初期化、ユーザー情報の取得機能を実装します。

以下のコマンドを実行してサービスを作成します。
```
ng generate service keycloak
```
サービスを利用可能とするため以下に追加します。
```ts:src/app/app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { KeycloakService } from './keycloak.service';
import { AuthPageComponent } from './auth-page/auth-page.component';
import { NonAuthPageComponent } from './non-auth-page/non-auth-page.component';

@NgModule({
  declarations: [AppComponent, AuthPageComponent, NonAuthPageComponent],
  imports: [BrowserModule, AppRoutingModule],
  providers: [KeycloakService],
  bootstrap: [AppComponent],
})
export class AppModule {}

```

実際に実装したサービスの全体像は以下となります。
```ts:
import { Injectable } from '@angular/core';
import Keycloak from 'keycloak-js';
import { Observable } from 'rxjs';
import { environment } from '../environments/environment';
import { User } from './user.model';

@Injectable({
  providedIn: 'root',
})
export class KeycloakService {
  private kc: any;
  private auth: boolean = false;
  private user!: User;

  constructor() {
    this.kc = new Keycloak({
      url: environment.KEYCLOAK_URL,
      realm: environment.KEYCLOAK_REALM,
      clientId: environment.KEYCLOAK_CLIENTID,
    });
  }

  public init(): Observable<any> {
    return new Observable((ob) => {
      this.kc
        .init({ onLoad: 'login-required' })
        .then(() => {
          this.auth = this.kc.authenticated;
          this.kc
            .loadUserProfile()
            .then((profile: any) => {
              this.user = new User(
                profile.username,
                profile.firstName,
                profile.lastName,
                this.kc.idToken
              );
              ob.next();
            })
            .catch(() => {
              console.log('loadUserProfile error');
            });
        })
        .catch(() => {
          console.log('init error');
        });
    });
  }

  public isAuth(): boolean {
    return this.auth;
  }

  public getUser(): User {
    return this.user;
  }
}
```
サービスの初期設定として、コンストラクタで以下のコードを実行し、keycloakサーバの環境情報を設定します。
```ts
  constructor() {
    this.kc = new Keycloak({
      url: environment.KEYCLOAK_URL,
      realm: environment.KEYCLOAK_REALM,
      clientId: environment.KEYCLOAK_CLIENTID,
    });
  }
```
`init()`関数でサービスの初期化を行っています。この`init()`関数では、認証機能を呼び出すために以下のコードを実行しています。この設定により、未認証時にはkeycloakサーバへリダイレクトさるようになります。
```ts
init({ onLoad: 'login-required' })
```
認証済ユーザーの情報を取得するため、`init()`成功後に`loadUserProfile()`を実行しています。また、取得した情報をサービス利用者が参照できるようにUserに詰め込んで`getUser()`で取得できるように関数を用意しています。

### keycloakサービスの利用
作成したkeycloakサービスを認証が必要な画面で利用するため以下のように画面のコンポーネントに追加します。

```ts:src/app/auth-page/auth-page.component.ts
import { Component, OnInit } from '@angular/core';
import { KeycloakService } from '../keycloak.service';
import { User } from '../user.model';

@Component({
  selector: 'app-auth-page',
  templateUrl: './auth-page.component.html',
  styleUrls: ['./auth-page.component.scss'],
})
export class AuthPageComponent implements OnInit {
  constructor(private ks: KeycloakService) {}

  user!: User;

  public ngOnInit(): void {
    this.ks.init().subscribe(() => {
      this.user = this.ks.getUser();
    });
  }
}
```
このコンポーネントでは、`ngOnInit()`で作成したサービスの`init()`を実行して認証済みかどうかのチェックをしています。ユーザーが正常に認証に成功した場合のみ、本画面を表示することができます。

## 動作検証
すべての実装が完了したので実際にアプリを起動して動作を確認していきます。
`ng serve`でAngularクライアントアプリケーション起動して、http://localhost:4200 へアクセスします。このページはkeyclaokサービスを適用していないため、想定の通り認証不要(リダイレクトもされず)画面が表示されます。
![動作検証1.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3333320/8fa10947-f056-9fb8-aaf8-7f43f4bf8c9e.png)

次に認証が必要な画面を表示してみます。
「要認証」ボタンを押下すると以下のようにkeycloakの認証画面へ自動でリダイレクトされます。
![動作検証2.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3333320/9c222210-9c0d-56cb-4240-762c927c4d60.png)

作成したユーザーの認証情報を入力してサインインすると再度クライアントアプリケーションの画面へリダイレクトされ、ユーザーの情報が表示されました。
![動作検証3.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3333320/dd8ebc3e-fdf4-69bb-6c25-34d933a80e51.png)

これでkeycloakのjavascriptクライアントアダプターを利用して、クライアントアプリからkeycloakサーバとやり取りできることが検証できました。

## 最後に
今回は検証のため認証が必要な画面、不要な画面という形で設定して動作検証したが、実際のSPAでは最初にアクセストークンを取得したら認可については、API側で実施する形が一般的かと思う。
次は、keycloakとAPI GatewayなどでSPAからのAPIのアクセス制御など検証してみたい。

## 参考資料
https://www.keycloak.org/docs/latest/securing_apps/index.html#_javascript_adapter
https://www.npmjs.com/package/keycloak-js