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

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

AADSTS501051 <ClientId> is not assigned to a role for the application への対処

出るたびに対応を忘れて調べているのでメモ。

Microsoft.Identity.ClientConfidentialClientApplicationBuilderでシークレットを使用したアクセストークンの取得を行いたいときに発生する場合の対応です。

はじめに

2021年7月時点の情報です。

参照時期によっては記事内の画面キャプチャや設定内容が異なっている可能性があるのでご留意ください。

何が起きているのか

エラーは下記のような内容です。

MsalUiRequiredException: AADSTS501051: Application 'Client Id'(AAD App name) is not assigned to a role for the application 'Scopes'(AAD App name).

僕がよくやる構成の話です。

Entiprise Applications上で認証に使用しているAzure ADアプリケーションの設定で

ユーザー割当必須にしている場合があります。

f:id:TakasDev:20210711111246p:plain

雑にサインインの制御を行いたい場合(ゲストユーザーのみとかその逆とか)の設定で

シークレット認証の場合ユーザーの情報を伴わない状態なのでエラーになる。というわけです。

対処

App Roleの作成

認証を行いたいAzure ADアプリケーションのApp rolesでアプリケーションのRole作成を行います。

項目 設定内容
Display name 任意な名前
Allowed member types Applications
Value 任意な値
Description Roleの説明文

APIのアクセス許可の設定

次にシークレット認証用のAzure ADアプリケーションを用意します。

このアプリケーションはユーザー割当必須の設定は行いません。

API permissionsから認証を行いたいAzureADアプリケーションへのアクセス許可の設定を行います。

先程設定を行った認証を行いたいAzure ADアプリケーションを指定します。

Appliaction permissionsで先程作成したApp Roleを指定します。

f:id:TakasDev:20210711113440p:plain

シークレットによる認証が可能となる

シークレット認証用のAzure ADアプリケーションでシークレットを生成し確認を行います。

f:id:TakasDev:20210711114527p:plain

モザイクだらけでアレですが、audが適切な形となっていること

rolesは指定したアプリケーションロールが設定されていることが確認できます。

おわりに

ちょいちょい発生しては対処方法を忘れているAADSTS501051のメモでした。

Azure Web PubSub ServiceのイベントをトリガーにAzure Funcrtionsを実行する

はじめに

2021年7月時点の内容です。

また、AzureWebPubSubServiceはプレビュー版ということもあり

記載している画面やソースコードや設定内容は参照時点によっては異なっている可能性が高いので

参照される際はご留意ください。

今回は.NETラボのセッション資料中で時間の関係上セッションに落とせなかった部分のメモ書きをこちらに落としておきます。

基本などはイベントでお話しました。

まず基本的なところから抑えたいという方は、資料などをご覧いただければと思います。

PubSubイベントをトリガーにFunctionsを実行したい

Azure Web PubSub Serviceの設定

Azure Web PubSub Serviceで起きたイベントをFunctionsで処理したいと思います。

ドキュメントはこちらから確認できます。

Azure Web PubSub ServiceのSettingsから接続先のURLを指定することで

イベントをトリガーにFunctionsを実行できるようになります。

URLはFunctionsのExtensionを使う場合(エンドポイント + /runtime/webhooks/webpubsub)となります。

設定内容は大まかに下図のとおりです。

f:id:TakasDev:20210705220630p:plain

とりあえず動きを確かめたい程度なら、SystemEventsは何も設定しなくてOKです。

Azure Functionsの実装

NuGetでMicrosoft.Azure.WebJobs.Extensions.WebPubSubを取得します。

Functionsでは下記を実装することでメッセージの送信をトリガーにした処理が可能になります。

Hub=…で指定された箇所が上図のHub nameと対応する箇所です。

[FunctionName("<Function名>")]
public static async Task<MessageResponse> Broadcast(
    [WebPubSubTrigger(WebPubSubEventType.User, "message")] BinaryData message,
    [WebPubSub(Hub = "SampleHub")] IAsyncCollector<WebPubSubOperation> operations
)
{
    // なんやかんや
}

PubSubServiceで使用されるwsswss://<resource name>/client/hubs/<hub name>?access_token=...の形式で

↑のWSSで指定されるhub nameに対する通信のイベントがトリガーとなる。といった流れです。

System Eventsをトリガーにする

設定のSystem Eventsの各項目にチェックを入れた場合は下図のコードでトリガー実行可能となります。

WebPubSubTriggerの第3引数がそれぞれ、connect/connectedのいずれかになります。

チェックを入れた方(あるいは両方)を実装します。

[FunctionName("<Function名>")]
public static ServiceResponse Connect(
    [WebPubSubTrigger("SampleHub", WebPubSubEventType.System, "connect")] ConnectionContext connectionContext,
    ILogger log
)
{
  // なんやかんや
}

[FunctionName("<Function名>")]
public static async Task Connected(
    [WebPubSubTrigger(WebPubSubEventType.System, "connected")] ConnectionContext connectionContext,
    [WebPubSub] IAsyncCollector<WebPubSubOperation> operations
)
{
  // なんやかんや
}

気をつけなければならないこと

当然といえば当然なのですが、message含めSettingsでURLを指定した場合

トリガーで実行される関数がない場合エラーとなり、Connectやメッセージの送信が失敗するようになります。

さいごに

Azure Web PubSub Serviceで発生する各イベントをトリガーにFunctionsで実行することができ

Connect前後にフローを追加したり、Messageに情報を付加して返却したりなどが簡単にできるようになっていることが確認できました。

発表や今回の記事で使用したサンプルソースは下記となります。

github.com

メモ書き-msal.jsでSSOしたらiOSやChromeでエラーが発生したので対応した話

はじめに

下記のライブラリのバージョンで発生したエラーに対する対応となります。

参照される時期によっては対応方法や対応不要になったりと状況が変わっている可能性がありますのでご留意ください。

  • @azure/msal-browser: 2.13.1

(状況変わっているのが一番うれしい

iOSChromeでSSOしたときにエラーが発生する

すでにadal.jsを使用しているWebアプリケーションやほかシステムでログインが行われている前提であった場合msal.jsの ssoSilent はよく使用する機能かもしれません。

SSO SilentはセッションCookieを利用しているのですが

iOSChromeのシークレットブラウザを使用した場合サードパーティCookieを参照できないため ssoSilentで下記のようなエラーとなってしまいます。

InteractionRequiredAuthError: login_required: AADSTS50058: A silent sign-in request was sent but no user is signed in. The cookies used to represent the user's session were not sent in the request to Azure AD. This can happen if the user is using Internet Explorer or Edge, and the web app sending the silent sign-in request is in different IE security zone than the Azure AD endpoint (login.microsoftonline.com).

シングル サインオン (MSAL.js) - Microsoft identity platform | Microsoft Docs

この問題に対する対応

対応策1:ChromeiOSの設定を変更する

てっとりばやいのはChromeの下記設定をOffにする

f:id:TakasDev:20210404153527p:plain

iOSの下記設定をOffにすることです。

f:id:TakasDev:20210404154042p:plain

しかし、あえてセキュリティ上守られているものを解除する方に寄せていくのもよろしくありません。

ここの設定を変更するのは原因の切り分けを行うときくらいにとどめておいたほうが良いと思っています。

対応策2:エラー発生時にLoginRedirect/LoginPopupを行うように対応する

エラーの内容としては「ちゃんとログインしてくださいね」ということなので、別途ログインするだけで良さそうです。

UserAgentなどで特定ブラウザだけloginRedirectすることも考えましたが

対応しなければいけないブラウザが増える可能性もありますし

ssoSilentしたときでAADSTS50058が出たときに対応したい。と状況は限定的なので

ssoSilentでエラーをキャッチしそこでloginRedirectなりloiginPopupを行うように対応してみました。

Popupを使用する場合は↓な感じです。

  async login(): Promise<AuthenticationResult> {
    const res = await this.client.ssoSilent({ loginHint: loginhint }).catch((err: AuthError) => {
      if(err.errorMessage.includes('AADSTS50058')) {
        return this.client.loginPopup();
      }
      throw err;
    });
    this.account = res.account;
    return res;
  }

AADSTS50058発生しているときだけloginPopupするようにしています。

Redirectを使用する場合は少し手間です。

RedirectされたときにキャッチするhandleRedirectPromiseをエラー発生時にだけ使用したいからです。

  async login(): Promise<AuthenticationResult | null> {
    const errFlow = sessionStorage.getItem('errflowaction');
    if (errFlow) {
      const res = await this.client.handleRedirectPromise();
      if (res) {
        this.account = res.account;
      }
      sessionStorage.removeItem('errflowaction');
      return res;
    }
    const res = await this.client.ssoSilent({ loginHint: loginhint }).catch((err: AuthError) => {
      if(err.errorMessage.includes('AADSTS50058')) {
        sessionStorage.setItem('errflowaction', 'dummy');
        this.client.loginRedirect();
        return null;
      }
      throw err;
    });
    if (res) {
      this.account = res.account;
      return res;
    }
    return null;
  }

少し不格好ですが、Redirect前にsessionStorageにFlow状態を記録しておき

それによって動作を変える方法で逃げることにしました。

さいごに

実験に使ったコードは以下に格納しています。

github.com

メモ書き - AzureDevOpsで別プロジェクトのCIをトリガーにパイプラインを実行する

はじめに

このメモは2021年1月17日時点のAzure DevOpsを使用したメモになっています。

参照されるタイミングによっては、記事内で称しているキャプチャ・設定内容が変更されている可能性がありますのでご注意ください。

やりたいこと

Docsみててこんがらがったので自分用メモ・・・

Azure DevOpsで複数のプロジェクトを構築。

各プロジェクトの構成は下な感じで。

  • 統合プロジェクト
    • サブプロジェクト1
    • サブプロジェクト2
    • ...

サブプロジェクトでライブラリ作成→統合プロジェクトでサブプロジェクトのライブラリを使用してクライアントアプリの開発。といった構成。

サブプロジェクトxのCIが成功→統合プロジェクトのCIを実行 な感じで動作させたい。

統合プロジェクトのパイプラインの構築

サブプロジェクト側のパイプラインは今まで通りの構成で問題なし。

統合プロジェクト側のyamlの頭に👇の構成。

trigger: none

resources:
  pipelines:
  - pipeline: subProjectPipeline  # パイプラインの名称。このCIからアクセスするときの識別子
    source: 'integration-pipeline-two (1)'  # トリガーするパイプライン
    project: integration-pipeline-two  # 別プロジェクトにあるCIの場合はこれを指定
    trigger:
      branches:
      - master

サブプロジェクトでPublisしたものをDownloadしたいときは👇でDLできる。

- download: subProjectPipeline  # pipelineで指定した識別子
  artifact: drop  # Artifact名

補足

Releasesを使用する場合、👇の2つをOffにしないと権限なしでサブプロジェクトでPublishされたリソースにアクセスできないとかあったけど、Pipelinesではそういうものはなさげ。

(まぁReleases Pipeline特有の設定っぽいからそらそうなんだけど、PipelineのほうはこういうのOffにする必要ないんだ。楽だなぁ。と。)

f:id:TakasDev:20210117135717p:plain

参考にしたサイト等

実験で使用したAzure DevOpsプロジェクト

Azure DevOpsのPipelineでOWASP ZAPを実行してみる

はじめに

2020年12月時点の情報で記事を作成しています。

参照される時期によっては、記事内で使用されているコマンド、画面キャプチャが使用できなくなっている可能性がありますのでご留意ください。

Azure PipelineでOWASP ZAPを実行したい

だいぶ前にGitHubActionsでOWASP ZAPのScanができるようになりました

例のごとくAzurePipelineでは使用できません。

Azure Pipeline上でOWASP ZAPのスキャンを使用してみましょう。

ZAP Dockerを使用する

OWASP ZAPがDockerファイルを提供しています。

Pipeline上でDockerコマンドが実行できるのでそれを使用してPipeline上で脆弱性チェックを行います。

と、いってもZAP Dockerのコマンド等の類の説明は山程あると思いますので省きます。

今回はDocker内で提供されているFullScanを使用しますが

細かい機能を使用したい場合はPowerShell経由でZAP APIを叩いて

Context作成→ContextにURL追加→Spider実行ないつもの流れを行えばいいと思います。

OWASP ZAP2.7でzap-API を使ってSpiderの実行 - 備忘録/にわかエンジニアが好きなように書く

WebAPIをPowerShellからテストする - Qiita

Azure Pipeline上で実行する

すでにPipeline上で実行する記事を書かれている方がいるのでそれを参考にしてみます。

How to run OWASP ZAP Security Tests Part of Azure DevOps CI/CD Pipeline

この記事ではReleaseパイプライン上で実行されているので、MultiStagePipeline上で実行できるようにいじってみます。

また、結果レポートはAzure Artifactsに格納されていますが、すこしアクセスしづらいのでBLOB上に格納してみます。

Pipeline構成

下記な構成のymlとなります

  1. SPAのWebApplicationをビルド
  2. Angularアプリケーションをビルドします
  3. WebAppsにデプロイ 1, デプロイ先のWebAppsの脆弱性調査+Report出力
trigger:
  branches:
    include:
    - master

stages:
- stage: build
  jobs:
  - job: build_job
    displayName: Build Angular
    pool:
      vmImage: ubuntu-latest
    steps:
    - task: npm@1
      displayName: npm ci
      inputs:
        command: custom
        customCommand: 'ci'
    - task: npm@1
      displayName: npm build
      inputs:
        command: custom
        customCommand: 'run build:ci'
    - task: ArchiveFiles@2
      displayName: 'Archive dist/pipeline-learn-front'
      inputs:
        rootFolderOrFile: 'dist/pipeline-learn-front'
        includeRootFolder: false
        archiveFile: '$(Build.ArtifactStagingDirectory)/drop.zip'
    - task: PublishBuildArtifacts@1
      displayName: 'Publish Artifact: drop'

- stage: deploy
  dependsOn: build
  jobs:
    - deployment: deploy_webapp
      displayName: Deploy WebApp
      environment: deploy
      strategy:
        runOnce:
          preDeploy:
            steps:
              - download: current
                artifact: drop
          deploy:
            steps:
              - task: AzureRmWebAppDeployment@4
                inputs:
                  ConnectionType: 'AzureRM'
                  azureSubscription: '***'
                  appType: 'webApp'
                  WebAppName: '***'
                  packageForLinux: '$(Pipeline.Workspace)/**/*.zip'

- stage: security_test
  dependsOn: deploy
  jobs:
  - job: security_test
    displayName: SecurityTest
    pool:
      vmImage: ubuntu-latest
    steps:
    - task: DockerInstaller@0
      inputs:
        dockerVersion: '17.09.0-ce'
    - task: Bash@3
      inputs:
        targetType: 'inline'
        script: |
          chmod -R 777  ./
          docker run --rm -v $(pwd):/zap/wrk/:rw -t owasp/zap2docker-stable zap-full-scan.py -t https://okawa-test-webapp.azurewebsites.net/ -j -g gen.conf -x OWASP-ZAP-Report.xml -r scan-report.html
          true
    - task: PowerShell@2
      inputs:
        targetType: 'inline'
        script: |
          $XslPath = "$($Env:SYSTEM_DEFAULTWORKINGDIRECTORY)/OWASPToNUnit3.xslt"
          $XslPath
          $XmlInputPath = "$($Env:SYSTEM_DEFAULTWORKINGDIRECTORY)/OWASP-ZAP-Report.xml"
          $XmlInputPath
          $XmlOutputPath = "$($Env:SYSTEM_DEFAULTWORKINGDIRECTORY)/Converted-OWASP-ZAP-Report.xml"
          $XmlOutputPath
          $XslTransform = New-Object System.Xml.Xsl.XslCompiledTransform
          $XslTransform.Load($XslPath)
          $XslTransform.Transform($XmlInputPath, $XmlOutputPath)
    - task: PublishTestResults@2
      inputs:
        testResultsFormat: 'NUnit'
        testResultsFiles: 'Converted-OWASP-ZAP-Report.xml'
        searchFolder: '$(System.DefaultWorkingDirectory)'
    - task: AzurePowerShell@5
      inputs:
        azureSubscription: '***'
        ScriptType: 'InlineScript'
        azurePowerShellVersion: latestVersion
        Inline: |
          $storage = Get-AzStorageAccount -ResourceGroupName "vse-sandbox" -Name "***"
          $ctx = $storage.Context
          $containerName = "zap-result"
          Set-AzStorageBlobContent -File "$($Env:SYSTEM_DEFAULTWORKINGDIRECTORY)/scan-report.html" -Container $containerName -Blob "scan-report.html" -Context $ctx

結果

AzureのCIレポートでテスト結果を確認できるように、レポート出力されたXMLファイルをNUnit形式に変換しています。

結果、下図のようにCIのレポートで発見された脆弱性のレポートを確認できるようになっている感じです。

f:id:TakasDev:20201230132000p:plain

HTMLで出力されたレポートを見たい場合はBLOBからですね。

f:id:TakasDev:20201230132611p:plain

Azure Artifactsに上げる場合のハマりどころ

今回はBLOBにあげてみましたが、参考サイトにある通りAzure Artifactsにあげようとした場合にハマったポイントがありました。

準備段階で、AzureArtifactsにaz artifacts universalを使用してArtifactsを作っているのですが

azコマンドからは403が出てしまいPublishできないといった現象がおきました。

vsts CLIを使用した場合にはエラーは発生しなかったので、azコマンドでエラーが発生した場合はvsts CLIを使用してみるといいかもしれません。

まとめ

今回は雑にCIに組み込めるか程度のレベルで試してみました。

AngularアプリのようなSPA構成のアプリの場合は単純なSpiderではなくAjaxスパイダーを使用したり

試行時間を決定しないと永遠に終わらなかったり…と考慮することは多そうなので

実際にしっかり運用するとなったら直接APIを叩いてガリガリ組んでいくしかないかなと思います。

と言っても、知らん間にガチ脆弱性チェックを行ってデータが壊れるのも色々嫌な感じですし

サーバーの設定レベルだけチェックするような現在の構成のほうが、自動実行するレベルとしては扱いやすいのかもしれないですね。

SubscriptionのLifecycleNotificationUrlをいじってみた

Subsriptionのライフサイクル通知が存在するようです。

サブスクリプションと変更通知の消失を減らす

Docsの履歴を見る感じだと機能自体は2020年の7月くらいに使用できるようになってたみたいなのですが

Microsoft Graphの変更ログに上がってきていなかったため気づくことができなかったようです。無念。

さて、その機能のおかげでSubscriptionの通知のライフサイクルの管理問題が解決することができるかも…?と思ったので試してみました。

(通知のライフサイクルが切れたり…な管理を楽にできれば良いなー。。。的な

詳細な使い方を見ていこうと思います。

ライフサイクル管理を行う

Subsriptionを作成する際にライフサイクル通知の通知先としてlifecycleNotificationUrlを指定。

この際、通常の変更通知でやるのと同様に、エンドポイントの検証を実装。

加えて、通常の通知先とライフサイクル通知の通知先は同じホスト名を指定する必要があるようです。

既存のSubscriptionにPATCHメソッドを使用してライフサイクル通知先を指定することはできないようなので

既存の変更通知がある場合は作成し直すしかなさそうです。

さて、例のごとくFunctionsに通知先を作成し、動作を確認してみようと思います。

ホスト名が違う状態を指定してみる

どんなエラー出るのか試したかったんですが…できちゃったんですよねぇ…

f:id:TakasDev:20201103121706p:plain

Functionsのログを見てみます。

f:id:TakasDev:20201103122727p:plain

ResourceNotifications(通常の通知先)は問題なく通知が来ているようですので別エンドポイントでもワンチャン動くかもしれません。

ひとまず以降の処理でおかしなことになるかもなので、同一のエンドポイントにしてから検証を続行していきます。

どんなときにLifecycle通知が発生するのか色々試してみる

有効期限後のログを見てみる

"expirationDateTime": "2020-11-03T03:30:00Z",を指定しているので12:30以降にどのようなログが出力されるか確認してみます。

  1. 変更通知作成
  2. 通知時間切れまでまつ
  3. 予定情報変更

有効期限後についてはLifecycle通知は行われませんでした。

有効期限切れも通知してくれたらexpirationDateTimeの管理も楽になると思ったのですが残念ですね。

手動でSubscriptionを削除してみる

手動で作成した変更通知を削除したあと、Lifecycle通知が捕捉できるか試してみます。

  1. 変更通知作成
  2. 1.で作成した変更通知を削除
  3. 予定情報変更

手動削除もLifecycle変更通知は捕捉できませんでした。

ユーザーのパスワードを変更/削除してみる

Documentには下記の条件のときのみに発生すると記載されています。

  • ユーザーのパスワードがリセットされた場合
  • ユーザーのデバイスが準拠しなくなった場合
  • ユーザーのアカウントが取り消された場合

意図しないSubscriptionの削除を補足する。といった機能のようですね。

そこでユーザーのパスワードを変更する方法で動作を検証してみます。

変更通知は下記の通りの内容で設定します。特定ユーザーの予定作成/変更時です。

このユーザーのパスワードをリセットを行うことでLifecycleNotificationが呼び出されるか検証します。

順序としては下記のとおりです。

  1. 変更通知作成
  2. 変更通知を作成したユーザーのパスワードリセット
  3. 予定情報変更

結果、下記のようなJSONがLifecycleNotification側に通知されました。

{
  "value": [
    {
      "subscriptionId": "6482c2c2-96ba-4505-b375-4329c1aad3d4",
      "subscriptionExpirationDateTime": "2020-11-24T16:00:00-08:00",
      "clientState": "<state>",
      "tenantId": "<tenantId>",
      "lifecycleEvent": "subscriptionRemoved"
    }
  ]
}

通常のNotification側への変更通知は発生せず、Lifecycleの通知だけ発生していることも確認できました。

f:id:TakasDev:20201123145731p:plain

Microsoft365側の都合による変更通知の削除によってLifecycle通知が発生することが確認できました。

通常通知先を潰してみる

通知先を潰してmissedを捕捉できるか試してみました。

AzureFunctionsに公開しているリソースエンドポイントをリネームして公開しなおしただけです。

ただ、これは捕捉できませんでした。

Document的には、Microsoft365側で配信されなかった変更通知があった場合に呼び出されるようです。

ただ、こちらは先のように具体的な例がなかったのでどのような状況の時に実行されるかはわかりませんでした。

  1. 通知先潰す
  2. Outlookイベント変更
  3. 復活させる
  4. Outlookイベント変更

としたら通知されてない2のデータがでてくる?と思ったけど出てきませんでした。

Microsoft365内で何かしら起きて変更通知の整合性が合わなくなったときに通知されるものかもですね。

まとめ

サブスクリプションと変更通知の消失を減らすで紹介されていた動作を見てきました。

動作させてみて分かった通り、Microsoft365側からのアクションで、動作しなくなった変更通知の通知ということがわかりました。

Microsoft365側からのアクションによる変更通知未実行というのは、補足しにくいものではあるので一定の効果はあると思います。

ただ、通知の有効期限切れや通知先の消失なんかの問題は変わらず自分で管理しないといけない。という状況です。

まだまだApplication Insightなどを使用したり、定期的に変更通知の状況を監視するといった工夫は必要そうです。

実験に使ったソースコードこちらです。