しばやん雑記

Azure Serverless とメイドさんが大好きなフリーランスのプログラマーのブログ

Easy Auth と Managed Identity を使ってグループ情報でのアクセス制限を行う

App Service の Easy Auth を使うと簡単に Azure AD でログインするアプリケーションが作れますが、Azure AD と連携しているならユーザーのロールを利用した認証が行いたくなるはずです。

Security Group をしっかりと使っている場合には、アプリケーション側での制御が必要になりますが、単純に実装しようとすると Object ID しか取れずに詰むので、Security Group 名ベースで制御してみます。

今回必要になる処理は以下の通りです。Azure AD がグループ名を Claims に入れてくれれば楽なのですが、いろいろ試しても Object ID しか入ってこないので諦めました。

  1. Microsoft Graph API を使ってグループ情報を引いておく
  2. Easy Auth の Claims にユーザーに紐づいているグループ情報を追加する
  3. Object ID を使ってグループ名とのマッピングを行う

ちなみに Azure Static Web Apps の認証にはロールという概念が入っていますが、Azure AD と連携したものではなく自分で定義する仕組みなのでイマイチな感じです。

Managed Identity を有効化して Graph API の権限を追加

Graph API を叩く方法としては Service Principal を作成するのが一般的のようですが、Service Principal はあまり好きではないので Managed Identity を使って API を叩きます。

完璧な記事が既にあるので、Managed Identity の有効化の後は以下を参考に Graph API を叩くための権限を追加してください。自分は Cloud Shell で行いました。

たったこれだけの作業で Managed Identity で Graph API の実行が可能になります。Service Principal に必要な認証情報を App Service に別途持たせる必要がないので最高ですね。

Easy Auth を有効化して Claims に Security Group を追加

次に Easy Auth を有効化しますが、Azure AD を選んで Express でアプリケーションを作成すれば良いです。

f:id:shiba-yan:20200525000723p:plain

アプリケーションの作成後は Azure AD のアプリケーション一覧から、作成したアプリケーション設定の中にある Token configuration を開きます。

昔は Manifest を直接弄る必要がありましたが、今は GUI でポチポチ設定できます。

f:id:shiba-yan:20200524215323p:plain

Add groups claim を選ぶと、どのタイプのグループを Claims に含めるか選べるので、今回は Security groups を選択して保存します。複数選べるので必要なものを選べばよいです。

f:id:shiba-yan:20200524215341p:plain

Token の種類によって値として何を含めるかをカスタマイズできますが、Azure AD をソースとして使っている場合は Object ID しか実質入ってこないようになっているみたいです。

Emit groups as role claims にチェックを入れると、Claim 名が groups から roles になります。

f:id:shiba-yan:20200524215349p:plain

roles に含めるようにすると ASP.NET Core などから使いやすくなるのかもしれませんが、結局は Object ID しか入ってこないのでロール名へのマッピングは必要になるでしょう。

これで Easy Auth での認証時に Claims に紐づいているグループの Object ID が入るようになりました。

Azure Functions で Claims と Graph API を利用

後はアプリケーションで Claims に含まれている Object ID と Graph API から取得した Security Group 一覧を使ってマッピングすれば良いです。簡単にするために Azure Functions で書いています。

実際にアプリケーションで使いたいので Startup と DI を使っていろいろ分離しておきました。

public class Startup : FunctionsStartup
{
    public override void Configure(IFunctionsHostBuilder builder)
    {
        builder.Services.AddSingleton<IGraphServiceClient>(provider =>
        {
            var graphServiceClient = new GraphServiceClient(new DelegateAuthenticationProvider(async requestMessage =>
            {
                var accessToken = await new AzureServiceTokenProvider().GetAccessTokenAsync("https://graph.microsoft.com/");

                requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
            }));

            return graphServiceClient;
        });

        builder.Services.AddSingleton<AzureAdGroupsAccessor>();
    }
}

GraphServiceClient はシングルトンにしたいので、DI を使って作成するようにしています。Managed Identity を使って Access Token を取る部分も含めています。

実際に Graph API を使って Security Group 一覧を取る部分は Accessor として別クラスにしています。

public class AzureAdGroupsAccessor
{
    public AzureAdGroupsAccessor(IGraphServiceClient graphServiceClient)
    {
        _graphServiceClient = graphServiceClient;
    }

    private readonly IGraphServiceClient _graphServiceClient;

    public async Task<IList<Group>> GetSecurityGroupAsync()
    {
        var groups = await _graphServiceClient.Groups
                                              .Request()
                                              .Filter("securityEnabled eq true")
                                              .GetAsync();

        return groups;
    }
}

起動時に 1 回だけ取得して、更新が必要な場合は Azure Functions 再起動で十分だと思いますが、簡単にするためのその辺りは省きました。必要なら AsyncLazy<T> などで初期化してあげれば良いです。

ここまで準備が出来れば、後は Function 本体でメソッド呼び出せば Security Group の一覧が取れます。今回は roles に追加するオプションを有効にしたので、Claims から取る時の名前も roles になっています。

public class Function1 : HttpFunctionBase
{
    public Function1(IHttpContextAccessor httpContextAccessor, AzureAdGroupsAccessor azureAdGroupsAccessor)
        : base(httpContextAccessor)
    {
        _azureAdGroupsAccessor = azureAdGroupsAccessor;
    }

    private readonly AzureAdGroupsAccessor _azureAdGroupsAccessor;

    [FunctionName("Function1")]
    public async Task<IActionResult> Run(
        [HttpTrigger(AuthorizationLevel.Anonymous, "get")] HttpRequest req,
        ILogger log)
    {
        var groups = await _azureAdGroupsAccessor.GetSecurityGroupAsync();

        var assignedGroups = User.FindAll("roles").Select(x => x.Value).ToArray();

        return Ok(new { assignedGroups, groups });
    }
}

最近 Azure Functions で HTTP API を書くときは HttpFunctionBase を使うようにしているので、ClaimsPrincipal へのアクセスとレスポンスを返すのが楽です。

作成した Function App をデプロイすれば、後は確認のためにユーザーに Security Group を割り当てます。

Azure AD の UI がちょいちょい良くなっているので、割り当て自体はサクッと行えるはずです。

f:id:shiba-yan:20200525013347p:plain

最後に作成した Function へブラウザからアクセスすると、Easy Auth での認証が走った後に紐づいている Object ID と Security Group の一覧が返ってくるはずです。

ブラウザだと JSON が見にくかったので、Postman を使って同じ API を叩いたのが以下の例です。

f:id:shiba-yan:20200525013212p:plain

これで API 毎に必要なロールを定義して、アクセス制限を実装できるようになりました。

ASP.NET Core だと ClaimsPrincipal のオーバーライドや、ポリシーの定義が可能なので Authorize 属性で許可ロールを指定できますが、Azure Functions だとチェックするメソッドを用意するしかないかなと思います。Functions Filter は先行きがかなり怪しいです。