はまったりひらめいたり…とか…

Angularや.NETやAzureやその他色々。

MSAL for Angular v2がGAしたので触ってみたお話

はじめに

この記事は下記の内容でお送りいたします。

  • Angular: v12.1
  • @azure/msal-angular: 2.0.1

参照時期によっては記述しているサンプルコードで動かないない可能性がありますので

その点ご留意ください。

@azure/msal-angularのv2がGAしました

@azure/msal-browserがGAしてから長らくプレビュー状態でしたが

Angularに対応したライブラリがGAされました。

MSAL for Angular v2 is now available - Microsoft 365 Developer Blog

なので、さっそく触ってみたお話になります。

@azure/msal-angular for Azure AD B2C

AADやMS Graphを使用したサンプルはDocsGitHubのリポジトリにありますので

今回はAzure AD B2CとAzure AD B2Cで保護したASP.NET Core WebAPIへのアクセスを実装してみようと思います。

ライブラリのインストール

npm i @azure/msal-browser @azure/msal-angular

msalの設定情報

まずはコアとなるPublicClientApplicationに食わせるための設定情報の作成です。

サンプルでは直接PublicClientApplicationに渡してあるパターンが多いですが、僕はenvironment.ts派です。

export const environment = {
  production: false,
  msalConfig: {
    auth: {
      clientId: '<AD B2C AppのClientId>',
      authority: 'https://<domain>.b2clogin.com/<domain>.onmicrosoft.com/<Signup/in Policy>',
      redirectUri: 'http://localhost:4200',
      knownAuthorities: [
        '<domain>.b2clogin.com'
      ]
    }
  },
  msalInterceptorConfig: [
    { resource: '/base-api/', scopes: ['<WebAPIのAzure AD B2Cスコープ>'] }
  ]
};

基本はmsal-browserの設定内容と同じです。Angularで特殊な箇所はmsalInterceptorConfig部分でしょう。

AngularのHTTP_INTERCEPTORのuseClassで使用できるMsalInterceptorが提供されています。

resourceで指定されたWebAPI宛の通信をHttpClientで行うときに、scopesで指定されたスコープのアクセストークンを取得/通信に付与してくれます。

まいどまいどアクセストークンを取得する処理なんかは書きたくないですし非常に重宝すると思います。

AppModuleの実装

ライブラリが提供するInjectionTokenに対して設定値やPublicClientApplicationインスタンスを設定してあげます。

(インジェクショントークンにこのインスタンスを渡してあげるというのはちょっと意外な構成でした)

// environment.tsの設定値を利用してPublicClientApplicationインスタンスを作成する
const msalInstanceFactory = (): IPublicClientApplication => {
  return new PublicClientApplication(environment.msalConfig);
}

// environment.tsの設定値を利用してMsalInterceptorConfiguration を作成する
const mealInterceptorConfigFactory = (): MsalInterceptorConfiguration => {
  const protectedResourceMap = new Map<string, Array<string>>();
  const conf = environment.msalInterceptorConfig;
  conf.forEach(f => {
    protectedResourceMap.set(f.resource, f.scopes);
  });
  return {
    interactionType: InteractionType.Popup,
    protectedResourceMap
  }
}

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    HttpClientModule
  ],
  providers: [
    {
      provide: MSAL_INSTANCE,
      useFactory: msalInstanceFactory // インスタンスを設定
    },
    {
      provide: MSAL_INTERCEPTOR_CONFIG,
      useFactory: msalInterceptorConfigFactory // インターセプターの設定情報を渡す
    },
    {
      provide: HTTP_INTERCEPTORS,
      useClass: MsalInterceptor, // インターセプターをProvideする
      multi: true
    },
    MsalService, // MsalServiceをProvideする
    MsalBroadcastService // MsalBroadcastServiceをProvideする(ログとか出したい場合)
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

AppComponentの実装

サンプルなんかではログインはボタン押下など明確なアクションをトリガーに実行されるのですが

僕がよくやる実装なんかは、ngOninit時点でログイン済みか検証→ログイン情報なかったらloginRedirect()みたいな実装にすることが多いです。

なのでそのやり方で実装してみようと思います。

export class AppComponent implements OnInit {

  constructor(
    private httpClient: HttpClient,
    private authService: MsalService
  ) {}

  ngOnInit() {
    this.authSettingInit();
  }

  private async authSettingInit() {
    await this.authService.instance.handleRedirectPromise();
    // ↑がないとエラー吐く
    this.authService.handleRedirectObservable().subscribe(x => {
      if (x) {
        console.log({handleRedirectObservable: x})
      }
    });
    const ac = this.authService.instance.getAllAccounts();
    if (ac && ac.length > 0) {
      return;
    }
    this.authService.loginRedirect();
  }

  request(): void {
    this.httpClient.get('/base-api/WeatherForecast').subscribe(x => {
      console.log(x);
    });
  }
}

すこし特殊なのが諸々の実装が走る前にawait this.authService.instance.handleRedirectPromise();をしている点ですね。

これがないとどうなるかというと下記のようなエラーがログイン後に発生します

BrowserAuthError: interaction_in_progress: Interaction is currently in progress. Please ensure that this interaction has been completed before calling an interactive API. For more visit: aka.ms/msaljs/browser-errors.

handleRedirectObservableが諸々の準備が完了する前に呼ばれているからのようです。

対応としては、上のソースで記述している通りで、MsalServiceはPublicClientApplicationのインスタンスinstanceに持っているようなので

それのhandleRedirectPromiseでawaitしてあげると安定して動作します。

というかhandleRedirectPromiseを実行している時点でhandleRedirectObservableはする必要がなくなります。

なので、今回のパターンのようにロード時に即ログイン検証+ログイン処理を行う場合はhandleRedirectObservableではなくhandleRedirectPromiseを使用したほうが良さそうです。

WebAPIと通信

通信を行っている部分は上記ソースコードrequestメソッドです。

/base-api/宛の通信のときに指定スコープのアクセストークンを取得し通信する処理が

HTTP_INTERCEPTORで指定したMsalInterceptorで実行されます。

f:id:TakasDev:20210716000405p:plain

WeatherForecastWebAPI宛の通信にAuthorizationヘッダが付与され、200で返却されることが確認できます。

おわりに

@azure/msal-angularのAzure AD B2Cでの実装の基本をざっと見てみました。

今回はさわりませんでしたがGuardもあり、v1のときに提供されていたAngularライブラリと同じような使用感で使うことができる感触です。

認証周りはmsal-browserをちょこちょこラップするのもなにかと手間なのでAngularを使用している場合は積極的に活用するのが良いと思います。

今回検証で使用したコードは下記となります。サンプルごった煮の一部となっていますm(__)m

github.com