💻

PnP Core SDK を学ぶ - 認証編

に公開

Learning PnP Core SDK - Authentication Section

PnP Core SDK は、PnP PowerShell の 内部で利用されるモジュールであり、コミュニティ主導で開発されている SharePoint を操作可能な SDK です。
最新の .NET 開発に対応する形で利用することを前提とした SDK のため、Dependency Injection (DI) を前提とした構成になっており、.NET における DI コンテナの利用方法をある程度知らないと利用が困難なものですが、これを活用できると組織内における SharePoint 活用の付加価値向上に寄与できます。

この記事では、PnP Core SDK を利用する際の、認証機能の部分について解説します。下記で解説されている公式のリファレンスを参照しつつ、公式ページには載っていない実装に関する部分についても触れていきます。

https://pnp.github.io/pnpcore/using-the-sdk/configuring authentication.html

https://pnp.github.io/pnpcore/using-the-sdk/custom-authentication-provider.html

無人バッチ実行形式

バッチ形式で無人実行したい場合の認証は、下記の記事を参考にすれば良いです。

https://zenn.dev/microsoft/articles/pnp-core-sdk-get-files

ですが、無人バッチでの実行形式はアプリケーションの許可という強権限を利用しなければならない都合上、テナント管理者以外の利用を許可されていない組織が一般的かと思いますし、そもそもテナント管理者においては、PnP を利用する際は、PnP PowerShell を利用することが通例かと思います。PnP 公式に記載されているアクセス内容は権限過多なので、基本的に一般ユーザーが使いたいというときは、Sites.Selected 固定で、特定サイトへの fullcontrol の権限 (Permission) をつける以外に利用シーンは限られると思います。

上記を踏まえると、PnP PowerShell では操作が困難なケースや、既存の .NET アプリケーションへのモジュール組み込みでユーザー権限を伴わない、特定のサイト群に対して実行するものにおいては採用されるかも、ぐらいのレベルかと思います。(SharePoint リストアイテムの更新処理など)

Web サイトでのログイン形式

社内の一般ユーザーなどが PnP PowerShell を実行することは基本難しいと思いますので、ブラウザ画面などを通じて PnP が提供する操作を提供できるように、PnP Core SDK を採用することは有り得るかと思います。ちなみに、PnP Core SDK は SharePoint API に加えて Microsoft Graph も実行できるようになっているため、組織内の一般ユーザーへのために PnP Core SDK を採用できる場面は多いでしょう。

下記で紹介する例は、ASP.NET Core MVC での実装例です。

Microsoft Entra ID アプリケーション登録

Entra ID のサービスプリンシパルは、以下の内容で作成しておきます。
下記に記載していない細かい部分の設定値は、本記事とは直接的に関係ないので割愛します。

  • 認証
    • プラットフォーム構成
      • モバイル アプリケーションとデスクトップ アプリケーション
        • リダイレクト URI
          • https://login.microsoftonline.com/common/oauth2/nativeclient
          • http://localhost
      • 暗黙的な許可およびハイブリッド フロー
        • ID トークン (暗黙的およびハイブリッド フローに使用): ON
  • API のアクセス許可
    • Microsoft Graph / 委任されたアクセス許可 / Sites.FullControll.All
    • SharePoint / 委任されたアクセス許可 / AllSites.FullControll

PnP Core SDK 標準の認証方式

公式のページは下記を参照です。

https://pnp.github.io/pnpcore/using-the-sdk/configuring authentication.html

パッケージとしては、PnP.Core. と PnP.Core.Auth を追加します。

dotnet add package PnP.Core --version 1.15.0
dotnet add package PnP.Core.Auth --version 1.15.0

下記のコードは、dotnet new mvc -n Sample を実行した後のデフォルト内容に PnP Core SDK の認証機能を追加したものです。
appsettings.json および appsettings.Development.json を利用していますので、csproj での読み込み設定を忘れずに追加してください。

dotnet run を実行すると、コンソール上に http://localhost:{PORT} の URL が表示されるので、アクセスすると PnP Core 用の Interactive 形式の Microsoft アカウント認証が別ウインドウで起動することが確認できます。

appsettings.json
{
  "AzureAd": {
    "ClientId": "YOUR_APPLICATION_CLIENT_ID",
    "TenantId": "YOUR_APPLICATION_TENANT_ID",
    "RedirectUri": "YOUR_SITE_REDIRECT_URL",
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  }
}
appsettings.Development.json
{
  "AzureAd": {
    "ClientId": "YOUR_APPLICATION_CLIENT_ID",
    "TenantId": "YOUR_APPLICATION_TENANT_ID",
    "RedirectUri": "http://localhost",
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  }
}
Program.cs
using PnP.Core.Auth.Services.Builder.Configuration;
using PnP.Core.Services.Builder.Configuration;
using Sample.Services;

var builder = WebApplication.CreateBuilder(args);
var builderConfig = builder.Configuration;

// Add services to the container.
builder.Services.AddControllersWithViews();

builder.Services.AddPnPCore(options =>
{
    options.DisableTelemetry = true;
    options.PnPContext = new PnPCoreContextOptions()
    {
        GraphAlwaysUseBeta = false,
        GraphCanUseBeta = false,
        GraphFirst = true,
    };

    options.HttpRequests = new PnPCoreHttpRequestsOptions()
    {
        UserAgent = "mappie-kochi.jp",
        SharePointRest = new PnPCoreHttpRequestsSharePointRestOptions()
        {
            UseRetryAfterHeader = true,
        },
    };
});

builder.Services.AddPnPCoreAuthentication(options =>
{
    options.Credentials.Configurations.Add("interactive", new PnPCoreAuthenticationCredentialConfigurationOptions
    {
        ClientId = builderConfig["AzureAd:ClientId"],
        TenantId = builderConfig["AzureAd:TenantId"],
        Interactive = new PnPCoreAuthenticationInteractiveOptions
        {
            RedirectUri = new Uri(builderConfig["AzureAd:RedirectUri"] ?? "http://localhost")
        }
    });
    options.Credentials.DefaultConfiguration = "interactive";
});

// Configure Dependency Injection
builder.Services.AddScoped<PnPCoreService>();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Home/Error");
    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

app.Run();

Services/PnPCoreService.cs
using PnP.Core.Services;

namespace Sample.Services;

public class PnPCoreService
{
    private readonly IPnPContextFactory _contextFactory;
    private readonly ILogger<PnPCoreService> _logger;

    public PnPCoreService(IPnPContextFactory contextFactory, ILogger<PnPCoreService> logger)
    {
        _contextFactory = contextFactory;
        _logger = logger;
    }

    public async Task AuthenticateAsync(string siteUrl)
    {
        await this.CreateContextAsync(siteUrl);
    }

    private async Task<IPnPContext> CreateContextAsync(string siteUrl)
    {
        // Generate a PnPContext based on user input values
        var context = await _contextFactory.CreateAsync(new Uri(siteUrl));
        _logger.LogInformation($"PnP Context created for site: {siteUrl}");
        return context;
    }
}

Controllers/HomeController.cs
using System.Diagnostics;
using Microsoft.AspNetCore.Mvc;
using Sample.Models;
using Sample.Services;

namespace Sample.Controllers;

public class HomeController : Controller
{
    private readonly ILogger<HomeController> _logger;
    private readonly PnPCoreService _pnpCoreService;

    public HomeController(PnPCoreService pnpCoreService, ILogger<HomeController> logger)
    {
        _pnpCoreService = pnpCoreService;
        _logger = logger;
    }

    public async Task<IActionResult> Index()
    {
        await Authenticate();
        return View();
    }

    public IActionResult Privacy()
    {
        return View();
    }

    private async Task Authenticate()
    {
        string siteUrl = "{{URL_YOUR_SHAREPOINT_ONLINE_SITE}}";
        await _pnpCoreService.AuthenticateAsync(siteUrl);
    }

    [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
    public IActionResult Error()
    {
        return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
    }
}

参考情報

https://pnp.github.io/pnpcore/index.html

https://zenn.dev/microsoft/articles/pnp-core-sdk-get-files

Discussion