【2019年11月版】Keycloak ニテ Angular ヲ AuthGuard セヨ

# Keycloak で Angular の AuthGuard をやってみよう

以前の記事("[【2019年10月版】Quarkus の REST API を KeyCloak で SSO するデモを docker-compose で動かしてみた。](https://qiita.com/koinori/items/331029bcc823cc222a8f)")にて、Quarkus の REST API と Keycloak の連携するデモを動かしてみましたが、連携が非常に簡単なのでとても驚き「こっ・・・これはイケるッ！！」となった次第でございます。

バックエンド側の REST API が Keycloak 対応バッチリなのであればフロント側も確認しないとダメでしょう？ということで、Anuglar の Router Guard 機能を使って Keycloak での AuthGuard にトライしてみたいと思います。

## 対象環境

Keycloak : 7.0.1
Angular: 7.2.0

`ng new`コマンドで新規生成した Angular プロジェクトに `admin`と`user`ロールでそれぞれアクセスできるページを用意し、Keycloak で設定したロールがAuthGuardで利かせられるかどうかを確かめてみたいと思います。

## 1. Angular プロジェクトの作成

```shell
$ ng new ng-keycloak-sample
...
$ cd ng-keycloak-sample
```
いくつか質問されますが、"Router"についての質問には `Y` として追加してください。あとは適当でOKです！

さて、このプロジェクトの中で作業を進めてまいります。

## 2. Keycloak パッケージの追加

keycloak-angular を追加します。バージョンは使用する Keycloak に合わせます。

```
$ npm i keycloak-angular@7.0.1
$ npm i keycloak-js@7.0.1
```

keycloak-js は `peer dependency` なので別途、インストールする必要があります。こちらもバージョンを実際に使用する Keycloak に合わせます。

## 3. 適当ページと Router の作成

それでは、実際のコンポーネントのコーディングに入ります。

### 3-1. ページコンポーネントの作成

以下のコマンドでコンポーネントを生成してしまいます。

```
$ ng g c main # -> トップページ
...
$ ng g c pages/config # -> Admin エリア
...
$ ng ng c pages/dashboard # -> user エリア
```
で共通エリア(？)、Userエリア、Adminエリアの３つを(適当に)ご用意いたしました。
今回は各コンポーネントの実装を先に行ってしまって、パスとGuardのルーティング設定は後の工程で行います。

まず共通エリアとなる`main.component.html` ですが、ここでは各ページへのリンクとログオフボタンを用意しておきます。

```html:main.component.html
<p>
  main works!
</p>

<li><a [routerLink]="[ '/dashboard']">Here is User Area</a></li>
<li><a [routerLink]="[ '/config']">Here is Admin Area</a></li>
<li><button (click)="logOut()">Log Out</button></li>
```

`main.component.ts` では `KeycloakService` とログオフの機能を追加します。

```ts:main.component.ts
import { Component, OnInit } from '@angular/core';
import { KeycloakService } from 'keycloak-angular';

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

  constructor(private keycloakAngular: KeycloakService) { }

  ngOnInit() {
  }
  async logOut() {
    await this.keycloakAngular.logout("http://localhost:4200/");
  }
}
```
コンストラクタで`KeycloakService`を受け取っておきます。
`logOut`メソッドでのログアウト後のリダイレクトには "KeyCloakからみたURL"を入れます。とりあえず Angular の開発サーバーである `localhost:4200` をリダイレクト先にしておきます。

Adminエリアである`config`コンポーネントと、Userエリアである`dashboard`コンポーネントでも同様に、メインへ戻るリンクと`Log Out`ボタンを設置しておきます。ここではソースコードは割愛させてください。

### 3-2. Router の設定

いよいよ本丸その1のルーターの設定です。ここでAuthGuardとロールの設定を行います。

```ts:app-routing.module.ts
const routes: Routes = [
  {
    path: '',
    component: MainComponent
  },
  {
    path: 'config',
    component: ConfigComponent,
    canActivate: [CanAuthenticationGuard],
    data: { roles: ['admin'] }
  },
  {
    path: 'dashboard',
    component: DashboardComponent,
    canActivate: [CanAuthenticationGuard],
    data: { roles: ['user'] }
  },
];

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

まず `MainComponent` はガードなしで、誰でもアクセス可能=Keycloakのログインなしで入場可能とします。
次の `ConfigComponent` は `CanAuthenticationGuard` というガードを設定し、`data` にて `{ roles: ['admin'] }` を指定して、`CanAuthenticationGuard` の中でチェックするべきロールを宣言します。
ここでは `admin` と宣言し、`CanAuthenticationGuard`内で `admin` ロールを持つユーザーのみがアクセス可能とします。
同様に `DashboardComponent` では `data`に `{ roles: ['user'] }` を宣言し、`CanAuthenticationGuard`内で `user` ロールを持つユーザーのみがアクセス可能とします。

`canActivate` でガードの指定と、`data` で許可するロールを宣言する、というのがここでのキモです。

## 4. keycloak-angular(Keycloak-js) の組み込み

続いて keycloak-angular の手当てを組み込んでいきます。
今回の手順はほぼ[本家リポジトリのREADME.md](https://github.com/mauriciovigolo/keycloak-angular#setup) に従った手順となります。

### 4-1. APP_INITIALIZER を使った初期化

まず Keycloakクライアントの初期化処理ですが、今回は `APP_INITIALIZER` を使ったパターンで行ってみます。
初期化のオプション少ないので `app.module.ts` に直書きでもいいかもしれませんが、一応、切り出しておきます。

```ts:util/init-app.ts
export function initializer(keycloak: KeycloakService): () => Promise<any> {
  return (): Promise<any> => new Promise(async (resolve, reject) => {
    try {
      await keycloak.init({
        config: {
          url: 'http://localhost:8180/auth',
          realm: 'Angular Sample',
          clientId: 'ng-keycloak-sample'
        },
      });
      resolve();
    } catch (error) { }
  });
}
```
Keycloak のアドレス、Keycloak で定義する Realm と ClientId を指定してクライアントの初期化を行います。
この `initializer` 関数を export しておきます。
そして `app.module.ts` で export した `initializer`　関数を使用して `KeycloakService` を初期化します。

```ts:app.module.ts
...
  providers: [
    {
      provide: APP_INITIALIZER,
      useFactory: initializer,
      multi: true,
      deps: [KeycloakService]
    }],
  bootstrap: [AppComponent]
})
export class AppModule { }
```
上記のようになります。

### 4-2. AuthGuard の実装

こちらの[本家のAuthGuardの説明](https://github.com/mauriciovigolo/keycloak-angular#authguard)より、AuthGuardを実装します。
~~クラス名もそのまま拝借したのですが、、ちょおっとアレですね。。。~~

```ts:service/CanAuthenticationGuard.ts
...
@Injectable({
  providedIn: 'root'
})
export class CanAuthenticationGuard extends KeycloakAuthGuard implements CanActivate {
  constructor(protected router: Router, protected keycloakAngular: KeycloakService) {
    super(router, keycloakAngular);
  }

  isAccessAllowed(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<boolean> {
    return new Promise((resolve, reject) => {
      console.log(this.roles);
      if (!this.authenticated) {
        this.keycloakAngular.login()
          .catch(e => console.error(e));
        return reject(false);
      }

      const requiredRoles: string[] = route.data.roles;
      console.log("required:%o", requiredRoles);
      if (!requiredRoles || requiredRoles.length === 0) {
        return resolve(true);
      } else {
        if (!this.roles || this.roles.length === 0) {
          resolve(false);
        }
        if (requiredRoles.every(role => this.roles.indexOf(role) > -1)) {
          resolve(true);
        }
        else {
          window.alert("Auth Blocked!")
        }
      }
    });
  }
}
```
`route.data.roles` で上記の`app-routing.module.ts`で宣言した `Routes` の `data: {roles:[...]}` が渡されます。this.roles にはログイン後に Keycloak から渡されたロールのリストが入ってきますので、これとのマッチングを行います。
現状では、ログイン後に権限がない場合は `window.alert("Auth Blocked!")` とアラートが出されるようにしております。

実は、コーディングとしては以上となります！AuthGuard の実装は実際のプロジェクトではいろいろ考慮が必要かと思います。。。

## 5. Keycloak の用意

続きまして Keycloak サーバーの準備に取り掛かります。

### 5-1. Keycloak with docker-compose

Keycloak はいつも通り、docker-composeで用意します。

```yml:docker-comopse.yml
version : "3"
services:
  keycloak:
    image: jboss/keycloak:7.0.1
    container_name: keycloak
    restart: always
    ports:
      - 8180:8180
    environment:
      - KEYCLOAK_USER=admin
      - KEYCLOAK_PASSWORD=admin
    volumes:
      - ./ng-keycloak-sample-realm.json:/config/ng-keycloak-sample-realm.json
    command: >
      -b 0.0.0.0
      -Djboss.http.port=8180
      -Dkeycloak.migration.action=import
      -Dkeycloak.migration.provider=singleFile
      -Dkeycloak.migration.file=/config/ng-keycloak-sample-realm.json
      -Dkeycloak.migration.strategy=OVERWRITE_EXISTING
    tty: true

```
Realm:`Angular Sample` と clientId:`ng-keycloak-sample` を設定した json はご用意いたしました！

以下のコマンドで起動します。

```
$ docker-compose up
```

### 5-2. ユーザーの追加

ユーザーのリストを含む設定値の Export がWEBコンソールからではできないようなので、ユーザーの登録とロールの割り当ては手作業でお願いします。すいません。。。

ブラウザから `http://localhost:8180` に `admin/admin` でログインし、手作業でユーザーの登録をお願いします。
以下のサーバー管理ガイドにて詳しく解説されておりますのでご覧ください。

- [User Management | Server Administratrion Guide](https://www.keycloak.org/docs/latest/server_admin/index.html#user-management)

また、以下の記事もご覧ください。

- [Keycloak Integration : Part 2: Integration with Angular Frontend](https://medium.com/@sairamkrish/keycloak-integration-part-2-integration-with-angular-frontend-f2716c696a28)

今回は "user001/user001" に "user" ロール、"user002/user02" に "user" ロールと "admin" ロールを付与して動作確認をしてみたいともいます。

ちなみに、結果はこんな感じになっております。

![Keycloak Admin Console User001.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/335150/54d49346-9e5e-a99e-1ec1-71c50dcd8512.png)

`user001` のロールはクライアント`ng-keycloak-sample` では `user` を割り当てました。
同様に `user002` では `user` と `admin` のロールをアサインしてください。

アサインが終わったら Keycloakから`signout`しておいてください。本物の`admin`でログインした情報が残ってしまいます！

## 6. 動作確認

それではいよいよ、Angular の開発サーバーを起動します。

```
$ ng serve
```

で、ブラウザから `http://localhost:4200` を開いてください。

![NgKeycloakSample.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/335150/d1ff27cf-95de-0cac-e83c-bea7168782ec.png)

MainComponentが表示されています。この時点では Keycloak のログインに行っていないです。~~(KeycloakとAngularのサンプルを検索するとアプリ丸ごとのログインするサンプルが多いので。。。)~~

さて、`Here is User Area` をクリックしてみましょう。

![Log in to Angular Sample.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/335150/3f00146f-e3c1-7446-c041-95ca0526be93.png)

はい！AuthGuard でめでたく Keycloakのログインに遷移しました！
ここで "user001/user001" でログインすると、いったんMainComponentに戻ってきます。(このあたりの動作は改善の余地ありで。。。)
で、再度`Here is User Area` をクリックすると・・・
![NgKeycloakSample (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/335150/64815e32-21e1-d15f-bf82-78ead1a5e961.png)

`dashboard works!` の文字が眩しい、`DashBoardComponent` に無事に遷移しました！
さて、`Back To Main` クリックで MainComponent にいったん戻り、`Here is Admin Area` をクリックすると・・・

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/335150/e65e0551-f8f0-ab9d-c9f6-b88aa9b0be57.png)

**ブロック！！** めでたく遷移がブロックされました！
`user001` は `admin` ロールは持っていないのでブロックされています。
さて、同様に `user002` でアクセスしてみましょうか。

いったん、Log Out をクリックします。

![NgKeycloakSample.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/335150/69742cf4-62a8-76c7-47f4-81a6b08cda16.png)

はい、MainComponent に何の変哲もない戻りました。
そして再度、`Here is Admin Area` をクリックすると・・・

![Log in to Angular Sample.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/335150/f6225ce0-0a29-10fa-b4a1-63e755768740.png)

KeyCloakのログイン画面です。ちゃんとログアウトされていたようですね。
さてここで `user002/user002` でログインします。

![NgKeycloakSample.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/335150/51ab91df-5e3b-85c6-fb1b-0d68b0b5fb44.png)


はい、再びのMainComponent です。

さて、ここで `Here is Admin Area` をクリックすると・・・
![NgKeycloakSample (2).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/335150/44a16735-f943-04e3-d5a0-9e0beeddd04c.png)

**config work!** 来ましたー！ConfigComponent が無事に表示されました。

Keycloak と連携したAuthGuard、ばっちり機能しているようです！

## まとめ

今回は Angular アプリ全体ではなく、一部のページのみの AuthGuard を Kyecloak と連携して行ってみる実験でした。
AuthGuard のページ数が増えるとちょっとコンポーネントの階層など工夫が必要かと思いますが、それほど実装負荷も大きくなく、ロールでの切り替えもバッチリ動くことが確認できました。
これはアリ、ではないでしょうか！？

今回作成したサンプルアプリは github に上げております！

- [ng-keycloak-sample](https://github.com/Yoshinori-Koide-PRO/ng-keycloak-sample)

本日は以上といたします！