BEACHSIDE BLOG

Azure と GitHub と C# が好きなエンジニアの個人メモ ( ・ㅂ・)و ̑̑

Azure Functions のローカルデバッグ時に LogLevel を変えたい

Azure Functions で、本番環境でのログレベルと、ローカルの PC でデバッグでログレベルは変えたいときにどう設定するかのお話です。

2021年7月時点で C# の Azure Functions v3.x (バージョンに関する詳しいことはここ)での話です。

ログレベルの設定は、基本的には host.json で定義します。設定方法はこちらのドキュメントlogging について書かれている部分や、場合によっては logging の child である applicationInsights をいじることがあるでしょう。

host.json はデプロイする artifact に含まれますので、host.json を変えるとプロダクション環境に影響を及ぼすことがあります(プロダクション環境の環境変数でセットすればもちろん回避できますがそれは今はおいておきます)。そんな host.json をいじって戻し忘れると、「ひでぶっ」って事故なることもあるかもしれません(知らんけど)。

host.json はいじらず、ローカルでのデバッグ時にはより細かいログを出すような設定をする方法のメモです。

local.settings.json で LogLovel を上書きする

host.json では以下のように logging の logLevel の default を Warning で設定してたとしましょう。(後述する Azure Functions で環境変数の設定とかしない限りは)デプロイ先の本番環境とかでもこの設定になりますので、無駄にいじりたくない気持ちがわいてきます。

  "version": "2.0",
  "logging": {
    "logLevel": {
      "default": "Warning"
    },
// 以下省略

local.settings.json でこの logLevel の default 部分を Trace に上書きするには以下の6行目のように書きます。prefix として AzureFunctionsJobHost、あとは json のネストされたフィールドを表現するのに _ を2つで繋いで書く定番 (?) のパターンですね。

あとはこんな感じで動作確認になんかめんどくさそうなコードを書いしまったのですが、local.settings.json で logLevel を変えながらデバッグすると、正しく構成されていることがわかります。

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Extensions.Logging;

namespace LogLevelCustomizationForLocalDebug
{
    public class Function1
    {
        [FunctionName("Function1")]
        public IActionResult Run(
            [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req,
            ILogger log)
        {
            log.LogTrace("Hello Trace");
            log.LogDebug("Hello Debug");
            log.LogInformation("Hello Information");
            log.LogWarning("Hello Warning");
            log.LogError("Hello Error");

            return new OkObjectResult($"Trace: {log.IsEnabled(LogLevel.Trace)}; " +
                                      $"Debug: {log.IsEnabled(LogLevel.Debug)}; " +
                                      $"Information: {log.IsEnabled(LogLevel.Information)}; " +
                                      $"Warning: {log.IsEnabled(LogLevel.Warning)}; " +
                                      $"Error: {log.IsEnabled(LogLevel.Error)}; ");
        }
    }
}

Azure Functions の環境変数で上書きする

local.settings.json で上書きしてれば事足りると思いますが前述でちょっと話題に出しましたので一応書いておくと、Azure 上にある Azure Functions の リソースに、前述の書き方で環境変数をセットすれば上書きできます。

Azure Functions のリソースの環境変数のセットが気になる方は、こちらから確認できます。

Cosmos DB で RBAC を構成して接続文字列の管理を不要にする ( Azure, PowerShell )

2月ごろに Azure Cosmos DBの RBAC のアナウンスされて気になってたけど放置してたのをついに書く時がきました(←気まぐれですが)。実装に関することはいつも通り C# の話になります。

書いてたら思った以上に長くなってしまいました...

Cosmos DB の RBAC とは

Cosmos DB の RBAC についての基礎知識やメリットをざっくり書いていきます。

接続文字列が不要になる

今でも多くの場合、データベースとアプリを接続するには接続文字列を使うことが多いでしょう。Azure Cosmos DB も同様です。
しかし、RBAC の機能を使うことで Cosmos DB へ接続する際の接続文字列が不要になります。
その代わりに RBAC の管理はもちろん必要になりますが、接続文字列を管理することでのリスクに比べるとベターなことが多いでしょう。

ロールによるアクセス制御が可能になる

Cosmos DB の接続文字列では、ロール(例えば read のみとか、read/write 両方できるとか)の制御はできません。
RBAC の機能を使うと、ロールにユーザーや Azure のリソース(ようは managed Identity)を割り当てることで細かい制御が可能になります。

例えばこんな感じです。

  • すべてのコンテナーに対して read 権限のみを与えるロールを作る
  • 特定のコンテナーに対してデータを追加できるが削除できないロールを作る
  • 特定の Azure Functions (の Managed Identity) にロールを付与することでアクセス制御をする

データプレーン操作の RBAC

重要なポイントのひとつとして、 Cosmos DB に対する RBAC には以下の2種類あり、今回のブログでとりあげているのは データプレーン操作の RBAC です。

種類 概要
データプレーン操作
(Data plane operations)
雑にいうとデータの読み取りとか削除の操作。2021年5月時点ではプレビュー中の機能。公式ドキュメントはこちら
管理プレーン操作
(Management plane operations)
雑な例だとデータベースやコンテナーの作成や RU の変更などの操作で、以前からある機能。公式ドキュメントはこちら

つまり Cosmos SDK を使ったプログラムで管理プレーンの操作をしたい場合は注意が必要です。むしろ管理プレーンの操作が動くと死にます。
具体的な例だと Database や Container がなければ作成する CreateIfNotExists メソッドを使っていたり、Cosmos DB の Change Feed 機能を使った Azure Functions で Lease container がなかったら作成する CreateLeaseCollectionIfNotExists =true のようなプログラムを書いてる場合です。

2021年5月 GA

2021年5月25日から開催された Build で GA の発表がありました。

azure.microsoft.com

ってかこのブログ、5月頭に書き始めてその間に PowerShell のネタ書いてたりで放置してたらもう6月も終わるのかと思っている今日この頃です。

前置きが思った以上に長くなりましたが本題に近づいていきましょう。

事前準備

Cosmos DB のリソース準備

ということでまずは Azure Portal で Cosmos DB がない場合はリソースを作成して、適当にデータベースとコンテナーを作る準備が必要ですが、そこが不明な場合はこちらの公式ドキュメントでセットアップしましょう。

Cosmos DB のリソースを作成したあと、クイックスタート をクリックして、Coreate 'Items' container のボタンをクリックすると、ToDoList というデータベースの中に Items という Container が作成されますので、とりあえずこの状態で話を進めます。

この方法で database/container を作成すると、RU の管理が container 毎になる点は注意です。よくわからんという場合は、RU を container 毎で管理するか database 単位で管理するかのドキュメントをご参照ください。Cosmos DB を使う上で大事なポイントのひとつです。

ついでに Management という container を作って、それぞれの container にデータを入れておきました。この時点での Cosmos DB の構成は以下のようにcontainer が2つある状態です。

Az.CosmosDB module のインストール

RBAC の管理はそのうち Azure Portal からできるようになるかなと勝手に妄想してますが、2021年6月時点だとまだできないです。そのため RBAC を操作するには、以下どちらかを使う必要があります。

GA のタイミングで PowerShell は preview が外れるかと思ったら今のところ更新の様子がありません。

今回は私があまり使わない PowerShell を使っていきます。ちなみに Azure CLI も試しましたが当たり前に操作感はかわりませんのでブログには書かないでおきます。

以下のコマンドを実行して Az.CosmosDB module をインストールします。

Install-Module -Name Az.CosmosDB -AllowPrerelease

ちなみに私の場合、図にあるように Administrator で実行しろとエラーがでました。でその私のケースでの解決策がこちら

PowerShell で Install-Module をするとエラー "Administrator rights are required to install or update ..." を解決する - BEACHSIDE BLOG

PowerShell で Azure にサインイン

2021年5月時点ではコマンドを使った作成になりますので、今回は Azure PowerShell (Az.CosmosDB バージョン 2.0.1-preview) を使っていきます。ということでまずはローカル環境で Powershell を起動して Azure にログインしておきます。これが不明の場合は前回ブログに整理しておきましたのでご参照ください。

PowerShell で Azure に サインイン / テナント ( サブスクリプション ) 切り替え - BEACHSIDE BLOG

さて次からがようやく本題です。

RBAC の操作

ここからは PowerShell のコマンドを使って RBAC の操作をしていきます。

Role の一覧を見る

GA 以降、2つの built-in ロールができました(Preview 期間中はありませんでした)。全ての container の Read only のロールと Read/Write できる Contributor のロールの2つです。

以下のコマンドで見てみましょう。

# TODO: 操作する Cosmos DB のアカウント名とリソースグループ名をセット
$accountName = ""
$resourceGroupName = ""

Get-AzCosmosDBSqlRoleDefinition -AccountName $accountName -ResourceGroupName $resourceGroupName

前述で「dedutuRead/Write できる Contributor のロール」とさらっと書きましたが、単純な Read も Write って制御ではなく、それぞれの操作を細かく制御できます。何ができるかは以下のドキュメントをチェックしましょう。

Cosmos DB の Custom Role を作成

Cosmos DB を操作して RBAC、つまりロールベースのアクセスコントロールをするためのロールを作成します。

New-AzCosmosDBSqlRoleDefinition を使ってロールを作成します。以下は、"Management" という container のみに対してすべてのアクションを実行できるロールを作成するコマンドです。

# TODO: 必要に応じて値をセット
$resourceGroupName = ""
$accountName = ""
$roleName ="Management container Contributor"

New-AzCosmosDBSqlRoleDefinition -AccountName $accountName `
    -ResourceGroupName $resourceGroupName `
    -Type CustomRole -RoleName $roleName `
    -DataAction @( `
        'Microsoft.DocumentDB/databaseAccounts/readMetadata',
        'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/*', `
        'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/*') `
    -AssignableScope "/dbs/ToDoList/colls/Management"

簡単に解説しておくと、

コードサンプルの最初にあるように3つの変数に値をセットします。

  • $resourceGroupName: Cosmos DB がおかれているリソースグループの名称
  • $accountName: Cosmos DB のアカウント名
  • $roleName: これから作成するロールの名前

それ以外では、

  • Type: CustomRole を指定します。
  • Data Action: どのリソースに対してどんなアクションができるかを定義します。1つのロールに対して複数定義できます。メタデータの読み込み、データの操作は全て可能という定義です。定義できるアクションはこちらで確認できます。
  • AssignableScope: 上記のコードだと"Management" という名称の container のみ DataAction で定義した操作が可能です。その他にデータベースやコンテナーレベルでの制御も可能です。詳しくはこちらで確認できます。

正しく実行出来たらこんな感じでレスポンスが返ってきます。レスポンスには数秒かかるので気長に待ちましょう。

レスポンスを待てずにぐちゃっと(?)した場合は、前述でも行った Get-AzCosmosDBSqlRoleDefinition でロールの一覧を確認できます。

ロールにユーザーや Managed Identity をアサインする

ロールができたら、そのロールが使えるユーザーや Azure のリソースの Managed Identity をアサインします。ユーザーなら Azure AD の ObjectID をセットします。"Azure のリソース" ってのは例えば特定の Azure Functions のインスタンスや Web Apps のリソースとかです。

  • 例えば特定の Azure Functions に割り当てたいなら、Azure ポータルからManaged Identity をサクッと設定して ID を取得しておきます(設定方法はこちら)。
  • ユーザーを割り当てたいなら、Azure Active Directory でそのユーザーの Object ID を取得しておきます。
  • -Scope の設定は
    • 全スコープを割り当てたいならドキュメントのサンプルのようにアカウント名をセットすればよいです。"/" でも行けます。
    • スコープを限定した Role の場合はそのスコープを割り当てます。前述で作成した "Management container Contributor" に何かをアサインするなら -Scope dbs/ToDoList/colls/Management をセットします。-Scope cosmos-beachside-sandbox20210628/dbs/ToDoList/colls/Management って入力してもいけたので、なんか緩いルールで入力してセットできるのが若干不安です。
# 
$principalId = "79fd8ad5-dd7c-42fa-bb37-401a0624c34f"

New-AzCosmosDBSqlRoleAssignment -AccountName $accountName `
    -ResourceGroupName $resourceGroupName `
    -RoleDefinitionName $roleName `
    -Scope "dbs/ToDoList/colls/Management" `
    -PrincipalId $principalId

アサインを削除する

アサインされたリストを以下のコマンドで見てみましょう。

Get-AzCosmosDBSqlRoleAssignment -ResourceGroupName $resourceGroupName -AccountName $accountName | Format-List

アサインの削除には Id が必要です。コマンドの結果を見ると Id がめっちゃ長いですが、削除する際は最後の GUID ぽいのだけで削除できます。例えばこんな感じ。

Remove-AzCosmosDBSqlRoleAssignment -AccountName $accountName -ResourceGroupName $resourceGroupName  -Id a2793668-cadc-43fc-95a4-c9d8c897aeb5

雑なサンプルコード

サンプルを書いた時には Console App は GenericHost 使ってたりしたので、ここではただの class のサンプルを書いておきます。

補足するのは NuGet で以下の2つ使ってる点くらいでしょうか。。。

  • Azure.Identity: v1.4.0 (現時点の最新)
  • Microsoft.Azure.Cosmos: v3.20.0 (現時点の最新)

ログの確認

この Data Plane の RBAC の操作のログは、以下のブログの Advanced auditing of data requests に書いてある通り、診断ログの設定をしておけばみれます。

Role-based access control with Azure AD now in preview - Azure Cosmos DB Blog

これについて書こうと思ったら診断ログの設定からだなーと思い、もう長くなったのでここら辺にしておきます。

参考

全般:

コマンドリファレンス:

Azure AD B2C で "with metadata key 'AllowInsecureAuthInProduction' set to true." ってエラーが出たときは ( Custom Policy )

Azure AD B2C の Custom Policy をさわっててたまにこのエラーがでることがありますかね。

Validation failed: 2 validation error(s) found in policy "B2C_1A_SIGNUP_SIGNIN" of tenant "***.onmicrosoft.com".Profile '***' uses an insecure authentication mode that is only allowed in production mode policies with metadata key 'AllowInsecureAuthInProduction' set to true.

Profile '***' uses an insecure authentication mode that is only allowed in production mode policies with metadata key 'AllowInsecureAuthInProduction' set to true.

出会う度になんだっけ?とかあーそれなって思うことが何度かあったので自分用にメモ。

解決策

エラーが起きるタイミングは RP の xml を B2C にアップロードする際です。

前提知識

TechnicalProfile で REST API を利用する際はMetadata の...

  • AuthenticationType でどんな認証方式を使うか定義できます。
  • AllowInsecureAuthInProduction で Production 環境でセキュアじゃない認証を許可するか boolean で定義できます。定義しないとデフォルト値の false になります。

エラーがおきるケース

AuthenticationTypenone に設定した場合、Production 環境ならば AllowInsecureAuthInProductionfalse (デフォルト値) だとエラーが発生します。

△ 解決策1

AllowInsecureAuthInProduction を true に設定すればエラーは起きませんので、Production でそのような運用するのであればこれでよいです。

あとはとりあえず検証するだけならいいのかもってのはあります。でもその検証結果をもとに数週間後または数か月後にコピペ作業で再利用した際、この部分を覚えてられるか?とかほかの人が作業したときにたとえコメントがあったとして見逃す可能性のリスクを踏むか?とか考えると心が荒んできます。認証関連かつセキュリティに関することなのでね。一歩踏み外すだけで地獄に落ちますからね...

ただ、そもそもセキュアな REST API をコールする想定であればこの対応はいまいちです。解決策2の方がよいです。

◎ 解決策2

大事なことなのでまた書きますが、そもそもセキュアな REST API をコールする想定であれば解決策1はいまいちです。
そこで、Production 環境か否かを判断することでエラーを回避します。

この環境の切り替えは、RP (←SignUpOrSignin.xmlとか) の TrustFrameworkPolicyDeploymentMode にて定義することができます。 DeploymentMode="Development" と定義すれば、AllowInsecureAuthInProduction がデフォルト値の false でもエラーが発生しません。

環境ごとに値を切り替えるのであれば DeploymentMode だけでなく REST API の Technical profile も含め、環境に応じた動的な Custom Policy の定義が必要になります。つまーり VS Code extension のAzure AD B2C を使い environment を定義していい感じに対応するところまでが解決策のセットになります

Azure Static Web Apps を Azure AD B2C で認証

Azure Static Web Apps での認証は、以前は Azure AD, Apple, Facebook, GitHub, Google, Twitter の設定はできました (Apple は新しい気がするな)。

そして2021年5月の GA の頃に OpenID Connect をサポートしてる IdP なら認証の設定ができるようになりました。OIDC サポートの IdP といえば、Auth0, Okta, AWS の Cognito や LINE とかぱっと浮かぶところでしょうか。

そして Azure AD B2C もですね。

今回は知名度も人気もいまだまだ低いであろう Azure AD B2C での設定方法をやっていきましょう。

ちなみに B2C 以外のOIDC プロバイダーを使った場合でも、SWA の設定やコードのは後半の SWA の認証設定を見れば、応用して設定できるはずです。

SWA の準備

SWA 作成における注意点

SWA のリソース作成時に最も重要な注意点として、今回は Custom Authentication の機能を使うため、Standard プランにする必要があります。 Free プランだと動きません (404 になる)。Plan による機能の違いは現時点ではざっくり以下図ですが、必要に応じて最新の情報をドキュメントで確認しましょう。

SWA のリソース作成

上記で書いたStandard プランの設定に注意点を考慮しつつ (←私がハマったので何度も書く...)、事前に SWA (Static Web Apps) のリソースを Azure 上に作っておきましょう。というのはこの後 Azure AD B2C をセットアップする際に SWA の URL が必要になるからです。

初めてやるって方は、以下のチュートリアルにあるようにコードをクローンして Azure にデプロイするまでをやってみると簡単です。クローンするコードも Vue や React のテンプレートが選べていい感じです。今回私は Vue のテンプレートで作って話を進めますが、コードは数行程度しか書かないのでほかのテンプレートでやっても問題ないです。

クイック スタート: 静的 Web アプリを初めてビルドする | Microsoft Learn

チュートリアルをやったりして SWA のリソースを作ったら Azure ポータルで SWA のリソースを開き URL をメモしておきましょう。

Azure AD B2C の設定

B2C の設定しながら"3つ"の値をメモして、次の SWA の設定につなげていきます。

テナントの作成

Azure AD B2C のテナントがない場合はテナントの作成からですね。これは Azure Portal からポチポチやるだけなのでドキュメントのリンクを貼っておくだけとします。

docs.microsoft.com

アプリケーションの設定

アプリケーションの登録

B2Cとは」とか「B2Cのアプリケーションとは」とかの話すと長くなるので一切せずガンガン進みます。

Azure ポータルで Azure AD B2C のリソースを開き アプリの登録 (①) > 新規登録 (②) をクリックします。

アプリケーションの登録は以下図のように設定します。四角でかこってない部分はデフォルトでよいです。名前は適当につけときます。

リダイレクト URI が重要なポイントの一つです。
前述でメモした SWA の URL の後ろに /.auth/login/b2c/callback をつけた値を入力します。例えば SWA の URL が https://abcd1234.azurestaticapps.net/ だったら以下のようになります。

https://abcd1234.azurestaticapps.net/.auth/login/b2c/callback

リダイレクト URI の値は公式ドキュメントにある通りにセットする感じですが、"/.auth/login.." を見ると、App Service Authentication の機能を使ったことがある方だとそっちの機能ねーと感じることがあるかもしれませんね。
/.auth/login/b2c/callback の "/callback" も前についてる "b2c" という値は、今回は私自身が勝手に決めたものでカスタム可能な要素です。あとで SWA で staticwebapp.config.json を構成する際に設定する値と合わせる必要があるってことだけ覚えておくとよいです。

登録ボタンをクリックしましょう。登録したアプリケーションのメニューになります。

認証 をクリックすると先ほど入力した内容が確認できます。フロントチャネルのログアウト URL を入力しておきましょう。値は、前述でメモした SWA の URL の後ろに /.auth/logout をつけた値を入力します。入力したら画面上部の保存ボタンを忘れずクリックしましょう。

シークレットの登録

次に 証明書とシークレット をクリックし (①) 、新しいシークレット をクリックして ます(②) 。シークレットの作成画面がでますので、作成しましょう。有効期限は適宜決めていただければよいです。余談として期限が切れるともちろん無効になりますが、期限が切れる前にメール通知がきます。

作成したシークレットは後から値の確認ができませんので、メモしておきましょう。メモすべき1つ目の値です。

アプリのクライアントID の確認

ここでこの B2C のアプリのクライアント ID を確認しておきます。概要 をクリックすると表示されますのでメモしておきます。 これは SWA で staticwebapp.config.json を構成するのに必要になります。これがメモすべき2つ目の値です。

ユーザーフローの設定

ユーザーフローの作成

B2C のメニューは慣れないとわかりにくいのですが、今開いてるのが B2C のアプリのメニューなので、B2C 自体のメニューに戻るために画面上部の Azure AD B2C をクリックします。

ユーザーフロー ついては詳しく語りませんが進めていきましょう。

ユーザーフロー をクリックし (①)、新しいユーザーフローをクリックします (②) 。

ユーザーフローのタイプは サインアップとサインイン を選択します。バージョンを選べますので、推奨 のやつを選び、画面下部の作成ボタンをクリックします。

作成の画面を設定していきましょう。

  • 名前: ドキュメントでも susi とか入ってる例が多いですが、これは寿司が好きなわけではなく "Sign Up Sign In" の略として使わています。
  • ローカルアカウント: Email signup にチェックを入れておきます。これで Email でサインアップできるようになります。ここの設定次第では GitHub や Facebook といったソーシャル ID プロバイダーでサインアップやサインインさせることも可能な便利な B2C なんですが、今回は EMail のみにします。

画面を下にスクロールするとユーザー属性とトークン要求 があります。詳細を表示 をクリックします。

作成画面が表示されます。ここで表示名のチェックボックス2つにチェックを入れておきましょう。

今回は B2C の細かい話はするつもりはないのですが、

  • 表示名属性を収集する のチェックをつけるとサインアップ時にその情報を入力できるようになります。表示名 できれば認証後に入力した表示名が取得できるようになります。ユーザーフロー作成後は、各ユーザーフローの「ユーザー属性」で確認できます。
  • 表示名要求を返すのチェックは必須です。この後 SWA でこのトークンを読み取る設定をするのでトークンがないとエラーになるためです。ユーザーフロー作成後は、各ユーザーフローの「アプリケーション要求」で確認できます。

この画面の OK ボタンをクリックし、前の画面に戻ったら 作成ボタンをクリックして作成を完了させます。

metadata の URL を確認

ユーザーフローが作成出来たらそのフローをクリックします。

ユーザーフローを実行します をクリックすると、 OpenID Connect metadata 情報の URL が出てきます。これもあとで使うのでメモしておきましょう。メモすべき3つ目の値です。

ここまでで、

  • クライアント ID
  • シークレット
  • metadata の URL

の3つをメモしました。これを使って残りの設定を進めていきましょう。

SWA の認証設定

SWA の環境変数をセット

まずは、先ほどメモした値のうちクライアント ID とシークレットを環境変数にセットしていきましょう。

Azure ポータルで SWA のリソースを開き、構成 をクリックし (①) 、追加をクリックします (②) 。

前述でメモしたクライアント ID とシークレットの2つをセットします。

今回クライアント ID の名前は B2C_CLIENT_ID、シークレットの名前は B2C_SECRET とつけて値をセットしました。こんな感じです。

プログラムを変更 1: staticwebapp.config.json

さて、ようやくプログラムをいじるときがきました(たいしたことしませんが)。

まず、プロジェクトのルートに staticwebapp.config.json というファイルがなければ追加します。

あとは、以下の json を追加します。

{
    "auth": {
        "identityProviders": {
            "customOpenIdConnectProviders": {
                "b2c": {
                    "registration": {
                        "clientIdSettingName": "B2C_CLIENT_ID",
                        "clientCredential": {
                            "clientSecretSettingName": "B2C_SECRET"
                        },
                        "openIdConnectConfiguration": {
                            "wellKnownOpenIdConfiguration": ""
                        }
                    },
                    "login": {
                        "nameClaimType": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name",
                        "scopes": ["openid", "profile"]
                    }
                }
            }
        }
    }
}

wellKnownOpenIdConfiguration の値が空にしてますので、ここに B2C の設定でメモしたmetadata の URL を貼り付けます。

補足として、metadata の URL はこちらのドキュメント にあるように "https://<TENANT_NAME>.b2clogin.com/<TENANT_NAME>.onmicrosoft.com/<POLICY_NAME>/v2.0/.well-known/openid-configuration" の形式でも結局は一緒です。ユーザーフローの名前が query string parameter か url に組み込まれているかの違いなだけですね。

その他のポイントでちょっと解説しておくと、

  • customOpenIdConnectProviders の下に b2c ってあります。これは自分で定義した値で、B2C のアプリのリダイレクト URI の後ろの方の /.auth/login/b2c/callback の login の後ろの値と合わせることで、この json の情報を読み取る仕組みになってます。ほかの名称を付けるときはこの部分とリダイレクト URL の部分の値を合わせて設定してあげましょう。
  • clientIdSettingNameclientSecretSettingName : ここは実際の値ではなく環境変数のキーをセットする必要がるので注意が必要です。今回だと、先ほど Azure の SWA の環境変数をセットしたときの名前をセットしています。
  • nameClaimType: これをセットしているため、B2C のユーザーフローの ユーザー属性とトークン要求表示名 の設定をしました。設定を忘れるとトークンにこれが含まれていないためエラーになります。

この json の設定のドキュメントの記載は以下になります。

https://docs.microsoft.com/ja-jp/azure/static-web-apps/authentication-custom?tabs=aad#register-your-application-with-the-identity-provider

ただ、2021年5月21日時点だと、この公式ドキュメントの Json に誤りがあるためドキュメント通りだと動かず、このブログの Json で動作します。
今後どう更新されるか(ドキュメントを直すのかまたは SWA の内部を直すのか?)で動向がかわるためチェックしていく必要がありそうです。変更の仕方次第ではこのブログでやってる現時点での正しい方法が動かなくなることもありそうですね。

正しいスキーマについては、JSON Schema StoreAzure Static Web Apps configuration file を開くと確認が可能なので、しばやんの以下リンクのブログの「staticwebapp.config.json の設定ミスを減らす」って部分で書かれているように設定すると、入力補完がきかせてミスを減らせます。これは必ずやった方がよいですね。

blog.shibayan.jp

プログラムを変更2: リンクをつけてみる

私の場合は Vue のテンプレートでプロジェクトを作成したので、App.vue を開いて 公式ドキュメントを参考にこんな変更を加えてみました。リンクつけずに手で URL をたたいてもいいんですがねw。

  • login の url をたたくリンクをつけました。 url は /.auth/login/b2c です。
  • login したら自分の情報を確認するためのリンクをつけました。url は /.auth/me です。
<template>
  <div>
    Hello {{ value }}
    <div>
      <a href="/.auth/login/b2c">Login</a>
    </div>
    <div>
      <a href="/.auth/me">My Info</a>
    </div>
  </div>
</template>

<script>
export default {
  name: "App",
  data() {
    return {
      value: "World",
    };
  },
};
</script>

では、実際に認証を動作するには、App Service の認証機能をつかっているため Azure のリソースにデプロイする必要があります。コードを GitHub の repo に push すると自動で CI/CD が動作します。main ブランチは Pull Request 必須がついてるので、PR 作って merge する必要ありますが、そこの説明は省きます。

デプロイできたら動作確認してみましょう。

動作確認

動作確認は、Firefox ならプライベートウインドウ、Microsoft Edge なら InPrivate とかでやった方が無難です。私は Chrome なのでシークレットウインドウを開いて、SWA の URL を開いてみます。

シンプルで味気の無い画面が表示されます。

My Info をクリックすると、認証してないので clientPrincipal の値がありません。リンクを作ってない場合は .auth/me をたたけば見れます。

Login してみましょう。設定が間違っていなければ Azure AD B2C のログイン画面が表示されます。はじめての場合は、下の方の Sign up now をクリックしてサインアップできます。

サインアップのフローではデフォルトでメールアドレスの verification がついてるので、メールアドレスを入力したら Send verification code をクリックしてそのメールに送られきたコードを入力してverification する必要があります。

設定ミスエラーとか起こして何度かトライしててユーザーができてんのか不明な場合は Azure AD B2C のユーザーのメニューをみると確認できます。

B2C の余談として、このサインイン画面はそこそこ自在にカスタマイズできます。

サインインすると、トップ画面に戻ります。My Info を見るとログイン前に null だった情報が取得できているが確認できます。

ログアウトしたい場合は、./auth/logout を手入力すればできます(リンクつければよかったか)

サイトに認証をかけるには

今はログインしてるだけで Static Web Apps 自体に認証をかけてないですが、サイト全体や、一部のページに認証をかけることができます。routes で "allowedRoles": ["authenticated"] とかつければ簡単に実現できますが、詳細は以下のドキュメントで確認できます。

https://docs.microsoft.com/ja-jp/azure/static-web-apps/configuration#securing-routes-with-roles

トラブルシュート

認証でエラーになった場合は、設定するポイントは数個ですが値のミスによることがほとんどだと思うので、ひとつずつ確認していきましょう。場合によってはブラウザの DevTools でネットワークをみると URL のパラメーターでエラーの内容がわかることもあるので、ネットワークの遷移を見て確認するのもひとつの手です。

ありがちなエラーと改善方法を書いておきます。

ログインの URL で 404

B2Cの設定は正しいのにログインしようとして B2C の画面に遷移せず404 のエラーが出るときは、Hosting Plan が Free になっているからかもしれません。冒頭でも書きましたが、Custom Authentication を利用するには Standard plan じゃないと動かないのでチェックしましょう。

認証後に 403 Forbidden

ログイン画面が表示してログインはできたっぽいのに .auth/complete の画面が表示されて 403 Forbidden ...この原因のひとつとして、トークンで必要な値を読み込めなくてしんでる可能性があります。前述の ユーザーフローの作成表示名 の設定を紹介してますが、それをしてないときにはなります。B2C のリソースでユーザーフローを開き「アプリケーション要求」から表示名にチェックがついているか確認しましょう。チェック後、反映まで数分かかるので、反映されたかを確認するには、ユーザーフローのリダイレクト URL に https://jwt.ms をつけてトークンを確認するのがよいです (その場合 implicit flow が必要となるので、app の認証で implicit flow を有効にする必要がありますが...)。

または、metadata の URL の設定 (staticwebapp.config.json でのwellKnownOpenIdConfiguration の設定)で、URL の最後にユーザーフロー名がついているんですが、そこを確認してみるとうっかり間違っているなんてこともあるでしょうか。

参考

docs.microsoft.com

PowerShell で Install-Module をするとエラー "Administrator rights are required to install or update ..." を解決する

PowerShell (v7.1.3)Install-Module コマンドを使って Az.CosmosDB 2.0.1-preview をインストールしようと思ったらこんなエラーが...

Install-Package: C:\program files\powershell\7\Modules\PowerShellGet\PSModule.psm1:9711
Line |
9711 |  … talledPackages = PackageManagement\Install-Package @PSBoundParameters
     |                     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     | Administrator rights are required to install or update. Log on to the computer with an account that
     | has Administrator rights, and then try again, or install by adding "-Scope CurrentUser" to your
     | command. You can also try running the Windows PowerShell session with elevated rights (Run as
     | Administrator).

f:id:beachside:20210517155438p:plain

私が解決した方法をメモしておきます。あくまで私の自己責任での解決なので(変なことしてはいないけど)ご注意ください。

Try and Error

PowerShell を Administrator で実行してみる

エラーメッセージをおさらいしましょう。

Administrator rights are required to install or update. Log on to the computer with an account that has Administrator rights, and then try again, or install by adding "-Scope CurrentUser" to your command. You can also try running the Windows PowerShell session with elevated rights (Run as Administrator).

素直に PowerShell を Administrator で実行してみましたが解決できず。"-Scope CurrentUser" をつけて実行してもエラーのまま解決できず。

違うやろなとは思いましたが、切り分けとしてとりあえずエラーメッセージに書いてあること油断せずにやっておくことは大事ですね。

PowerShell がブロックされてるか確認

Install できんのやらなんかブロックされとるやろってことで確認してみましょう。

Windows メニューで「ランサムウェアの防止」と入力してそのメニューを開きます。

f:id:beachside:20210517160229p:plain:w480

ブロックの履歴を見てみましょう。

f:id:beachside:20210517160500p:plain:w720

ちなみにここも最初は見れなくてウインドウが落ちたりしましたが、時間が経ったら見れました(原因はわからん)。ここを見ようとしたけどみれない場合は気にせず次の操作(アプリにフォルダアクセスを許可させる)に進みましょう。そこでもブロックされたプログラムが確認できます。

f:id:beachside:20210517160429p:plain:w720

pwsh.exe ブロックされとるぅぅぅ!

アプリにフォルダアクセスを許可させる (解決)

ランサムウェアの防止のウインドウで アプリをコントロールされたフォルダーアクセスで許可する をクリックします。

f:id:beachside:20210517160821p:plain:w720

許可されたアプリを追加する をクリックして 最近ブロックされたアプリ をクリックすると、pwsh.exe がいました。

一応ここでも書いておきます。

pwsh.exe ブロックされとるぅぅぅ!

f:id:beachside:20210517155639p:plain:w720

ということで pwsh.exe を追加します。

f:id:beachside:20210517155115p:plain:w720

これでコマンドを再度実行したら無事に実行できました♪

f:id:beachside:20210517161036p:plain

PowerShell で Azure に サインイン / テナント ( サブスクリプション ) 切り替え

PowerShell での Azure の操作は頻度の少ない私ですが、Power Shell を使って Azure のなんらかの操作をしたいときに、最初に行うサインインやサブスクリプションの確認・変更をブログで都度書くと長くなるなーと思ったのでここで改めて書いてきます♪

予備知識: レガシーな AzureRM PowerShell と新しい Az PowerShell モジュール

PowerShell で Azure を操作する際、現時点では AzureRM PowerShellAz PowerShell があります。

AzureRM PowerShellGet-AzureRmContext みたい "AzureRm" ってのがつくコマンド(全部のコマンド見たわけではないので雑なイメージですがw)。まだ数年使えますが2024年2月29日で廃止になるロードマップが出ています。

そして Az PowerShell モジュールを使う流れになっているようですね。既に AzureRM PowerShell の全機能が利用できるようになっています。Az PowerShell の特徴としては Connect-AzAccount ってコマンドのように "Az" が含まれていることです(全部のコマンド見たわけではないし未来はわからんのでざっくりな例ですがw)。

docs.microsoft.com

つまりは、

PowerShell で Azure を操作する際ドキュメントとかググってて "AzureRm" ってついたコマンドを見かけたら「これ古い方やん」と察して Az PowerShell で同じことできるコマンドを探す

ってことですね。

ということで、ここでは Az PowerShell を使ったサインインや、テナント(サブスクリプション)の切り替え方法をメモしていきます。

Az PowerShell module のインストール

インストール方法は、公式ドキュメントで3種類紹介されています。PowerShell のバージョンの依存についても書かれているので、初めて使う方はさらっと読んだ方がよいでしょう。

私自身は(個人的に一番楽と感じてる) PowerShellGet でさくっとインストールしたと思いますが、忘れましたw。

Azure にサインイン(接続)する

サインインするコマンドはこれです。これ実行してそんなコマンドないって感じのエラーが出たら、前述のインストール忘れです。

Connect-AzAccount

実行するとデバイスコードフローでのログインが求められます。どの URL にいって指定のコードを打てとメッセージが表示されるので、ブラウザを起動してその通り進めます。

場合によってはコードが出ずにすっと認証されることもありますが、そのときは迷う要素がないので触れずにいきますかね

f:id:beachside:20210517120339p:plain

指定の URL をデバイスコードの入力が求められ、それから Azure にログインします。

f:id:beachside:20210517120534p:plain

サインインを進めてこの画面に来たら、PowerShell に戻りましょう。

f:id:beachside:20210517120601p:plain

PowerShell の画面に戻ると、どのアカウントでどのテナント (サブスクリプション) にサインインしたか表示されます。

f:id:beachside:20210517144021p:plain

現在サインインしてるテナント (サブスクリプション) を確認

前述のようにサインインすると現在のテナント (サブスクリプション) がわかりますが、色々操作してるとたまに確認したくなることがあります。その時はこのコマンドです。

Get-AzContext

f:id:beachside:20210517141309p:plain

テナント (サブスクリプション) を切り替える

テナント (サブスクリプション)」 と毎回書いてますが、ググるときにどちらでもヒットしてくれたらなと思って書いてるだけです

今接続している テナント (サブスクリプション) を切り替えたいことも多々あります。

テナント (サブスクリプション) の一覧を確認

テナント (サブスクリプション) を切り替えるためには、その ID または Name が必要です。それを確認するには、Azure ポータルをみて確認するか、もしくは以下のコマンドで確認するのが良いでしょう。

※ コマンドを実行するにはもちろんサインインしている必要があります。

Get-AzSubscription

こんな感じで結果が表示されます。

f:id:beachside:20210517134909p:plain

パラメーターを指定した実行など詳しいことは、以下のドキュメントで確認できます。

docs.microsoft.com

サブスクリプションを切り替える

前述の Get-AzSubscription で一覧を表示していればそこで表示された Id をセットして以下のコマンドを実行すれば、切り替えることができます。

Set-AzContext -Subscription "ここに subscription の id をセット"

f:id:beachside:20210517135351p:plain

ドキュメントはここです。

docs.microsoft.com

参考

docs.microsoft.com

Microsoft Graph API で グループ 情報を操作する ( C#, .NET SDK )

前回のブログでは、C# の SDK を使ってユーザーの操作について触れましたが、今回はグループの操作をしていきましょう。

事前準備

この3つは前回のブログと同様なので省略します。

  • Azure AD で Microsoft Graph のアクセス許可を追加 (必要に応じて)
  • コンソールアプリの作成
  • GraphServiceClient の初期化

blog.beachside.dev

グループの作成

グループを作成するうえでまず Azure ポータルで Azure AD を作成する際の UI を見てイメージをつかんでおきましょう。

  • グループの種類 は、利用するアプリ側の認可目的で作成するのであれば セキュリティ でよいです。
  • メンバーシップの種類 は、割り当て済み でよいです。もう一つ選べる動的メンバーシップ はルールベース(例えば部署がどこだったらどのグループに割り当てるとか)で Premium plan の機能ですがここでは使用しません。

グループの作成(ユーザー追加無し)

上図の UI をコードで書くと以下の感じ。Group のプロパティの細かい設定が必要な場合はこちらの公式ドキュメントをチェックするとして、サクッと作るにはこれで十分です。

// Group の作成 ( Group の namespace は "Microsoft.Graph" です)
var group1 = new Group
{
    DisplayName = "Group 1",
    Description = "Group 1 description",
    SecurityEnabled = true,
    MailEnabled = false,
    // 必須のプロパティのためメールを使わなくても入力が必要なので、任意のものを入れておく。
    MailNickname = "mail"
};
// graphServiceClient のインスタンス化は前回のブログにて
var createdResponse = await graphServiceClient.Groups.Request().AddAsync(group1);
// レスポンスから生成された Group の Id を確認できます。
Console.WriteLine($"{createdResponse.Id} - DisplayName: {createdResponse.DisplayName}; Description: {createdResponse.Description}");

グループの作成(ユーザー追加もする)

グループの作成と同時にユーザーまたはオーナーを追加する場合、ユーザーの ID が必要になります。

いくつか癖があるので、公式のドキュメントを眺めた方がよさそうです。まず、このブロブ書いてる時点じゃ公式ドキュメントのコードじゃ動かないことが最大の癖です(GitHub の issue でなんか書いてたのでそれ以上のことはフィードバックしませんが...)。
あとは、1つの要求で20人までしか追加できないようなので人数が多いときは考慮する必要があります。

作成と同時にユーザーを追加するには、AdditionalData プロパティに値を追加します。
具体的なコードはこんな感じ。

// Owner に追加したいユーザーの一覧があるとする
var ownerIdsToAdd = new[] { "80490496-a683-4e68-9175-017ffd7f99bd" };
// グループに追加したいユーザーの一覧があるとする
var userIdsToAdd = new[] { "80490496-a683-4e68-9175-017ffd7f99bd", "9fa001c0-04da-4440-ac7c-f5389cebe7d1" };

var group101 = new Group
{
    DisplayName = "Group 101",
    Description = "Group 101 description",
    SecurityEnabled = true,
    MailEnabled = false,
    MailNickname = "mail",
    AdditionalData = new Dictionary<string, object>
    {
        // Owner の追加
        ["owners@odata.bind"] = ownerIdsToAdd.Select(id => $"https://graph.microsoft.com/v1.0/users/{id}"),
        // User の追加
        ["members@odata.bind"] = userIdsToAdd.Select(id => $"https://graph.microsoft.com/v1.0/users/{id}")
    }
};

var createdResponse = await graphServiceClient.Groups.Request().AddAsync(group101);

人数の制約とかあって気にする必要がある場合作成と同時にユーザーやると面倒なので、グループの作成 (+必要あればオーナーの追加、オーナーは少数しか追加しない想定ですが)で作成してから、別途ユーザーを追加した方がシンプルにはなりそうです。

グループにメンバーを追加する

上記の流れできたので別途メンバーを追加してきましょうか。メンバーの追加はひとりずつ追加する方法と複数人を一括追加する方法があります。

ひとりずつ追加

以下のように DirectoryObjectAddAsync すればよいです。

// TODO: 対象のグループの ID をセット
var targetGroupId = "";
// TODO: 追加したいユーザーの ID をセット
var userIdToAdd = "";

var target = new DirectoryObject { Id = userIdToAdd };

await graphServiceClient.Groups[targetGroupId].Members.References.Request().AddAsync(target);

既にグループ内にいるユーザーを追加しようとすると Exception が発生します。そりゃそうだろって感じですが、自分の中で確認したい点の一つだったので念のため書いておきます、こーゆーとこあんまり信用してないので。

Exception の Code は BadRequest で ErrorMessageはこんな感じ: One or more added object references already exist for the following modified properties: 'members'.

複数人を一括追加

(これも公式ドキュメントに記載されてるコードでは動作しませんが...)
複数人を一括追加する場合は、前述で group 作成時にユーザーを追加する方法と同じ感じです。最後に UpdateAsync を使うって流れです。

// TODO: 対象のグループの ID をセット
var targetGroupId = "";
// グループに追加したいユーザーの一覧があるとする
var userIdsToAdd = new[] { "80490496-a683-4e68-9175-017ffd7f99bd", "9fa001c0-04da-4440-ac7c-f5389cebe7d1" };
// 追加するグループのオブジェクトを作成
var group = new Group
{
    AdditionalData = new Dictionary<string, object>()
    {
        ["members@odata.bind"] = userIdsToAdd.Select(id => $"https://graph.microsoft.com/v1.0/users/{id}")
    }
};

await graphServiceClient.Groups[targetGroupId].Request().UpdateAsync(group);

このコードでは考慮してませんが、一括の最大が20人なので必要に応じて処理を追加すればよいです。 ちなみにこちらも既にグループ内にいるユーザーを追加しようとすると前述同様のエラーになります。Exception の ErrorMessage も同じです。

グループの取得

group を ID を指定して情報を取得するはドキュメント通りで動きます。ユーザーを ID 指定で取得する方法を知ってると予想がつくやつです。

// TODO: 対象のグループの ID をセット
var targetGroupId = "";
var g1 = await graphServiceClient.Groups[targetGroupId].Request().GetAsync();
Console.WriteLine($"{g1.Id}; DisplayName:{g1.DisplayName}; Description:{g1.Description}");

グループ内のメンバーを取得

Azure AD の Group は Group のメンバーに Group を入れることができるので。以下の2通りの取得方法が可能です。

  • グループの直下のメンバーのみを取得する
  • グループの直下のメンバー + ネストされたグループのメンバーも取得する

グループの直下のメンバーのみを取得する

Members メソッドを使って取得してみましょう。メンバーにはユーザーとグループが存在する場合は入ってくるので、一覧を見るならこんな感じでみることができます。

メンバーの中にいるグループの中のユーザーの情報はこれでは見えません。

// TODO: メンバーを取得したいグループの ID をセット
var targetGroupId = "";

var membersOfGroup1 = await graphServiceClient.Groups[targetGroupId].Members.Request().GetAsync();

foreach (var page in membersOfGroup1)
{
    switch (page)
    {
        case User user:
            Console.WriteLine($"USER: {user.Id} DisplayName: {user.DisplayName}; mail: {user.Mail}");
            break;

        case Group group:
            Console.WriteLine($"GROUP: {group.Id} DisplayName: {group.DisplayName}; Description: {group.Description}");
            break;

        default:
            Console.WriteLine("---");
            break;
    }
}

ユーザーのみを指定したいなら、前回のブログでも紹介したように OfType<T> を使って抽出できます。
また、前回のブログでも触れましたが、Select を使って最小限のデータを取得しましょうって場合、今回のように Group と User の複数の型が入ってくると多少面倒ですね。目的がユーザーの ID と名前だけ取得したいなら、シンプルにこう書けます。

// TODO: メンバーを取得したいグループの ID をセット
var targetGroupId = "";
// 本来は `Select` を使って必要な情報を最小限に取得しましょうね。
var membersOfGroup1 = await graphServiceClient.Groups[targetGroupId].Members
                                                       .Request()
                                                       .Select("id,displayName")
                                                       .GetAsync();

foreach (var user in membersOfGroup1.OfType<User>())
{
    // Select で Mail を指定してないので常に空になる
    Console.WriteLine($"USER: {user.Id} DisplayName: {user.DisplayName}; mail: {user.Mail}");
}

グループの直下のメンバー + ネストされたグループのメンバーも取得する

ネストされた?ネストした?...どうでもいっか...

ユーザーの一覧を取得したい場合は、(私個人の場合は)ネストされたグループ(グループの中のグループ)のメンバーの取得できるこちらを使うケースがメインです。

コードは先ほどとほぼ一緒ですが、Members ではなく TransitiveMembers を見ます。前述同様にユーザーもグループも入ってくるので、ユーザーの情報だけほしいなら、定番の OfType<T> です。

// TODO: メンバーを取得したいグループの ID をセット
var targetGroupId = "";
var transitiveMembers = await graphServiceClient.Groups[targetGroupId].TransitiveMembers
                                                    .Request()
                                                    .GetAsync();
// User のみを出力
foreach (var user in transitiveMembers.OfType<User>())
{
    Console.WriteLine($"{user.Id} DisplayName: {user.DisplayName}; mail: {user.Mail}");
}

どうでもいい余談ですが .Request() とかの改行してる場所微妙なのはブログの css の都合です。

その他の操作

全部を試したわけではないですが、ここまで書いた書き方や癖を把握しておくと、あとはドキュメントのままでサクッと動かせそうなのでここまでとします。

やりたいことに応じて以下のドキュメントを参考にコードを書いていけばばっちりです、たぶん。

docs.microsoft.com

Microsoft Graph API で ユーザー 情報を操作する ( C#, .NET SDK )

Microsoft Graph API を使って Azure Active Directory (略して Azure AD とか AAD) からユーザーの情報を操作するのってかなり昔から使ってるんですがブログにしたことがなかったので改めて書いてみようと思いました。

今回は C# の SDK を使ったユーザーの操作をメモします。

ちなみに認証フローは、Web API の中から管理系の操作をしたいので Client Credentials フローを使います。Web API から Graph API をコールするときは On Behalf Of を使うことも多いと思います。

今回は Graph API と SDK の利用がメインなので認証フロー周りは細かく話しませんが、シナリオと認証フローの選択についてはこちらのドキュメントを見るとわかりやすいと思います。

docs.microsoft.com

事前準備: Azure AD で Microsoft Graph のアクセス許可を追加

Microsoft Graph API は、デフォルトではログインしているユーザーのプロファイルしか取得できません。そのため、例えばユーザーの一覧を取得したいとか自分以外のユーザーの情報を取得するにはアクセス許可を追加してあげる必要があります。

アクセス許可の設定方法は、前回のブログにまとめましたので不明な場合はご参考ください。

blog.beachside.dev

今回は以下3つの情報が必要になりますので準備しておく必要があります(これも含め上記のブログに書いてあります)。

  • テナント ID
  • クライアント ID
  • シークレット

以降では特にアクセス許可には触れず SDK を使った操作を中心に書くので、認可関連のエラーが出たときは必要に応じて設定すれば解決できます。具体的には、一番最後にドキュメントのリンクを付けています。やりたいこと(「ユーザーの一覧を取得する」とか「ユーザーを招待する」とか)のドキュメントを開くとどのアクセス許可が必要か書いてますので、それを設定していく流れです。

余談: SDK の変遷

2020年前半に Service library ( .NET だと Microsoft.Graph ) と Core library ( .NET だと Microsoft.Graph.Core) に分かれた構成になりました。

この2つの責務を軽く説明すると、

  • Service library: Graph API の metadata の更新(頻繁にある)に合わせて機能を更新していく。将来的には metadata からコードを自動生成できるようにしたいらしい(あくまで将来的なゴール)。
  • Core library: インフラ周り、 reliability や resilient とかの実装とか。コア機能なのでリリースサイクルはゆっくり。手で書くコードはこっちのみにしたい。

この構成への変更前は、Microsoft.Graph.Beta に preview 機能があって GA 版が Microsoft.Graph っていう微妙に扱いにくい構成だったのでいい感じに変わってくれた印象です。

コンソールアプリの作成

冒頭で Client Credentials フローを使うと書きましたが、イメージしやすいようにユースケースの例を書くと、ユーザーを管理している Web API があり、その API が Graph API を使ってユーザーの登録とかを行うイメージです。

Web API のサンプルコードを書くと DI とか余計なことを書きたくなって脱線するので、コンソールアプリを作って進めます。

まずは Visual Studio 2019 を開いてコンソールアプリを作るだけなので手順は省略します。

.NET のターゲットフレームワークは .NET Core 3.1 を使います。最新の .NET 5でも NuGet Package に依存するとこはとくにないので問題ないですが、状況的にまだ私は .NET 5 を利用する気になっていないからです。

NuGet パッケージのインストール

Microsoft.GraphMicrosoft.Graph.Auth の2つを使います。 Visual Studio で NuGet パッケージの管理を開き「 Microsoft.Graph」と検索すると表示されますのでインストールしましょう。注意点として、このブログを書いた時点だと Microsoft.Graph.Auth は pre-release 中なので プレリリースを含める にチェックを入れてあげる必要があります。

一応このブログで使ったバージョンは以下です。

  • Microsoft.Graph: v3.28.0
  • Microsoft.Graph.Auth: 1.0.0-preview6

あとはコード書いていくだけです♪

GraphServiceClient の初期化

Microsoft Graph API にアクセスするには、GraphServiceClient という client を使ってアクセスします。初期化方法はこんな感じ。コード内の TODO 3か所は前述の「事前準備: Azure AD で Microsoft Graph のアクセス許可を追加」で触れた Azure AD から取得する情報です。

Client credentials 以外での初期化方法はこちらの公式ドキュメントを参考にさくっと作れます。

ユーザーの情報を取得

ユーザーの一覧取得

ユーザーの一覧はこれで取得できます。ドキュメント通りです。

var users = await graphServiceClient.Users.Request().GetAsync();

foreach (var user in users)
{
    Console.WriteLine($"{user.Id} - DisplayName: {user.DisplayName} (Email: {user.Mail})");
}

Select を使って指定したプロパティだけを取得することもできます。余計なプロパティを取得することでレスポンスのサイズが無駄に大きくなります。

そのため基本的には Select を使って最小限のプロパティのみを取得することが必須と考えましょう。

var users = await graphServiceClient.Users.Request().Select("id,displayName").GetAsync();

Select の中を文字列ではなく Func にすることで Type safety に指定することもできます。

var users = await graphServiceClient.Users.Request().Select(u => new { u.Id, u.DisplayName }).GetAsync();

ObjectId を指定したユーザーの取得

Azure Portal では Azure AD のユーザーの一意の ID は Object ID と表記されています。ASP.NET Core とかで認証してログインした場合はこの ID も取得できます。この ID をキーにユーザーのプロファイルを取得する際は以下のようになります。

// TODO: user の ObjectIdをセット
var user1Id = "";

var user1 = await graphServiceClient.Users[user1Id].Request().GetAsync();

ユーザーが所属しているグループを取得

MemberOf を使ってユーザーがどのメンバーに属しているかを取得できます。

// TODO: user の ObjectIdをセット
var user1Id = "";

var memberOf = await graphServiceClient.Users[user1Id].MemberOf.Request().GetAsync();

取得した結果は以下図の感じになります。ユーザーの一覧を取得したときも同様ですが CurrantPage の中にほしい情報が入ってきます。これは Microsoft Graph の特徴のひとつです。CurrantPage は DirctoryObject 型となっていますが雑に説明すると IList<T> の Generics で色んな型のオブジェクトが入ってきます。

デバッグして CurrantPage の中をみるとわかりますが、ユーザーが Group だけじゃなくなんらかの Role に属してると両方の情報が取得できます。

foreach (var obj in memberOf)
{
    switch (obj)
    {
        case DirectoryRole role:
            Console.WriteLine($"Role {role.Id}: {role.DisplayName}");
            break;
        case Group group:
            Console.WriteLine($"Group: {group.Id}:{group.DisplayName} ({group.Description})");
            break;
    }
}

例えば Group だけを取得したい場合、OfType を使って以下のように取得することができます。

foreach (var group in memberOf.OfType<Group>())
{
    Console.WriteLine($"Group: {group.Id}:{group.DisplayName} ({group.Description})");
}

その他の操作

SDK を利用する上での癖というか特徴はこれくらいです。あとの操作はドキュメントを見ながらやれば前述のやり方を考慮するだけでサクッとできます。

user リソースの種類 - Microsoft Graph v1.0 | Microsoft Learn

ユーザーを招待する

Azure AD にユーザーを追加する場合、私の場合はユーザーを新規に作成することはほぼなく、招待する方が多いです。そりゃそうですよね、新たにアカウント作ってパスワードも管理するなら、既存のどっかのアカウントを招待することでアカウントを増やすことなくすむので。

ということでユーザーを招待してみましょう。

以下のコードでは TODO が2つ、招待する相手のアカウントの Email とリダイレクトする URL をセットする必要があります。また、招待メールを送信するには SendInvitationMessagetrue にしてあげる必要があります。

// TODO: 招待を送るユーザーのメールをセット
 var targetUserEmail = "";
// TODO: 同意後、リダイレクトする URL をセット
 var inviteRedirectUrl = "";

var invitation = new Invitation()
{
    InvitedUserEmailAddress = targetUserEmail ,
    InviteRedirectUrl = inviteRedirectUrl ,
    InvitedUserMessageInfo = new InvitedUserMessageInfo() {MessageLanguage = "ja-jp"},
    // default: false。true をセットしないと招待メールは送信されない
    SendInvitationMessage = true
};

// 招待を実行
var response = await graphServiceClient.Invitations.Request().AddAsync(invitation);
// レスポンスから、採番されたユーザーの ID が確認可能
Console.WriteLine($"OID: {response.InvitedUser.Id}; InvitedEmail:{response.InvitedUserEmailAddress}");

レスポンスからユーザーの ID がわかるので、このユーザーを後続処理で Group に追加するときなどの操作に使えます。

招待メールは、Azure AD から招待を受けたことがある人なら見覚えのありそうなのが届きます。招待の承諾をすると、inviteRedirectUrl に設定した URL にリダイレクトします。

あとは動作で気になる点としては...

  • 招待を同じ Email に複数回送信しても、新なユーザーは作られず同じ ID に再送するだけとなります。
  • 招待を承諾した後に再度招待メールを送信しても同様です。
  • Invtation を作る際、InvitedUserMessageInfo の中の MessageLanguage のdefault は en-us とドキュメントにありますが、動作を確認したところこの値を設定しなくても日本語でメールが送られてきました。Azure AD の Notification language を English に変えてもかわらんかったで、デフォルト値はどっか別のところにありそうですね(もしくは変更の反映に時間がかかるとか?)。まぁセットしておけば間違いないでしょう。

終わりに

Microsoft Graph の操作も、C# の SDK の癖さえわかってしまえば簡単にできます♪

Group の操作については次のブログで整理しました。

blog.beachside.dev

参考

docs.microsoft.com

docs.microsoft.com

Azure AD で Microsoft Graph API のアクセス許可を追加する

Microsoft Graph API を使って情報を取得する際、デフォルトではログインしたユーザーのプロファイルしか見ることができません。ほかに情報を取得したい場合は、必要に応じてアクセス許可を追加してできるパーミッションを増やしてあげる必要があります。

ということで Azure AD でアクセス許可の追加をする方法と、必要なアクセス許可の探し方についてメモしてみました。

Graph API を使うために Azure AD でアクセス許可を設定する

アプリの登録

Azure Portal で Azure AD のリソースを開き、アプリの登録 > 新規登録 をクリックします。

以下を参考に入力します。今回は Graph API をサクッと試すだけなので設定方法は雑に進めますが、実際にはどんなクライアントアプリを使うとかどんな認証フローを使うかに応じて設定するところではあります。

  • 名前: 任意のものを入力します。
  • サポートされているアカウントの種類: 今回はとりあえず Graph API をたたきたいだけなので一番上のやつで十分です。
  • リダイレクト URI: 何も入力しなくてOKです。

入力したら 登録 をクリックします。

登録すると作成したアプリのメニューに遷移します。

シークレットの作成

Graph API を利用する際、認証フロー次第では Secret が必要になります。必要な場合は、左側のメニュー 証明書とシークレット をクリックし、新しいクライアントシークレット をクリックして作成します。

テナント ID とクライアント ID

SDK を利用する際必要になる情報となるテナント ID とクライアント ID はメニュー 概要 を開くと確認できますので必要に応じてメモしておきます。あとはやりたいこと次第で前述のシークレットが必要になります。

API のアクセス許可

本題のアクセス許可の設定です。

左側のメニューにある API のアクセス許可 をクリックして Graph API のアクセス許可の範囲を設定できます。下図のようにデフォルトでは User.Read のみが許可されています。これはサインインしてるユーザーのプロファイルを読み取る許可です。

アクセスの許可を追加をクリックしてやれることを増やします。

アクセス許可の要求 のブレードが開きます。デフォルトで開かれている Microsoft API のタブを下の方へするロールすると、Microsoft Graph がありますのでクリックします。

許可したいことに応じて 委任されたアクセス許可 または アプリケーションの許可 をクリックし、その下に表示されるアクセス許可を追加していきます。

必要なアクセス許可の探し方

アクセス許可の種類は細かく分かれてたくさんあります。それぞれのアクセス許可がどんなものなのかは以下の一覧は以下にあります。

docs.microsoft.com

この一覧から探すのは非効率で、やりたいことに応じてそのドキュメントを見ながらやる方が無難です。

ユーザーの操作に関するアクセス許可を探す

ユーザーの操作に関することなら以下のドキュメントを見ましょう。

docs.microsoft.com

ここにメソッドの一覧が出てます。例えばユーザーの一覧を取得したいのなら、メソッドの一覧にある user のリスト をクリックします。遷移先のページでどのアクセス許可が必要か書かれています。また、SDK や RES API での取得方法も書かれています。

グループの操作に関するアクセス許可を探す

グループの操作をしたい場合は以下に利用できるメソッドの一覧があります。ユーザー同様にメソッドの一覧からやりたいメソッドをクリックすると、必要なアクセス許可が確認できます。

docs.microsoft.com

その他のアクセス許可

上記のドキュメントのリンクを開いた際に、左側のメニューにおおよそやりたいことがありますのでそこからあさるといいでしょう。ぱっと見当たらなければ、アクセス許可のリファレンス から見る感じが楽だと思います。慣れ次第ですが。

あとは、今回紹介してるドキュメントのリンクは API v1.0 のリファレンスです。ここにないけど Beta 版でできる場合もあるので 、必要に応じて Beta 版のドキュメントも見ましょう。

終わりに

Graph API の SDK のブログを書くのにこの部分を書くと本質的じゃなくなりそうだったので別に切り出して書きました♪

開発で一度設定したそんなに変更しない部分なのでドキュメント見る頻度が低い分、ドキュメント内で迷子にならないためにもメモした感じでした。

Cognitive Services の Form Recognizer の更新情報(2021年3月編)

Form Recognizer は、v2.1 preview 3 のアップデートで実用的に使えるじゃないかってくらいに感じたので、改めて機能や更新内容をメモしてみます。

Form Recognizer とは

ざっくり機能を説明すると、請求書やレシートといったドキュメントからデータを読み取ることができるサービスです。大きく3つの機能があります。

  • Layout API: 日本語対応済み。様々なフォームのドキュメントから、テキストやテーブルなどを構造を読み取ることができます。
  • カスタムモデル: 日本語対応済み。フォームにラベル付けをして機械学習してモデルを作ることで、より複雑なフォームの分析ができるようにするサービス。フォームの学習データは5つから始めることができるのでお手軽に、かつ、いい精度の分析が十分に期待できる(個人的な肌感ですが)。
  • Prebuilt モデル (事前構築済みモデル) : Prebuilt モデルとは Microsoft が作った機械学習のモデルですが、特定のフォーム専用に作られたモデルを使ってデータを抽出します。4つは以下で、主に英語の対応のみです。
    • 請求書
    • レシート
    • 名刺
    • ID (パスポートや免許証)

最近のアップデート

細かいことは 公式ドキュメントの What's new を読むとして、ざっくりアップデートを要約します。

2021/3/15 に英語のドキュメントが更新されたため日本語のドキュメントは翻訳が追いついておらず古いです。しばらくの間は英語のドキュメントを見るのが無難です。

2020年11月のアップデート

Form Recognizer v2.1 public preview 2 のリリースに基づくアップデート情報になります。

  • Layout API とカスタムモデルで日本語サポート
  • Prebuilt の Invoice モデルの機能が強化
  • テーブル抽出の機能が強化され、結合された列、行、境界線を含む複雑なテーブルなどからのデータ抽出が可能に。

2021年3月 (Microsoft Ignite)

3/15 より v2.1 public preview 3 が気軽に試せるようになりました。このバージョンで更新された機能の情報になります。Ignite の予告通り 3/15 に英語のドキュメントはアップデートがかかりました。日本語はそのうち翻訳されるでしょう。

  • Layout API とCustom Models それぞれ73言語に対応(今回66言語増えた)
  • Prebuilt モデルに ID モデルが新規追加: パスポートやアメリカの免許証から情報を取得できる Prebuilt の API が増えた。
  • Prebuilt の Invoice モデルでフォーム内にあるテーブルの項目が取得できるようになった。
  • Prebuilt の Receipt モデルで特に明細の抽出が大幅に改善
  • テキストの行の並び順を視点できるようになった(こちらを見るとイメージがつきます)

API のバージョン

前述でさらっと触れてますが一応整理しておきましょう。

  • v2.0: 2020年6月 GA したバージョン。
  • ついこないだまでは v2.1 public preview 2 が最新でしたが、v2.1 public preview 3 が2021年3月15日に利用できるようになりました。 このブログで紹介した新しい機能を利用するにはこの API を使う必要があります。

Form Recognizer を試す準備

オープンソースで Web 上にデプロイされており Free で使える Form Recognizer Tool を設定することで簡易に Form Recognizer を試すことができます。

設定方法を書こうと思ったら長くなったので、こちらのブログにまとめました。

blog.beachside.dev

Layout API とカスタムモデルを使った所感

自分の請求書やネット上に落ちてるサンプルの請求書を Layout API で分析かけてみたんですが、完ぺきに取得できました。あまり複雑なフォームは持ってないのでどんだけ複雑のいけるのかってのは試してませんが、私が普段使ってるものはばっちりだったという結果だけ書いておきます。

完ぺきに分析できたのでカスタムモデルを使う理由がなくて日本語のは試せてません。(英語の請求書はサンプルがたくさんあるので試しましたが)

ちなみに Layout API とカスタムモデルをサクッと試す方法も前述でもリンクをはった同じブログに書いてます。

blog.beachside.dev

Prebuilt の ID を試す

Pre-built の ID service は、OCR の機能によってパスポートや免許証から情報を取得するものです。免許証は "U.S. Driver's Licenses (all 50 states and D.C.)" と限定されてますが、パスポートは日本のパスポートでもほぼ英語で書かれてるからいけるんじゃね?と思って試してみました。

試す手順はこんな感じです。

  • https://fott-preview.azurewebsites.net/ を開いてセットアップします(セットアップ方法は前回のブログのここに詳しく書いてます)。
  • Prebuilt analyze (図①)をクリックします。
  • アップロードする写真を選択します (図②) 。URL を指定することもできますし、ローカルのファイルを指定もできます。今回はパスポートの写真を撮っておいてローカルからアップロードしました。
  • Form Type は ID を指定します (図③) 。
  • Run analyze をクリックすると分析が開始されます (図④) 。

メニューがない場合は、右下に表示されてる API のバージョンが異なる可能性がありますのでチェックしましょう。

f:id:beachside:20210316041259p:plain

結果は下図のようになりました。パスポートの情報ということでぼかしをかけたので全く伝わらない図ですが、パスポートの番号や有効期限などすべての情報がラベルと紐づいて正確に取得できました。この1枚だけの結果を見る限りだと実用性あるレベルだなぁと感じました。

f:id:beachside:20210316035304p:plain

文字だけ取得するのはある程度精度のある OCR ならできると思いますが、パスポートは国によってレイアウトが全く違うこともあり単純な OCR だと実用はしんどいです。読み取った値がパスポート番号なのか、有効期限なのかとか判別するのは面倒だったのでこんなにサクッとできるとありがたいです。他国のがどれだけいい感じに取得できる希望が芽生えたので、機会があれば試してみたいですね。

終わりに

DX が進んでない組織が最初の PoC で、紙をなくすようなプロジェクトの導入として使えそうな空気を感じますね。