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

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

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などを使用したり、定期的に変更通知の状況を監視するといった工夫は必要そうです。

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

VisualStudioからAzure KeyVaultにアクセスするときにえらいハマった話

はじめに

今回の記事は下記の環境で検証を行っています。

記事を参照されるタイミングによっては画面構成や設定などが変わる可能性がありますのでご留意ください。

※以降Visual StudioはVSと記載しています

発生した問題

とあるアカウントを使用した場合、VSのConnected Serviceを使用してAzure Key Vaultにアクセスできない。

結論

16.7以降のVisualStudioからAzureに接続するための認証を行う場合、認証がうまく行かない場合があるようです。

Azureのリソースは認識できてるのに、肝心の接続のときはうまくいかない!

そんな場合はNuGetでAzure.Identityのバージョンを1.2.2以上に上げて、下記のいずれかを行うことでうまくいくようです。

  • launchSettings.jsonAZURE_TENANT_IDを指定する
  • new DefaultAzureCredential(new DefaultAzureCredentialOptions { VisualStudioTenantId = "<AzureのテナントID>" }))を指定する

どえらいハマってVSアンインストールとかして悔しい感じなので、以降、解決までの試行錯誤やらの過程の話です。

ConnectedServiceでAzure KeyVaultにアクセスできなくなった

VS16.7からVisualStudioのConnectedServiceを使用した際に構成される内容が変わりました。

開発時は基本的にユーザーシークレットを使用すると思うのですがちょっと使ってみたろと思ったのがすべての始まりでした。

ひとまず、Azure KeyVaultへの接続設定をVSから行ってみます。

f:id:TakasDev:20200905132659p:plain

f:id:TakasDev:20200829111829p:plain

構成されるライブラリだけで見ると👆な感じの変更内容のようです。

Azure.Identityを使用するような変更が行われたという感じですね。

さて、これで実行を行うとエラーとなり実行ができませんでした。

f:id:TakasDev:20200905132549p:plain

正常に実行できる環境もあり、いわゆるおま環事象といった感じです。

AzureADアプリケーションを調べる

MsalServiceException: AADSTS70002: The client does not exist or is not enabled for consumers. If you are the application developer, configure a new application through the App Registrations in the Azure Portal at https://go.microsoft.com/fwlink/?linkid=2083908.

エラー内容は上記のとおりです。指定されたAzureADアプリが存在しないようです。

なるほどー。というわけでAzureADアプリを作成し、KVのアクセスポリシーに割り当てます。

f:id:TakasDev:20200905133919p:plain

Program.csも下記のように作成したClientIdを使用するように書き換えました。

.ConfigureAppConfiguration((context, config) =>
{
    var keyVaultEndpoint = new Uri(Environment.GetEnvironmentVariable("VaultUri"));
        config.AddAzureKeyVault(
        keyVaultEndpoint,
        new DefaultAzureCredential(new DefaultAzureCredentialOptions { ManagedIdentityClientId = "<作成したADアプリID>" }));
})

しかし結果は変わりませんでした。

別の認証が通るか試してみる

Azure SDK: What’s new in the Azure Identity August 2020 General Availability Releaseという記事でライブラリについて詳細にかかれています。

f:id:TakasDev:20200905132859p:plain

ひとまずVisualStudioの認証で失敗したけど、他の認証で成功してたらおkなのでは?と短絡に考え

Azure CLIでログイン状態にしてリトライしてみましたが結果は同じでした。

ADアプリの設定の問題かな?と思ったので正常に動作する側のAzureADアプリを見てみようと考えました。

認証フローを追う

設定を見るにも認証に使用されているAzureADのClientIDを見ないと始まりません。

そこでどのような認証が行われているのか正常に動作する環境を動かしながらFiddlerで追っかけてみました。

色々アレな情報が出てくるのでキャプチャは控えますが

ここで使用されているであろうClientIDの特定はできましたが、正常に動作する環境上でもAzureADアプリは見つかりませんでした。

で、あれば問題の根本はアプリが存在しないということはないだろうな。ということで別の線を当たることにしました。

テナントを指定する

先程記載したブログの記事でテナントの指定の仕方を記載した項目があります

authenticate-to-a-specific-tenant

僕のアカウントはAD/AD B2C含め複数のテナントに所属しており、且つオーナーとなっているテナントも複数あるのでHomeテナントを誤認したのかもしれません。

まずは👆記事に記載されている内容で解決できるか確認してみます。

Azure.Identityの1.1.1のDefaultAzureCredentialOptionsVisualStudioTenantIdは存在しないので

Azure.Identityの1.2.2に上げる必要があります。

.ConfigureAppConfiguration((context, config) =>
{
    var keyVaultEndpoint = new Uri(Environment.GetEnvironmentVariable("VaultUri"));
    config.AddAzureKeyVault(
        keyVaultEndpoint,
    new DefaultAzureCredential(new DefaultAzureCredentialOptions { VisualStudioTenantId = "<Azure Tenant Id>" }));
})

これでやっと動作するようになりました。

その後に試したこと

DefaultAzureCredentialOptionsを指定せずに解決できないか試しました。

VisualStudioのAzureサービス認証のアカウント絞り込み

VisualStudioの認証で使用しているアカウントに紐づくテナントを絞り込めば同じ状態になるかな?

と思い試してみましたが結果は変わりませんでした。

f:id:TakasDev:20200905142122p:plain

lauch.settings.jsonAZURE_TENANT_IDを指定する

この方法はうまくいきました。実行ソース側をいじるのが嫌であればこちらを指定するほうが良い感じですね。

おわりに

認証まわりで沼ると解決まで時間がかかるので

ローカルでの開発時はユーザーシークレットがド安定ですね😏

msal.js v2.0.1 をAngularで使用する

はじめに

今回の記事で使用するコードは下記の構成で実装しています。

記事を参照されるタイミングによっては動作しない可能性がありますのでご留意ください。

  • @angular/cli:10.0.5
  • @azure/msal-browser:2.0.1

msal.js v2.0.0がリリース

先月の末頃(2020年7月21日)にmsal.jsのv2.0.0 がリリースされました

(色々試している間にv2.0.1がリリースされちゃいましたが…

PKCEの対応が行われ、よりかんたんにセキュアに使用することができるようになります。

詳細なお話は公式のドキュメントや、今月の.NETラボを見ていただくとして…

ひとまず、僕がメインで使用しているAngularでの使用方法を確認していこうと思います。

@azure/msal-angular

msal.jsには派生ライブラリとして、Angular対応された@azure/msal-angularというライブラリが存在します。

ServiceやInterceptorが提供され、Angularで使用するのに便利なライブラリとなっています。

f:id:TakasDev:20200809223243p:plain

👆microsoft-authentication-library-for-jsより抜粋

上図の通り、@azure/msal-angularもv2.0がPKCE対応されるはずなのですが、まだリリースされていないようです。

Roadmapにも特に予定は明記されていないようですが

何はともあれ使用したいので、プレーンな@azure/msal-browserをAngularで使用してみましょう。

@azure/msal-browserをAngularで使用する。

基本的な使い方は@azure/msal-browserのドキュメントの通りで問題ありません。

ライブラリのインポート

npm install @azure/msal-browser

msalアプリケーションインスタンスの生成

msalのv1からmsalの処理のエントリークラスがUserAgentApplicationからPublicClientApplicationに変更されています。

インスタンスの生成にADアプリなどの設定のConfigurationを与えるあたりの使い方は変わらないようです。

Configurationの構成自体も大きく変わっていないようで、clientIdauthorityなどしか項目を使っていない場合は設定記述部分は変更する必要はありません。

参考 - Configuration

handleRedirectCallback

loginRedirectなどを行った場合に引っ掛けるhandleRedirectCallbackは大きく変更されています。

Promiseで実行されるようになっておりそれに合わせた変更が必要です。

const res = await this.clientApp.handleRedirectPromise().catch(err => {
    console.log({err});
});

自分は検証でエラーをトラップしたかったので雑に👆の実装にしていますが、resがvoid | AuthenticationResultとなるのでthenを使うのもありです。

handleRedirectPromiseの内部処理実行前にloginRedirect処理などが走るとエラーが発生するので

handleRedirectPromiseの処理終了を待機して、処理終了後にログイン処理を行うのがベターです。

login

loginを行う処理のインターフェースは変わっていません。loginRedirectloginPopupでログインを実行します。

Redirectは前述するCallbackで、PopupはPromiseで結果を取得します。

ユーザー情報の取得

msal.jsのv1ではユーザー情報を取得するインタフェースがClientApp上に用意されていましたが、v2でなくなりました。

msal.jsのv2では、ログインやトークン取得時の結果としてアカウント情報が含まれた状態で返却されるので

そこからデータを取得することになります。

リダイレクトの場合は下記のようにhandleRedirectPromiseで取得できたレスポンスから抽出することになります。

const res = await this.clientApp.handleRedirectPromise().catch(err => {
    console.log({err});
});
this.account = (res as AuthenticationResult).account;

PopupはloginPopupの返却値です。

アクセストークンの取得

acquireTokenSilentで必要な引数にアカウント情報のパラメータが追加されています。

const res = await this.clientApp.acquireTokenSilent({ scopes: this.scopes, account: this.account });
return res.accessToken;

ログイン時などにアカウント情報は取得し、プライベートな変数なりに格納しておいたほうが良さそうです。

ここまでの実装はAngular特有というものではなく、普通の@azure/msal-browserの使用の仕方ですね。

本家のmsalServiceよろしく、一つのServiceクラスに👆までの処理を実装しておくと使いやすいと思います。

Interceptorの作成

Angularのサービスらしく。ということでInterceptorを作成し、WebAPIへの通信時に自動的にヘッダにトークンを付与させます。

ここは@azure/msal-angularのInterceptorをちょっとだけ参考にします。

[
  { endPoint: string, scopes: string[] }
]

上記の構造の設定を使用して、アクセスするエンドポイントによって

使用するスコープを切り替える機能を実装していきます。

(👇本家と比べるとかなり雑ですが…!

@Injectable()
export class MsalInterceptor implements HttpInterceptor {

    constructor(
        private auth: MsalService
    ) {}

    intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        const target =  environment.endPointTargetScope.filter(f => req.url.includes(f.endPoint));
        let scopes = [];
        if (target.length > 0) {
             scopes = target[0].scopes;
        }
        if (!scopes || scopes === []) {
            return next.handle(req);
        }
        return from(
            this.auth.acquireTokenSilent(scopes)
                .then((token: string) => {
                    const authHeader = `Bearer ${token}`;
                    return req.clone({
                        setHeaders: {
                            Authorization: authHeader,
                        }
                    });
                })
        )
        .pipe(
            mergeMap(nextReq => next.handle(nextReq))
        );

    }
}

ひとまずこれで、ログイン👉アクセストークン取得👉WebAPI通信の必要最低限な動きは確認できるようになります。

まとめ

Angularでのmsal.js v2.0.xの使用の仕方を見てきました。

@azure/msal-angularのv2がリリースされる可能性はありますが

@azure/msal-browser準拠のインターフェースや使い方になりそうな気はしているので

これを使用して、変わりそうな箇所を抑えておくのも良いかもしれません。

今回検証で使用したソースコードは下記にのごった煮レポジトリ格納しています。

github.com

AngularのRouterでuseHashをtrueにしているときにmsalで`Error: Cannot match any routes. URL Segment: 'access_token'`が出るときの対処

はじめに

2020年7月時点の記事となります。

msal.jsのv2が出たのですが、書きかけだった記事の供養となります。。ご留意ください。

Angular・msal.js・@azure/msal-angularは下記のバージョンとなっています。

  • Angular: 10.0.1
  • msal.js: 1.3.2
  • @azure/msal-angular: 1.0.0

検証を行うバージョンによっては動作が異なることがありますのでご留意ください。

何が起きているか

タイトルのとおりですが、AngularのRouterModuleを設定するときにuseHashtrueにすると

Angularのルーティングはhttp://localhost:4200/#/<path>のような#付きのパスとなります。

AzureADでimplicit flowでトークンが返却される場合、リダイレクトURLに#access_token=~~のようにトークン情報が付与されて返却されているようです。

参考-Implicit grant (暗黙的な付与)

  1. fragmentでトークンが送られる
  2. Angularのルーティングで#付きのパス=遷移と誤認し遷移を実行
  3. 実際のパスは当然存在しないのでエラー

この流れが原因のようです。

queryで返却されるようになれば良かろうかとも思うのですが

msal.jsのConfigをさらっと眺めた感じだと、responce_modeをいじる設定はないように見えるので別の方法を模索してみます。

参考-msal・Configuration

どのように解消するか

routerのeventをsubscribeするのも良いですがちょっと手間なのでシンプルな解決方法が望ましいですね。

解決方法1:BootstrapModuleの変更

window.parent かどうか判定し、IFrame内部であればRouterを持たないダミーのモジュールを流し込めば良いはずです。

MSAL.js を使用してトークンを自動的に取得、更新するときにページのリロードを回避する」が参考になるかと思います。

別の問題の解消方法ですが、これに似た方法で対応が可能となります。

ただ、上記のサイトに上げた方法だと、useHash=trueにしたRouterModuleをImportすることになるので、問題となっていた現象を解決できません。

そこで、window.parentによる分岐をAppModule下ではなく

もう少し上のレイヤーで実行し、BootstrapするModuleをRouterを持たないダミーのModuleに変更する。といった対応を行います。

処理の分岐を下記のようにmain.tsで実行します。

if (window !== window.parent && !window.opener) {
  // IFrame内で実行されるダミーモジュール
  platformBrowserDynamic().bootstrapModule(RedirectDummyModule)
    .catch(err => console.error(err));
} else {
  // 通常のモジュール
  platformBrowserDynamic().bootstrapModule(AppModule)
    .catch(err => console.error(err));
}

解決方法2:リダイレクト先の変更

そもそもAngularを使用していないプレーンなダミーサイトにリダイレクト先を変更しちゃおうという手です。

ADAL Wiki - FAQ - Q1. My app is re-loading every time adal renews a token.」が参考になります。

これはadalの記事ですがmsal、@azure/msal-angularでも同様の実装が可能となります。

リダイレクト先のHTMLとして、下記のようなHTMLを用意しておきます。

リダイレクト時点でmsalのUserAgentApplicationインスタンスが生成されていないとエラーとなるので

msal.jsの読み込みと、UserAgentApplicationのインスタンス生成を行う必要があります。

<!doctype html>
<html>
<head>
    <meta charset="utf-8">
    <title>Frame Redirect</title>
    <script type="text/javascript" src="https://alcdn.msftauth.net/lib/1.3.2/js/msal.min.js" integrity="sha384-4cH4i4aLXsdIJJz5DF27S+GxZXRSqBORS4NN7kc7NyZBBIugxFyt6hQt5FiR/DuZ" crossorigin="anonymous"></script>
</head>
<body>
    dummy
    <script>
        var msalConfig = {
            clientId: "<Azure AD App Client ID>"
        };
        var authContext = new Msal.UserAgentApplication(msalConfig);
        authContext.handleWindowCallback((err, res) => {
            console.log({ err, res });
        });

        if (window === window.parent) { window.location.replace(location.origin + location.hash); }
    </script>
</body>
</html>

仮に、このHTMLがredirect-page.htmlとすると

アクセストークン取得時などにリダイレクトURLとしてこのURLを指定します。

    // 👇msal-angularのMsalService.acquireTokenSilent()
    // 👇msal.jsのUserAgentApplication.acquireTokenSilent()
    const res = await this.msalService.acquireTokenSilent({
      scopes: [ 'スコープ'],
      redirectUri: 'http://localhost:4200/redirect-page.html' // 👈こんな感じ
    });

デプロイする時も、今回作成したredirect-page.htmlが必要になるのででangular.jsonでデプロイ対象に指定しておきましょう。

{
  "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
   ....
      "architect": {
        "build": {
          "builder": "@angular-devkit/build-angular:browser",
          "options": {
            ...
            "assets": [
              ...
              "src/redirect-page.html" 👈追加
            ],
            ...

まとめ

そもそも別問題の対応のために、解決方法1や2を行っていたので今更見つけた感じになります。

無限リダイレクト問題とかにぶつかって、解決方法1,2とかに行き当たる方は多いと思うので知らん間に対応している。といったことが多そうです。

useHash:trueで且つ、そこらの対応がされていないとかのすこしニッチな状況のものになりますね。

msalのv2でこの記事が無意味なものに変わってくれるといいなーと若干期待しています。

今回検証で使用したリポジトリは下記となります。

解決方法1

解決方法2

Microsoft Graphのpresenceについて

修正

2020/7/19 : presenceのβ公開時期を2020年6月から2019年12月に修正しました。

(@karamem0さんご指摘ありがとうございました!)

2020/8/2:presenceのSubscriptionのドキュメントが公開されたので反映しました。

はじめに

2020年7月時点のMicrosoft Graphを使用した記事となります。

また記事で扱うpresenceは2020年7月段階でβ状態なので

参照されるタイミングによっては仕様が変更されている可能性がありますのでご留意ください。

presence

2019年12月に取得WebAPIがβ公開され2020年7月に変更通知がサポートされました

presenceとは具体的になんなのでしょうか。

めちゃくちゃかんたんに説明すると👇コレです。

f:id:TakasDev:20200719131308p:plain

presenceの取得

Graphの変更履歴の7月からのリンクは404なのですがドキュメントは確認できます。

presenceのデータを取得するのは下記のWebAPIで取得します。

※今はβしか公開されていないのでhttps://graph.microsoft.com/beta/エンドポイントからしかアクセスできません。

GET /me/presence
GET /users/{id}/presence
GET /communications/presences

GET /communications/presencesと記載されていますが素直に使用しても複数人取得できず404になりました。

ユーザーid指定が必須のようなので、/communications/presences/{id}が正しいと思います。

複数人のpresenceの取得はgetPresencesByUserIdです。

POSTで取得したいユーザーのidをBodyに指定するので、ちょっと違和感がありますね。

POST communications/getPresencesByUserId
{
    "ids": ["<UserId>", "<UserId>"]
}

presenceで取得するプロパティについて

プレゼンスのリソースの種類

取得するプロパティは少なく、idactivityavailabilityの3種だけです。

f:id:TakasDev:20200719134234p:plain

👆のドキュメントはめったくそややこしいですが、動作させて確認したところこれで正しいようです。

(user's availabilityがavailibityじゃないんだ…的な)

Presenceの状態については、Teams でのユーザーのプレゼンスで確認できます。

availabilityとactivityの対応は下図の通りの認識です。

f:id:TakasDev:20200719134843p:plain

presenceの変更通知

2020年7月に変更通知に対応されました。

CreateSubscriptionにはリソースの明記はありませんが、OverViewによると/communications/presencesのようです。

presencesの取得から考えると単一ユーザー指定の/communications/presences/{id}の指定になるようです。

IDなしやカンマ区切りの複数指定は400になりました。

また、Subscriptionの作成では有効期限を指定する必要があります。

ドキュメントに記載の通り、1時間のようです。

サブスクリプションの作成は他のリソースのときと同様、下記のようなJSONをPOSTすることで作成できます。

{
      "changeType": "updated",
      "notificationUrl":  "<エンドポイントURL>",
      "resource": "communications/presences/<user id>",
      "expirationDateTime":  "<有効期限>"
}

複数指定する場合は$filterを使用して下記のように指定するようです。

参考-Doc-RequestExample

"resource": "communications/presences$filter=id in ('<userid>', '<userid>'...)",

他のリソースのSubscriptionを作成する時とそこまで大きく変わるところはありませんね。

変更通知で通知される内容

Subscriptionを作成して内容を確認した結果、下記のようなデータが取得できました。

{
    "value": [
        {
            "subscriptionId": "cc87f8f1-45aa-4b33-9dd3-660bbaf3ed57",
            "clientState": null,
            "changeType": "updated",
            "resource": "communications/presences('<userid>')",
            "subscriptionExpirationDateTime": "2020-07-18T23:03:50.4305891-07:00",
            "resourceData": {
                "@odata.type": "#Microsoft.Graph.presence",
                "@odata.id": "communications/presences('<userid>')",
                "id": null,
                "activity": "InACall",
                "availability": "Busy"
            },
            "tenantId": "<tenantid>"
        }
    ]
}

activityavailabilityも取得できているので、TeamsやEventのように別途WebAPIでリソースデータの取得などをする必要はなさそうです。

おわりに

β公開中のpresenceの取得と変更通知の使い方を見ていきました。

第 1 回 Japan M365 Dev User Group 勉強会ではこれをネタにLTする予定です。

よろしくおねがいします。

Angular Elementsで作成したWeb Componentsの設定情報をプレーンなJSから変更する

はじめに

この記事内のアプリケーションは下記バージョンで構成しています。

  • Angular CLI: 10.0.1
  • Angular: 10.0.2

この記事を参照されるタイミングによっては

サンプルコードが動作しなかったりする可能性がありますのでご留意くださいませ。

そもそもなにをしたいのか

Microsoft GraphはGraph ToolkitというWeb Componentsを公開していまして

オリジナルでそれを作ってみたいなー。というのが発端です。

今回の記事はその前哨戦。

Web Componentsを作ってみてハマった箇所の備忘録。といった感じですね。

Angular Elements

Angularを使用している場合は、Angularで作成したComponentをWebComponentsでパッケージングしてくれる

Angular Elementsというものが存在します。Angularで諸々なれている自分はこれを使うのが一番手っ取り早そうです。

基本的な使い方などは、👆のドキュメントを参考にすれば良いと思うので割愛します。

最終的に下記のようにAngularを使用していないようなプレーンなHTMLで

作成したWebComponentsのセレクタを指定してAngularで作成したComponentを表示させることが出来ます

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <link rel="stylesheet" href="../../dist/custom-element/styles.css">
    <script src="../../dist/custom-element/custom-element.js"></script>
</head>
<body>

    <h1>Web Component (Costum Elements)</h1>
    <scl-custom-button></scl-custom-button>
    <scl-sample-form-group></scl-sample-form-group>
    <scl-router-page></scl-router-page>
</body>
</html>

👇

f:id:TakasDev:20200705111959p:plain

情報を設定する🤔

さて、Micorosoft Graphにアクセスするということは、ADのアプリケーション情報などが必要になります。

設定情報をWeb Components作成時点でビルドインしても良いのですが、それでは使い回しがしにくいです。

具体的には下図のような構成にしたいところですね。

f:id:TakasDev:20200705113551p:plain

ビルドされたソースはどうなっている?

ビルドされたパッケージ内のServiceクラスに値を直接外部から突っ込めるでしょうか?

下記のTSがビルドされたソースを整形して見てみます。

export class LibSettingsComponent implements OnInit {
...
 constructor(
    private service: LibStateService
  ) { }
...
  set setting(setting: { state: string, userId: string, userName: string }) {
    console.log('emit');
    this.service.setting(setting);
    console.log(this.service.state);
    this.dummyStSet = 'emit';
  }
}
        Oy), By = ((Iy = function() {
            function e(t) {
                _classCallCheck(this, e),
                this.service = t,
                this.dummySt = "dd"
            }
            return _createClass(e, [{
                key: "ngOnInit",
                value: function() {}
            }, {
            ...
            }, {
                key: "setting",
                set: function(e) {
                    console.log("emit"),
                    this.service.setting(e),
                    console.log(this.service.state),
                    this.dummyStSet = "emit"
                }
            }]),

this.serviceはfunctionの引数のtで更にそこの呼び出し元の…?

いずれにせよServiceのインスタンスに直接アクションをするのは多大な労力を伴いそうです。

設定用のComponentを用意する

Componentからのアクセスは簡易に行えます。なので設定用のComponentを作成する。というアプローチをとってみます。

中身が空の設定用のComponentを用意します。

設定用のComponentは@InputでSetterのみを提供し、Setterの実装内で設定情報Serviceを書き換えます。

具体的なソースコードは下記のとおりです。

色々視認やデバッグしやすいようにHTMLテンプレートにものを突っ込んでますが空で問題ないと思います。

import { Component, OnInit, Input } from '@angular/core';
import { LibStateService } from '../../services/lib-state.service';

@Component({
  selector: 'scl-lib-settings',
  template: '<div>{{dummySt}}</div>'
})
export class LibSettingsComponent {

  dummySt = 'bf';

  constructor(
    private service: LibStateService
  ) { }

  @Input()
  set setting(setting: { state: string, userId: string, userName: string }) {
    // ここで設定情報をServiceに反映する
    this.service.setting(setting);
    this.dummySt = 'emit';
  }
}

これでプレーンなHTMLのタグ上などで、settingプロパティに値を突っ込むことで設定情報が反映されるようになりました。

設定用のコンポーネントタグを書かなくて良いようにする

さて、設定を外から与えることができるようになりましたが

何も情報を出力しない設定用のコンポーネントをHTML上に書くのはなんともダサ味があります。

なので、スクリプトでも設定情報の反映ができるようにしたいところです。

そこで、

  1. 設定用のコンポーネントタグをcreateElementして
  2. 設定を行い
  3. 設定用のコンポーネントタグを削除する

こんな感じの設定スクリプトをAngular Elementsから提供することにします。

設定スクリプトは下記のようなものです。

class LibSettings {

    setSettings(data) {
        const settingDom = document.createElement('scl-lib-settings');
        document.body.appendChild(settingDom);
        settingDom.dummySt = 'hoge';
        settingDom.setting = data;
        document.body.removeChild(settingDom);
    }
}

作成した設定スクリプトをWebComponentsをビルドする際に一緒に吐き出したいので、angular.jsonをいじります。

    "custom-element": { // Angular Elementsのプロジェクト名
      ...
      "architect": {
        "build": {
          "builder": "@angular-devkit/build-angular:browser",
          "options": {
            ...
            "scripts": [
               // 👇を追加
              "projects/custom-element/src/static-scripts/lib-settings.js"
            ]

これで、WebComponentsをパッケージングしたときに、同一ディレクトリに設定スクリプトも吐かれるようになりました。

f:id:TakasDev:20200705121442p:plain

使ってみる

設定スクリプトで設定が反映されたサービスのプロパティをアラート表示するボタンComponentを作成しました。

このボタンを押下することで設定情報の反映の確認を行ってみます。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <link rel="stylesheet" href="../../dist/custom-element/styles.css">
    <script src="../../dist/custom-element/custom-element.js"></script>
</head>
<body>

    <h1>Web Component (Costum Elements)</h1>
    <scl-custom-button></scl-custom-button>
    <scl-sample-form-group></scl-sample-form-group>
    <scl-router-page></scl-router-page>
    <script src="../../dist/custom-element/scripts.js"></script>
    <script type="text/javascript">
        // 設定スクリプトを使用して設定情報の反映
        const cl = new LibSettings();
        const setting = { state: 'HTMLから設定したよ!', userId: 'user', userName: 'hogehoge' }
        cl.setSettings(setting);
        //
    </script>
</body>
</html>

上記の状態で、アラートに「HTMLから設定したよ!」と表示されればOKです。

f:id:TakasDev:20200705122727g:plain

設定できました。

これで設定情報を外部から与えることができそうです。

おわりに

長い前哨戦が終わったので、次回はようやく目的だったオリジナルのGraph Toolkitの作成に入ってみます。

手を付けれていないのでいつになるかわかりませんが

今回検証を行ったソースコードは下記となります。

github.com

色々検証で試行錯誤したので少々とっちらかっているのはご容赦くださいませ…