Azure FunctionsでMicrosoft Graph PowerShell SDKを使う(powershell) 修正版

以前作成した手順に一部修正を加えました。こちらが正です。

以前のブログはこちら。

Azure FunctionsでMicrosoft Graph PowerShell SDKを使う(powershell) | 今日も元気にIT屋さん

Microsoft Graph PowerShell SDKとは?

Microsoft GraphのAPIをPowerShellネイティブ(という表現でいいのか?)で触れるようにしたモジュール群。できることは、Microsoft GraphをHTTPSで触りに行くのと同じだが、認証プロセスなどがより簡単になっている…はず。

最近python版がリリースされたのも話題になりました。

GitHub – microsoftgraph/msgraph-sdk-python-core: Microsoft Graph client library for Python

手法の比較と利用シーン

手法 1. 環境準備 2. 認証 利用シーン
普通にPowerShellでHTTPSする powershell環境のみでOK (割愛) (割愛)
Microsoft Graph PowerShell SDK(手作業想定) Install the Microsoft Graph PowerShell SDKが必要 対話的認証 手作業
Microsoft Graph PowerShell SDK(バッチ処理) Install the Microsoft Graph PowerShell SDKが必要 証明書認証 バッチ処理

まず、今まで通りの普通のやり方であれば、powershell環境のみでOKだし、アプリケーションシークレットとキーでバッチ処理に対応できるような非対話認証も可能です。

が、今回は検証という事で、Microsoft Graph PowerShell SDK(バッチ処理)をAzure Functionsで実施することを目指します。

ゴールはMicrosoft 365 Message Centerのメッセージを取得する、です。

ローカル環境で試す

1. 環境準備

これは前回のブログに書いた通り、こちらでOKです。

Install the Microsoft Graph PowerShell SDK | Microsoft Docs

Install-Module Microsoft.Graph -Scope CurrentUser

2. 認証

これも前回のブログに書いた通りですが、一度手順を整理します。

2-1. アプリケーションの登録

いつもの通り、Azure ADへのアプリケーション登録を行います。

Use app-only authentication with the Microsoft Graph PowerShell SDK | Microsoft Docs

2-2. 証明書の作成

今回はConnect-MgGraphで非対話的に処理を行いたいです。そのためには、Connect-MgGraphの接続方法で証明書認証を行う必要があります。

※ アプリのシークレットキーを利用して非対話的にログインすることはできません。

公的証明書を利用してもよいですが、ひとまず試すだけなら自己証明書でも動きます。以下のコマンドで自己証明書を作成します。

$mycert = New-SelfSignedCertificate -Subject "20221210_API" -CertStoreLocation "Cert:\CurrentUser\My" -NotAfter (Get-Date).AddYears(1) -KeySpec KeyExchange
$mycert | Export-Certificate -FilePath c:\data\mycert.cer
$mycert | Export-PfxCertificate -FilePath c:\data\mycert.pfx -Password $(ConvertTo-SecureString -String "20221210password" -Force -AsPlainText)
$mycert | Select Thumbprint

自己証明書の作成は以下のサイトを参考にしました。

2-3. 証明書(cerファイル)のアップロード

Azureに登録したアプリケーションに証明書(cerファイル)をアップロードできますので、アップロードします。

証明書とシークレット -> 証明書より、アップロードを行います。

2-4. 証明書を利用した認証

ローカル環境では以下の通りに実行することで、証明書の拇印だとか、CN名を指定すれば証明書認証が可能でした。

Connect-MgGraph -Clientid "<登録したアプリのアプリケーション (クライアント) ID>" -TenantId "<アプリを登録したテナントのディレクトリ (テナント) ID>" -CertificateThumbprint "<証明書の拇印>"
2-4. (別の方法)証明書を利用した認証

秘密鍵をエクスポートして、X509Certificate2 オブジェクトで証明書として読み込むこともできます。この方法であれば、証明書ストアに証明書をインポートしていない別のマシンでもアクセスできます。

$password= "20221210password"
$passwordSecure = ConvertTo-SecureString -AsPlainText -Force $password

$cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2("C:\data\mycert.pfx", $passwordSecure)

Connect-MgGraph -ClientID "クライアントID" -TenantId "テナントID" -Certificate $cert

参考:open Unable to authenticate using Connect-MgGraph with certificate on Windows Server or Azure HybridWoker error is "certificate was not found or has expired." · Issue #675 · microsoftgraph/msgraph-sdk-powershell

まとめ

ローカル環境での構成図は以下です。

  1. 証明書ストアの証明書を読み込んでアクセス
  2. エクスポートした秘密鍵を読み込んでアクセス

どちらでも可能です。自分のマシンで実行するのであれば1ですが、サーバーなどでバッチアクセスさせるのであれば2となります。 ただし、2はAzure Functionsでは実行できません。 ここはハマりましたので、後でコメントします。

Azure Functions環境で実装

1. 環境準備

前回のブログに書いた通り、こちらでOKです。

まず普通にAzure Functionsを作成します。

作成後に、プラットフォームを32bitから64bitに変更します。これはやらなくてもよいかもしれません。

この後に、Microsoft Graph PowerShell SDKをAzure Functions環境に用意します。

"高度なツール"からKudoを起動し、Kudu > Debug console > PowerShellに進みます。

site -> wwwrootと移動して

[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
Install-PackageProvider nuget -Force -Scope CurrentUser
mkdir modules
Save-Module Microsoft.Graph.Authentication -Path .\modules 

とすることで、modules以下に必要なモジュールがダウンロードされ、Azure functions関数起動時に読み込まれます。

※ Microsoft.Graph.Authenticationではなく、Microsoft.Graphのすべてをダウンロードできますが、多すぎるとエラーが出ることがあります。

参考:Azure Functions 用に Save-Module する

2. 認証

ここからが問題です。

2-1. アプリケーションの登録

Azure ADへのアプリケーション登録は、ローカル環境実行時と変わりません。

2-2. 証明書の作成

これもローカル環境実行時と変わりません。

2-3. 証明書(cer)のアップロード

これもローカル環境実行時と変わりません。

2-4. 証明書を利用した認証

ここがローカル環境実行時と違います。

ローカル環境では

  1. 証明書ストアの証明書を読み込んでアクセス
  2. エクスポートした秘密鍵を読み込んでアクセス

のどちらかの方法で、証明書認証が可能でした。しかしAzure Functions環境ではどちらの方法も使えません。1については当然ですが、2の方法はいけるんちゃう?と思いきやダメでした。

念のため、試した手順は以下です。

まず秘密鍵をKudo経由でAzure functionsにアップロードします。アップロード場所はC:\home\site\wwwroot\certfile\mycert.pfx です。

そして、以下のコードを入力するとエラーが出ました。

$password= "20221210password"
$passwordSecure = ConvertTo-SecureString -AsPlainText -Force $password

$cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2("C:\home\site\wwwroot\certfile\mycert.pfx", $passwordSecure)

Connect-MgGraph -ClientID "クライアントID" -TenantId "テナントID" -Certificate $cert

よってAzure Functions専用の方法で対応します。Azure Functionsから読めるところに証明書(pfx)をアップロードすればよいです。

2-4-1. Azureキーコンテナを作成し、証明書(pfx)をアップロードする
2-4-2. Azure FunctionsからAzureキーコンテナの証明書(pfx)を読み込み、認証する

以下のようなコードで認証を行います。

$ClientID = 'クライアントID'
$TenantID = 'テナントID'

$containerName = 'Key20221210' # キーコンテナの名前
$certName = '20221210pfx' # キーコンテナ中に作成した証明書の名前

###########################################################
# 1-1.鍵の読み込み
###########################################################

$certificate = Get-AzKeyVaultCertificate -VaultName $containerName -Name $certName
$secret = Get-AzKeyVaultSecret -VaultName $containerName -Name $certificate.Name

$ptr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($secret.SecretValue)
try {
    $secretValueText = [System.Runtime.InteropServices.Marshal]::PtrToStringBSTR($ptr)
}
finally {
    [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($Ptr)
}

$secretByte = [Convert]::FromBase64String($secretValueText)
$Certificate = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($secretByte, '', 'Exportable,PersistKeySet')

###########################################################
# 1-2.テナントへのアクセス
###########################################################
Connect-MgGraph -ClientID $ClientID -TenantId $TenantID -Certificate $certificate

これらを実行するために準備がいくつかあります。

準備1. Get-AzKeyVaultCertificate Get-AzKeyVaultSecretをAzureFunctionsから実行できるようにする

デフォルトでは、キーコンテナ中の証明書を読み込むためのコマンドがAzureFunctionsから実行できないため、実行できるようにします。

具体的には以下の通りです。

  1. host.jsonでmanagedDependencyが"Enabled": trueであることを確認する。※ ここはデフォルト値から変更不要です
  2. requirements.psd1で# 'Az’~の行がコメントアウトされているのを、コメントインする
  3. AzureFunctionsの再起動を行う
準備2. AzureFunctionsからキーコンテナ中の証明書を読み込めるよう、アクセス権を付与する

実はキーコンテナに証明書を作ったとしてもアクセス権を付与しないと読み込めないため、アクセス権を付与します。アクセス権はユーザーやグループに対して付与できますが、関数はユーザーでもグループでもありません。どうするかというと、関数のマネージドID機能をONにして、関数をユーザー的に扱えるようにします。

準備3. アプリケーション設定より WEBSITE_LOAD_USER_PROFILE = 1 と設定する

以下の通り、アプリケーション設定より WEBSITE_LOAD_USER_PROFILE = 1 と設定します。

この理由なのですが

  • WEBSITE_LOAD_USER_PROFILE = 1 が設定されていない場合、 Functions(App Service) ホストはユーザー プロファイルおよびその配下の証明書ストアの情報をロードしません。
  • しかし、 X509Certificate2 クラスのコンストラクタはユーザー プロファイル内から証明書情報をインポートしようとします。
    • X509Certificate2 クラスを作成した際に作成される証明書情報はデフォルトでは X509KeyStorageFlags.UserKeySet が設定されているためユーザー プロファイル内の証明書ストアに保存されるため
  • よってWEBSITE_LOAD_USER_PROFILE = 1 が設定し、ユーザープロファイル内の証明書ストアを読み込めるようにする必要があります。

ここら辺の詳しい手順は以下の記事を参照させていただきました。

Azure Functions (App Service) 上で X509Certificate2 クラスを作成するときには

Posted by tera