Keycloakのクライアント・アダプターを試してみる（Angular編）

# はじめに
　Keycloakアドベントカレンダー6回目は、JavaScript（クライアントサイド）のクライアント・アダプター（OpenID Connect版）を試してみます。最近では、iOS・Androidのネイティブアプリはもちろんのこと、AngularなどのクライアントサイドMVCフレームワークの利用も増えており、クライアントサイドアプリケーションをセキュアに利用したいユースケースも増えてくると考えられます。今回はAngularを使ったクライアントサイドアプリケーションに対して、JavaScript（クライアントサイド）のクライアント・アダプター（OpenID Connect版）を導入し、Keycloakサーバに接続する方法を試してみます。

# 動作確認用サーバ構成
　今回のお試しのため、OpenID Connect の RP(Relying Party) として、Angularを使ったクライアントサイドアプリケーションの開発に使った環境は、以下の通りです。

   - Node.js：8.9.1
   - Angualr CLI：1.5.3
   - Angular：5.0.3

　また、Keycloakサーバには、`demo`レルムが作成されており([3日目の記事を参照](https://qiita.com/tamura__246/private/4290a1035e1adcab733b))、ログイン可能なユーザとして`user001`が存在していることが前提となります。
　今回のお試し環境はすべてローカルマシン上で実行していますが、それぞれのサーバの役割分担と利用しているポートは以下の通りです。

| サーバ種別 | 役割 | ポート |
|:-------------------------------------|:-------------------|:---------|
|Keycloakサーバ|OIDC OP|8080|
|Angularクライアントサイドアプリケーション|OIDC RP(クライアント)|4200|

# Keycloak側のサーバ設定
## クライアントの追加・設定
1. Keycloakの管理コンソールにログインします。
2. 左メニューバーから、`demo`レルムを選択します。
3. 左メニューバーで「クライアント」をクリックします。クライアント一覧が表示されます。
4. 右上の「作成」ボタンを押下します。
5. 以下のようなクライアント設定値を入力し、「保存」ボタンを押下します。
    - クライアントID：kc-angular
    - クライアントプロトコル：openid-connect

![2017-12-01_150702.png](https://qiita-image-store.s3.amazonaws.com/0/136020/17fa3e90-0162-64f7-ef2a-c917562191fb.png)
6. 引き続き`kc-angular`の設定画面が表示されるので、以下の設定値を入力し「保存」ボタンを押下します。
    - アクセスタイプ：public
    - 有効なリダイレクトURI：http://localhost:4200/
    - Webオリジン：http://localhost:4200

![2017-12-05_201708.png](https://qiita-image-store.s3.amazonaws.com/0/136020/0eade740-6e39-abab-af58-e74f9d7560e7.png)

　「アクセスタイプ」には、クライアントサイドアプリケーションでは安全にクライアントの資格情報を保存する方法が存在しないため、`public`を設定する必要があります。
　「有効なリダイレクトURI」には、ログイン要求でリダイレクト値として使用できるURIを設定します。Keycloakサーバは、リクエストに別のリダイレクトURIを受け取った場合、エラーメッセージとともにログインリクエストを拒否します。
　「Webオリジン」には、どのホストがCORS（Cross-Origin Resource Sharing）要求を行うことが許可されるかを設定します。Webオリジンが設定されていない場合、ブラウザからのすべてのJavaScriptリクエストは、CORSヘッダーの欠落のために失敗してしまいます。

# Angularを使ったクライアントサイドアプリケーションの作成
## Angularアプリケーションの作成
　今回のお試し環境では、ログインしたユーザだけがアクセスできる、クライアントサイドアプリケーションを作成します。まずは、Angular CLI の以下のコマンドを実行し、Angularを使ったクライアントサイドアプリケーションを作成します。

```
ng new frontend
```

## JavaScriptクライアント・アダプターの設定
　JavaScript（クライアントサイド）のクライアント・アダプターを設定するため、`index.html`のheadタグ内に、以下のscriptタグを挿入します。

```
<script src="//localhost:8080/auth/js/keycloak.js"></script>
```
タグを挿入した結果は以下の通りです。

```src/index.html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Frontend</title>
  <base href="/">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico">
  <script src="//localhost:8080/auth/js/keycloak.js"></script>
</head>
<body>
  <app-root></app-root>
</body>
</html>
```
　ここで、JavaScript（クライアントサイド）のクライアント・アダプターの格納先に、`localhost:8080/auth/js/keycloak.js`を指定していますが、Keycloakサーバ内のクライアント・アダプターを指定しています。これは今回のお試し環境がローカルマシン上で動かしているためであり、本番環境などではクライアント・アダプターの格納先を適切に修正してください。

## Keycloakサービスの作成
　Angularを使ったクライアントサイドアプリケーションから、Keycloakサーバに接続するため、サービスを作成します。以下のコマンドを実行し、サービスを作成してください。

```
ng generate service keycloak
```

　このサービスでは、サービスの初期化、ログインしたユーザ情報の取得を行います。
　JavaScriptクライアント・アダプターを利用し、ログインしたユーザだけがアクセスできるようにするため、サービス初期化時に呼び出される`init()`関数を作成します。この`init()`関数では、環境設定ファイルからKeycloakサーバの設定を呼び出し、以下のコードにおいて、KeycloakサーバのURL・レルム・クライアントを設定します。

```
    const keycloakAuth: any = Keycloak({
        url: environment.KEYCLOAK_URL,
        realm: environment.KEYCLOAK_REALM,
        clientId: environment.KEYCLOAK_CLIENTID
    });
```

　また、この`init()`関数では、ログイン機能を呼び出すため、以下のコードにおいて、未ログイン時はKeycloakサーバへのリダイレクトと、ログイン時にはクライアントの認証を行います。このコードではflowパラメータを設定していないため、デフォルトのAuthorization Code flowで動作しますが、この場合にはクライアントの詳細設定画面において、「Standard Flowの有効」を「オン」に設定する必要があります。これらのAPIの詳細な仕様は、[Keycloak Documentation](http://www.keycloak.org/docs/latest/securing_apps/index.html#_javascript_adapter) を参照してください。

```
    keycloakAuth.init({ onLoad: 'login-required' })
```

　さらに、ログインしたユーザの情報を取得するため、`init()`関数に以下のコードの追加と、他のクラスからユーザ情報を取得するため、`getUser()`関数を作成します。

```
    KeycloakService.auth.authz.loadUserProfile()
```

　これらを実装した結果は以下の通りです。

```src/app/keycloak.service.ts
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { User } from './user.model';
import { environment } from '../environments/environment';

declare var Keycloak: any;

@Injectable()
export class KeycloakService {

    static auth: any = {};
    static user: User;

    static init(): Promise<any> {
        const keycloakAuth: any = Keycloak({
            url: environment.KEYCLOAK_URL,
            realm: environment.KEYCLOAK_REALM,
            clientId: environment.KEYCLOAK_CLIENTID
        });

        KeycloakService.auth.loggedIn = false;

        return new Promise((resolve, reject) => {
            keycloakAuth
                .init({ onLoad: 'login-required' })
                .success(() => {
                    KeycloakService.auth.loggedIn = true;
                    KeycloakService.auth.authz = keycloakAuth;
                    KeycloakService.auth.logoutUrl =
                        keycloakAuth.authServerUrl +
                        '/realms/' +
                        environment.KEYCLOAK_REALM +
                        '/protocol/openid-connect/logout?redirect_uri=' +
                        document.baseURI;

                    KeycloakService.auth.authz.loadUserProfile().success(data => {
                        this.user = new User();
                        this.user.username = data.username;
                        resolve();
                    });
                })
                .error(() => {
                    reject();
                });
        });
    }

    getUser(): User {
        return KeycloakService.user;
    }

}
```

　作成したサービスを利用できるようにするため、`src/app/app.module.ts`にサービスを追加します。

```src/app/app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { KeycloakService } from './keycloak.service';
import { AppComponent } from './app.component';

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

　JavaScriptクライアント・アダプターとKeycloakサーバを通信できるようにするため、環境設定ファイルに対してKeycloak側のサーバ設定で追加したクライアント情報（KeycloakサーバのURL・レルム・クライアント）を設定します。

```src/environments/environment.ts
export const environment = {
  production: false,
  KEYCLOAK_URL: 'http://localhost:8080/auth',
  KEYCLOAK_REALM: 'demo',
  KEYCLOAK_CLIENTID: 'kc-angular'
};
```

　また、ログインしたユーザ情報を取得する際に利用するため、ユーザのモデルを定義します。

```src/app/user.model.ts
export class User {
    username: string;
}
```

## Keycloakサービスの初期化
　ログインしたユーザだけがアクセスできるようにするため、Keycloakサービスの初期化を行います。ここでは、ユーザが正常にログインした場合のみ、Angularモジュールを起動します。

```src/main.ts
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
import { KeycloakService } from './app/keycloak.service';

if (environment.production) {
  enableProdMode();
}

KeycloakService.init()
    .then(() => platformBrowserDynamic().bootstrapModule(AppModule))
    .catch(e => {
        console.error(e);
    });
```

## ユーザ情報の取得
　最後に、ログインしたユーザの情報を取得します。起動処理中にログイン処理は完了しているため、アプリケーションコンポーネントの`onInit`フェーズにおいて、ユーザ情報を直接取得します。

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

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
    profile: User;

    constructor(private keycloakService: KeycloakService ) {}

    public ngOnInit(): void {
        this.profile = this.keycloakService.getUser();
    }
    
}
```

　取得したユーザ情報を画面に出力するため、クライアントサイドアプリケーションの画面のHTMLを修正します。

```src/app/app.component.html
<div style="text-align:center">
  <h1>Welcome {{profile.username}}!</h1>
</div>
```


# 動作確認
　次のコマンドで作成した、Angularクライアントサイドアプリケーションを起動します。

```
ng serve
```

　http://localhost:4200 にアクセスすると、Keycloakのログイン画面にリダイレクトされます。

![2017-12-01_151706.png](https://qiita-image-store.s3.amazonaws.com/0/136020/109d9f67-04f9-ddc8-2294-6834d54dc331.png)

　「動作確認用のサーバ構成」に記載されたユーザ`user001`でログインしてください。これまでの設定が適切にできていれば、次のような画面が表示されます。

![2017-12-01_151905.png](https://qiita-image-store.s3.amazonaws.com/0/136020/e52368d4-f793-d590-b4de-54c350c4be31.png)

　このように、画面にログインしたユーザ名が表示され、Keycloakのログインに成功し、ユーザ情報がKeycloakサーバから取得できたことが確認できました。

# まとめ
　本日のKeycloakアドベントカレンダーでは、JavaScript（クライアントサイド）のクライアント・アダプター（OpenID Connect版）を試してみました。Keycloakのドキュメントでは、他にもiOS・Androidのネイティブアプリからの連携方法も記載されていますので、興味がある方は公式ドキュメントを参照ください。


# 参考情報
- [Keycloak Documentation](http://www.keycloak.org/documentation.html)
    -  1.2. [Supported Platforms](http://www.keycloak.org/docs/latest/securing_apps/index.html#supported-platforms)
    -  2.2. [Javascript Adapter](http://www.keycloak.org/docs/latest/securing_apps/index.html#_javascript_adapter
)
- [Angular Quick Start](https://angular.io/guide/quickstart)
- [Angular Tutorial: Tour of Heroes](https://angular.io/tutorial)
- [Authentication with Spring Boot, Angular 2 and Keycloak](http://slackspace.de/articles/authentication-with-spring-boot-angular-2-and-keycloak/)
- [NRI OpenStandia Keycloak日本語ドキュメント](http://keycloak-documentation.openstandia.jp/master/ja_JP/server_admin/index.html#_account-service)