AD B2C で Angular と Quarkus の OpenID Connect
前回の記事(【2019年12月版】Quarkus、Azure Functionsに載せるは天国。AD B2C で OIDC は地獄。)で、AD B2C と Quarkus の OIDC に挑戦いたしました。
この際、クライアントにPostmanを使っておりましたが、今回はAngularによる本当のクライアントサイドの実装に挑戦したいと思います。
タイミングよく以下の記事も発見いたしましたので、これを参考に Angular でのクライアント実装、いってみましょう!
対象環境
Angular : 8.2.14
Azure CLI: 2.0.77
今回は Angular 8 です。
1. AD B2C の設定: Angular 用の"アプリの登録"
まずは実装・・・と行きたいところですが、説明の都合上 AD B2C でのアプリケーション登録を先に行いたいと思います。すいません。。。
さて、例によってどうにかして AD B2C で作成したディレクトリの AD B2C のサービス概要のページに辿り着いてください。(いきなりがハードル高い。)
メニューから "アプリの登録(プレビュー)"をクリックして新規追加いたします。
今回は Angular のWEBクライアントなので "クライアント アプリケーション" を選択します。名前は適当でOKです。
で、作成されたらアプリケーションIDをコピペしておきます。
続いてメニューから "APIアクセスの許可"をクリックし、"アクセス許可の追加"をクリックします。
"自分のAPI" にて前回の記事で追加した "File.Read" のスコープをクリックして許可に追加し、"許可の追加"をクリックします。
追加されたら "管理者の同意"をクリックします。ログインダイアログと承認ダイアログがでますので、承認をしておきます。
承認後、状態欄にグリーンのチェックがついたらOKです。
引き続いて、AD B2C のトップのメニューから、"ユーザーフロー(ポリシー)"をクリックし、前回の記事で追加した "B2C_1_susi" フローを選択します。
概要の"ユーザーフローを実行します"をクリックして、.../.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
という名前で作成します。
$ ng new ng-quarkus-adb2c
...
$ cd ng-quarkus-adb2c
今日のサンプルは1ページだけなのでrouter
は無しでOKです。
続いて angular-oauth2-oidc
パッケージを追加します。
$ npm i angular-oauth2-oidc
2-2. OIDC の準備
"app.module.ts" にて、OIDCモジュールの初期化を追加します。
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
を作成します。
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
をサンプル+αで以下のようにします。
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
の方もサンプル+αで以下のようにします。
<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/ で確認すると勉強になります。
さて、Angular の実装は以上です!
3. Blob Storage に上げる
続いて、Angular のソースをデプロイする先を作成いたしましょう。
3-1. "ストレージアカウント" の作成
Azure ではストレージ系のサービスは個別ではなく"アカウント"にまとまっているようです。
というわけでまずは"ストレージアカウント"を作成したいのですが・・・
はい、AD B2C 用のディレクトリからサブスクリプションの割り当てらているディレクトリに移動してからです。。。
ディレクトリを切り替えた後に、"新規リソースの追加"で"ストレージアカウント"までどうにか辿り着いてください。
ここで"作成"をクリックします。
オプションはデフォルトで大丈夫ですが、"アカウントの種類"が"StorageV2" であることを確認してください。V2 でないとメニュー構成が異なってきます。。。
作成後、しばらくデプロイ作業が続きます。デプロイ完了後、"リソースに移動"をクリックします。
3-2. 静的サイトの作成
"リソースに移動"からストレージアカウントの概要に遷移してきたら、左側のメニューから"静的なWEBサイト" をクリックします。最初、画面には表示されていませんのでスクロールしてください。(というかストレージだけでどんだけメニューあるんじゃい。。。)
"静的なWEBサイト"を"有効"にし、"インデックスドキュメント名" に "index.html" を指定します。
設定が終わったら"保存"をクリックすると、WEBサイト用のコンテンツの入れ物である $web
という"コンテナ" が作成されます。
この"プライマリエンドポイント"がAngular のサイトとなります。
3-3. デプロイ
それでは Angular 側のソースをパッケージしてデプロイしましょう!
以下のコマンドとなります。
$ npm run build --prod
...
$ az storage blob upload-batch -d '$web' --account-name xxxxxxxxxxxxxxx -s ./dist/ng-quarkus-adb2c
Finished[#############################################################] 100.0000%
...
$web
というコンテナ名は明らかに不審な名前でシェルとの相性もバッチリ最悪です。(だって解釈されちゃうし)
というわけで明示的に '$web'
とシングルクォートしてあげてください。
最初、自分も引っかかりましたw
4. 動作確認・・・の準備
さて、アクセスしてみましょう!・・・の前にもう少しだけ初回の調整が必要です。
4-1. AD B2C の設定
AD B2C で Angular のアプリを登録しましたが、この時点では WebサイトのURLが未定でした。
以下の手順でサイトのURLを追加します。
まずは、AD B2C の"ディレクトリ"に切り替え、AD B2Cリソースの概要ページになんとかして行きます。
"アプリの登録(プレビュー)"からAngularのアプリを選択し、アプリのメニューから"認証"をクリックします。
認証画面から"プラットフォームの追加"、"Web" とクリックしていき・・・
WEB の構成画面で URL を追加して、"暗黙の付与"にてアクセストークンとIDトークンを追加し、(Quarkusはアクセストークンだけでよいかも?)、"構成"をクリックで保存されます。
4-2. Functions の CORS 設定
続いては Functions の CORS です。これが最後です!
まず、AD B2C のディレクトリから 関数アプリのリソースがあるディレクトリに切り替えた後、関数アプリのリストから Quarkus 用のアプリを選択します。
関数アプリの概要画面が開くので、"プラットフォーム機能"のタブをクリックし、"CORS" をクリックします。
"CORS" 設定のダイアログが表示されますので、"Access-Allow..."の設定を"有効"にして、AngularサイトのURLをリストに追加し、"保存"します。
これで完了です!
5. ブラウザからアクセス
それではデプロイされた Angular アプリをブラウザから開いてみましょう。
最初はシンプルです。。。"Login" をクリックしてください。
Azure AD B2C のログイン画面が表示されました。ちゃんとOIDCプラグイン、効いてますね。。。
ここでサインインをするか、sute.jp で適当なアドレスを作ってサインアップするかでちゃんとログインが成功いたしますと・・・
AD B2C で取得できたユーザー情報、トークンなどが表示されます。ぼかしだらけでなんだかわからないページになっておりますが・・・
アクセストークンがゲットできたところで"API Call"をクリック!
はいっ! Functions上の Quarkus からhello jaxrs
とのレスポンスが返ってきました!!
Functions のAPIですがタイミングによっては寝ていることもあるので、応答が全くない場合は再度、クリックしてみてください。
お疲れさまでしたー!
まとめ
というわけで、若干のトラップはありましたが Functions に上げた Quarkus との OIDC での SSO が確認できました。
なによりAD B2C というか Azure 全般的に設定がちりばめられているので、操作が大変です。
そろそろ Terraform の導入するかな・・・w
今回、作成したAngularのクライアントは前回のQuarkusのAPIと同じリポジトリに追加いたしました。ご参考までにどうぞ~
今回はここまでといたします。