【2019年12月版】Blob Storage の Angular で AD B2C の OIDC はそこそこ問題ない。

# AD B2C で Angular と Quarkus の OpenID Connect

前回の記事([【2019年12月版】Quarkus、Azure Functionsに載せるは天国。AD B2C で OIDC は地獄。](https://qiita.com/koinori/items/8a2f1a2be0c15c662fe3))で、AD B2C と Quarkus の OIDC に挑戦いたしました。
この際、クライアントにPostmanを使っておりましたが、今回はAngularによる**本当のクライアントサイドの実装**に挑戦したいと思います。
タイミングよく以下の記事も発見いたしましたので、これを参考に Angular でのクライアント実装、いってみましょう！

- [Using Azure AD B2C with Angular 8](https://about-azure.com/2019/09/26/using-azure-ad-b2c-with-angular-8/)

## 対象環境

Angular : 8.2.14
Azure CLI: 2.0.77

今回は Angular 8 です。

## 1. AD B2C の設定: Angular 用の"アプリの登録"

まずは実装・・・と行きたいところですが、説明の都合上 AD B2C でのアプリケーション登録を先に行いたいと思います。すいません。。。

さて、例によってどうにかして AD B2C で作成したディレクトリの AD B2C のサービス概要のページに辿り着いてください。~~(いきなりがハードル高い。)~~
メニューから "アプリの登録(プレビュー)"をクリックして新規追加いたします。

![Angular アプリケーションの登録 - Microsoft Azure.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/335150/17853dd7-091d-8cab-0c85-0b70a0043068.png)

今回は Angular のWEBクライアントなので "クライアント アプリケーション" を選択します。名前は適当でOKです。
で、作成されたらアプリケーションIDをコピペしておきます。

![Angular-Client 概要 - Microsoft Azure.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/335150/47f089b9-3ac0-2bfc-577e-d0711f0e116b.png)


続いてメニューから "APIアクセスの許可"をクリックし、"アクセス許可の追加"をクリックします。

![Angular API アクセス許可の要求 - Microsoft Azure.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/335150/46a52a13-9b32-5b8b-0b02-b8123d48b95d.png)

"自分のAPI" にて前回の記事で追加した "File.Read" のスコープをクリックして許可に追加し、"許可の追加"をクリックします。

![Angular-Client - 管理者の同意 - Microsoft Azure.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/335150/a5f00b01-e03b-1a48-cb6b-63b6ccb9eedf.png)

追加されたら "管理者の同意"をクリックします。ログインダイアログと承認ダイアログがでますので、承認をしておきます。

![Angular-Client - 管理者の同意後 - Microsoft Azure.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/335150/35686774-033e-97f9-d5dd-c111f07a342e.png)

承認後、状態欄にグリーンのチェックがついたらOKです。

引き続いて、AD B2C のトップのメニューから、"ユーザーフロー(ポリシー)"をクリックし、前回の記事で追加した "B2C_1_susi" フローを選択します。

![ユーザー フローを実行 - Microsoft Azure.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/335150/0663053d-c716-b553-4490-571bd74672e3.png)

概要の"ユーザーフローを実行します"をクリックして、`.../.well-known/openid-configuration`のURLをコピー＆リンクを開き、`issuer` と `token` のエンドポイントのURLをコピペしておきましょう。
後ほど必要なのは `.../.well-known/openid-configuration`　と　`issuer` と `token` のエンドポイントの3つです。

それではやっと、Angular の実装に移りたいと思います。

## 2. Angular の実装

Angular は参考記事の内容、ほぼそのままで行きます。

## 2-1. プロジェクトの作成と準備

まず、CLI からプロジェクトを生成します。
今回は `ng-quarkus-adb2c` という名前で作成します。

```shell
$ ng new ng-quarkus-adb2c
...
$ cd ng-quarkus-adb2c
```
今日のサンプルは1ページだけなので`router`は無しでOKです。

続いて `angular-oauth2-oidc` パッケージを追加します。

```shell
$ npm i angular-oauth2-oidc
```

## 2-2. OIDC の準備

"app.module.ts" にて、OIDCモジュールの初期化を追加します。

```ts:ng-quarkus-adb2c/src/app/app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppRoutingModule } from './app-routing.module';
import { HttpClientModule } from '@angular/common/http';
import { OAuthModule } from 'angular-oauth2-oidc';
import { AppComponent } from './app.component';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    HttpClientModule,
    OAuthModule.forRoot({
      resourceServer: {
        allowedUrls: ['https://addressToFunctionsApp.azurewebsites.net/api/'],
        sendAccessToken: true
      }
    })
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }
```
`OAuthModule`の設定で、アクセストークンを投げる先のURLの`プレフィックス`を列挙できます。
今回は Azure Functions に上げた Quarkus APIのアドレスを指定します。

続いて接続設定を定義します。`app.module.ts` と同じフォルダに `auth.config.ts` を作成します。

```ts:ng-quarkus-adb2c/src/app/auth.config.tsauth.config.ts
import { AuthConfig } from 'angular-oauth2-oidc';

export const DiscoveryDocumentConfig = {
  url: "https://xxxxxxxxxxxx.b2clogin.com/tfp/xxxxxxxxxxx.onmicrosoft.com/b2c_1_susi/v2.0/.well-known/openid-configuration"
}

export const authConfig: AuthConfig = {
  redirectUri: window.location.origin,
  responseType: 'token id_token',
  issuer: 'https://xxxxxx.b2clogin.com/tfp/xxxxxxxxxxxx/b2c_1_susi/v2.0/',
  strictDiscoveryDocumentValidation: false,
  tokenEndpoint: 'https://xxxxxxxxxx.b2clogin.com/xxxxxxxxxxxxx.onmicrosoft.com/b2c_1_susi/oauth2/v2.0/token',
  loginUrl: 'https://xxxxxxxxxxxxxx.b2clogin.com/xxxxxxxxxxxxx.onmicrosoft.com/b2c_1_susi/oauth2/v2.0/token',
  clientId: 'xxxxxxxxxxxxxxxxxxxxxxxx',
  scope: 'openid profile https://xxxxxxxxxxxxxxxx.onmicrosoft.com/xxxxxxxxxxxxxx/File.Read',
  skipIssuerCheck: true,
  clearHashAfterLogin: true,
  oidc: true,
```

設定値は以下のようになります。

- `DiscoveryDocumentConfig` に `.well-known/openid-configuration` のURL
- `issuer` に issuer のエンドポイント URL
- `tokenEndpoint`、`loginUrl` に token のエンドポイント URL
- `clientId` にアプリケーションID
- `scope` に "openid profile " と**例のスコープのURI**

`responseType` の設定でいろいろ挙動が変わります。これは各自、実験してみてください。とりあえず `token id_token` で行きます。

### 2-3. 画面の実装

`app.componet.ts` をサンプル+αで以下のようにします。

```ts:ng-quarkus-adb2c/src/app/app.component.ts
import { Component } from '@angular/core';

import { OAuthService, NullValidationHandler } from 'angular-oauth2-oidc';
import { authConfig, DiscoveryDocumentConfig } from './auth.config';
import { HttpClient } from '@angular/common/http';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent {
  title = 'ng-quarkus-adb2c';
  constructor(private http: HttpClient, private oauthService: OAuthService) {
    this.configure();
    this.oauthService.tryLoginImplicitFlow();
  }

  message: string;

  public getMessage() {
    this.http.get("https://addressToFunctionsApp.azurewebsites.net/api/hello", { responseType: 'text' })
      .subscribe(r => {
        this.message = r
        console.log("message: ", this.message);
      });
  }

  public login() {
    this.oauthService.initLoginFlow();
  }

  public logout() {
    this.oauthService.logOut();
  }

  public get claims() {
    let claims = this.oauthService.getIdentityClaims();
    return claims;

  }

  public get accToken() {
    return this.oauthService.getAccessToken();
  }

  public get idToken() {
    return this.oauthService.getIdToken();
  }

  public get scopes() {
    return this.oauthService.getGrantedScopes();
  }

  private configure() {
    this.oauthService.configure(authConfig);
    this.oauthService.tokenValidationHandler = new NullValidationHandler();
    this.oauthService.loadDiscoveryDocument(DiscoveryDocumentConfig.url);
  }
}

```
`getMessage()`メソッド内で関数アプリを呼び出します。ここがうまくいくかどうかがポイントです。
また、画面上でアクセストークンとIDトークンを表示できるように get メソッド追加してみました。完全に実験用ですから！

さて、`app.component.html` の方もサンプル+αで以下のようにします。

```html:ng-quarkus-adb2c/src/app/app.component.html
<h1 *ngIf="!claims">
  Hi!
</h1>

<h1 *ngIf="claims">
  Hi, {{claims.given_name}}!
</h1>

<h2 *ngIf="claims">Your Claims:</h2>

<pre *ngIf="claims">
  {{claims | json}}
  </pre>
<br />

<h2 *ngIf="accToken">Your AccessToken:</h2>

<pre *ngIf="accToken">
  {{accToken | json}}
  </pre>
<br />

<h2 *ngIf="idToken">Your idToken:</h2>

<pre *ngIf="idToken">
  {{idToken | json}}
  </pre>
<br />

<div *ngIf="!claims">
  <button (click)="login()">Login</button>
</div>

<div *ngIf="claims || accToken || idToken || scopes">
  <button (click)="logout()">Logout</button>
  <button (click)="getMessage()">API Call</button>
  <div *ngIf="message">
    Response:
    {{message | json}}
  </div>
</div>
```
接続設定の `responseType` でいろいろ実験できるように、画面にプロファイルやアクセストークンを表示するようにしてみました。例によって [https://jwt.io/](https://jwt.io/) で確認すると勉強になります。

さて、Angular の実装は以上です！

## 3. Blob Storage に上げる

続いて、Angular のソースをデプロイする先を作成いたしましょう。

### 3-1. "ストレージアカウント" の作成

Azure ではストレージ系のサービスは個別ではなく"アカウント"にまとまっているようです。
というわけでまずは"ストレージアカウント"を作成したいのですが・・・

![ディレクトリ切り替え - Microsoft Azure.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/335150/931c01bf-9325-0cc7-f1ff-f6ac6bb932d8.png)

はい、AD B2C 用のディレクトリからサブスクリプションの割り当てらているディレクトリに移動してからです。。。

ディレクトリを切り替えた後に、"新規リソースの追加"で"ストレージアカウント"までどうにか辿り着いてください。

![ストレージ アカウントの作成 - Microsoft Azure.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/335150/b3e42b54-9583-8e41-d586-3a0af34aea31.png)

ここで"作成"をクリックします。

![ストレージ アカウントの新規作成 - Microsoft Azure (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/335150/bbda651c-5b60-1c2d-fb47-dea14f597c5a.png)

オプションはデフォルトで大丈夫ですが、"アカウントの種類"が"StorageV2" であることを確認してください。V2 でないとメニュー構成が異なってきます。。。

![ストレージ アカウントの作成後 - Microsoft Azure.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/335150/c53d5680-87ad-aecf-3f4d-3769b718c8de.png)

作成後、しばらくデプロイ作業が続きます。デプロイ完了後、"リソースに移動"をクリックします。

### 3-2. 静的サイトの作成

"リソースに移動"からストレージアカウントの概要に遷移してきたら、左側のメニューから"静的なWEBサイト" をクリックします。最初、画面には表示されていませんのでスクロールしてください。(というかストレージだけでどんだけメニューあるんじゃい。。。)

![静的な Web サイトの作成 - Microsoft Azure.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/335150/e5407a42-e9a7-e8b8-ec72-daccc4176316.png)

"静的なWEBサイト"を"有効"にし、"インデックスドキュメント名" に "index.html" を指定します。
設定が終わったら"保存"をクリックすると、WEBサイト用のコンテンツの入れ物である `$web`という"コンテナ" が作成されます。

![静的な Web サイト作成結果 - Microsoft Azure.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/335150/c5bca2d1-457e-a694-e20c-d87db982daaa.png)

この"プライマリエンドポイント"がAngular のサイトとなります。

### 3-3. デプロイ

それでは Angular 側のソースをパッケージしてデプロイしましょう！
以下のコマンドとなります。

```shell
$ npm run build --prod
...
$ az storage blob upload-batch -d '$web' --account-name xxxxxxxxxxxxxxx -s ./dist/ng-quarkus-adb2c
Finished[#############################################################]  100.0000%
...
```

`$web` というコンテナ名は明らかに不審な名前でシェルとの相性もバッチリ最悪です。(だって解釈されちゃうし)
というわけで明示的に `'$web'` とシングルクォートしてあげてください。

- [storage blob upload-batch fails with InvalidQueryParameterValue on Ubuntu](https://github.com/Azure/azure-cli/issues/10131)

最初、自分も引っかかりましたw

## 4. 動作確認・・・の準備

さて、アクセスしてみましょう！・・・の前にもう少しだけ初回の調整が必要です。

### 4-1. AD B2C の設定

AD B2C で Angular のアプリを登録しましたが、この時点では WebサイトのURLが未定でした。
以下の手順でサイトのURLを追加します。

まずは、AD B2C の"ディレクトリ"に切り替え、AD B2Cリソースの概要ページになんとかして行きます。
"アプリの登録(プレビュー)"からAngularのアプリを選択し、アプリのメニューから"認証"をクリックします。

![プラットフォームの追加Microsoft Azure.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/335150/14ad2816-5f68-8afb-cb95-416763b1db07.png)

認証画面から"プラットフォームの追加"、"Web" とクリックしていき・・・

![Web の構成 - Microsoft Azure.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/335150/17825226-8c44-cda4-ac64-0b73f4e76e1b.png)

WEB の構成画面で URL を追加して、"暗黙の付与"にてアクセストークンとIDトークンを追加し、(Quarkusはアクセストークンだけでよいかも？)、"構成"をクリックで保存されます。

### 4-2. Functions の CORS 設定

続いては Functions の CORS です。これが最後です！

まず、AD B2C のディレクトリから 関数アプリのリソースがあるディレクトリに切り替えた後、関数アプリのリストから Quarkus 用のアプリを選択します。

![関数アプリ概要 - Microsoft Azure.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/335150/9b2308f6-0aa2-4aee-2104-eab5007de4e2.png)

関数アプリの概要画面が開くので、"プラットフォーム機能"のタブをクリックし、"CORS" をクリックします。

![CORS - Microsoft Azure.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/335150/ac90b3cb-435e-1e9f-6162-7d059c301e2e.png)

"CORS" 設定のダイアログが表示されますので、"Access-Allow..."の設定を"有効"にして、AngularサイトのURLをリストに追加し、"保存"します。

これで完了です！

## 5. ブラウザからアクセス

それではデプロイされた Angular アプリをブラウザから開いてみましょう。

![NgQuarkusAdb2c Start.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/335150/ae078427-f2d4-9b42-b5bd-fb72d5b69004.png)

最初はシンプルです。。。"Login" をクリックしてください。

![Sign up or sign in.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/335150/e86fe981-9a14-3060-d79a-456a43b7b398.png)

Azure AD B2C のログイン画面が表示されました。ちゃんとOIDCプラグイン、効いてますね。。。
ここでサインインをするか、[sute.jp](https://siute.jp) で適当なアドレスを作ってサインアップするかでちゃんとログインが成功いたしますと・・・

![NgQuarkusAdb2c Login.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/335150/e7e4d59c-07cc-5e35-fc45-831ac3d043fd.png)

AD B2C で取得できたユーザー情報、トークンなどが表示されます。ぼかしだらけでなんだかわからないページになっておりますが・・・
アクセストークンがゲットできたところで"API Call"をクリック！

![NgQuarkusAdb2c API Response.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/335150/9973347e-0702-2b6d-3eb3-073ddda95444.png)

はいっ！ Functions上の Quarkus から`hello jaxrs`とのレスポンスが返ってきました！！
Functions のAPIですがタイミングによっては寝ていることもあるので、応答が全くない場合は再度、クリックしてみてください。

お疲れさまでしたー！

## まとめ

というわけで、若干のトラップはありましたが Functions に上げた Quarkus との OIDC での　SSO が確認できました。
なによりAD B2C というか Azure 全般的に設定がちりばめられているので、操作が大変です。
そろそろ **Terraform の導入**するかな・・・ｗ

今回、作成したAngularのクライアントは前回のQuarkusのAPIと同じリポジトリに追加いたしました。ご参考までにどうぞ～

- [quarkus-examples branch azure-functions](https://github.com/Yoshinori-Koide-PRO/quarkus-examples/tree/azure-functions)

今回はここまでといたします。