A complete guide on using Microsoft.Identity.Client (MSAL) with the PnP Core SDK, manually injecting tokens into SharePoint Online connections — all using clean dependency injection and external configurati


🔐 Authenticating to SharePoint Online Using MSAL and PnP.Core with Custom Token Injection

A complete guide on using Microsoft.Identity.Client (MSAL) with the PnP Core SDK, manually injecting tokens into SharePoint Online connections — all using clean dependency injection and external configuration.


🧩 Project Structure

/YourProject
│
├── config.json                          ← Configuration file
├── AppConfig.cs                         ← Strongly typed config model
├── AuthService.cs                       ← MSAL token provider
├── DelegatePnPAuthenticationProvider.cs ← Token-based IAuthenticationProvider
└── Program.cs                           ← Console app entry point


1️⃣ Configuration File — config.json

{
  "ClientId": "your-client-id",
  "TenantId": "your-tenant-id-or-domain",
  "TenantName": "yourtenant.sharepoint.com",
  "SiteUrl": "https://yourtenant.sharepoint.com/sites/YourSite"
}

⚠️ TenantName must be just the host — no https://.


2️⃣ AppConfig.cs

public class AppConfig
{
    public string ClientId { get; set; }
    public string TenantId { get; set; }
    public string TenantName { get; set; }
    public string SiteUrl { get; set; }
}


3️⃣ AuthService.cs — Interactive MSAL Flow

using Microsoft.Identity.Client;
using System.Threading.Tasks;

namespace MyApp.Auth
{
    public class AuthService
    {
        private readonly AppConfig config;
        private readonly string[] scopes;
        private readonly string authority;

        public AuthService(AppConfig config)
        {
            this.config = config;
            authority = $"https://login.microsoftonline.com/{config.TenantId}";
            scopes = new[] { $"https://{config.TenantName}/AllSites.FullControl" };
        }

        public async Task<string> GetAccessTokenAsync()
        {
            var app = PublicClientApplicationBuilder
                .Create(config.ClientId)
                .WithAuthority(authority)
                .WithDefaultRedirectUri()
                .Build();

            var result = await app.AcquireTokenInteractive(scopes).ExecuteAsync();
            return result.AccessToken;
        }
    }
}

✅ This uses MSAL’s public client flow with WithDefaultRedirectUri() — no need to pre-register redirect URIs in Azure.


4️⃣ DelegatePnPAuthenticationProvider.cs

using System;
using System.Net.Http;
using System.Threading.Tasks;
using PnP.Core.Services;

public class DelegatePnPAuthenticationProvider : IAuthenticationProvider
{
    private readonly string _accessToken;

    public DelegatePnPAuthenticationProvider(string accessToken)
    {
        _accessToken = accessToken ?? throw new ArgumentNullException(nameof(accessToken));
    }

    public Task AuthenticateRequestAsync(Uri resource, HttpRequestMessage request)
    {
        request.Headers.Authorization =
            new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", _accessToken);
        return Task.CompletedTask;
    }

    public Task<string> GetAccessTokenAsync(Uri resource)
    {
        return Task.FromResult(_accessToken);
    }

    public Task<string> GetAccessTokenAsync(Uri resource, string[] scopes)
    {
        return Task.FromResult(_accessToken);
    }
}

✅ This class satisfies PnP.Core.Services.IAuthenticationProvider using a static token, as returned from MSAL.


5️⃣ Program.cs — The Application Entry Point

using System;
using System.IO;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using PnP.Core.Services;
using MyApp.Auth;

class Program
{
    static async Task Main(string[] args)
    {
        // Load config
        var configJson = File.ReadAllText("config.json");
        var config = JsonSerializer.Deserialize<AppConfig>(
            configJson,
            new JsonSerializerOptions { PropertyNameCaseInsensitive = true }
        );

        var services = new ServiceCollection();
        services.AddPnPCore();

        // Get token via MSAL
        var authService = new AuthService(config);
        string accessToken = await authService.GetAccessTokenAsync();

        // Inject token into PnP authentication provider
        services.AddSingleton<PnP.Core.Services.IAuthenticationProvider>(sp =>
        {
            return new DelegatePnPAuthenticationProvider(accessToken);
        });

        var serviceProvider = services.BuildServiceProvider();
        var authProvider = serviceProvider.GetRequiredService<IAuthenticationProvider>();
        var contextFactory = serviceProvider.GetRequiredService<IPnPContextFactory>();

        // Use the PnP context
        using var context = await contextFactory.CreateAsync(new Uri(config.SiteUrl), authProvider);
        await context.Web.LoadAsync(p => p.Title);

        Console.WriteLine($"✅ Connected to: {context.Web.Title}");
    }
}


🧪 Verifying Scope Construction

To avoid malformed scopes like https:///AllSites.FullControl, insert a debug log:

Console.WriteLine($"Using scope: https://{config.TenantName}/AllSites.FullControl");

If the output shows https:///AllSites.FullControl, your TenantName is empty or incorrect in config.json.


📦 Required NuGet Packages

Install via CLI:

dotnet add package Microsoft.Identity.Client
dotnet add package PnP.Core
dotnet add package PnP.Core.Auth


✅ End Result

CapabilitySupported
Externalized config via JSON
MSAL authentication (interactive)
No redirect URI registration
SharePoint access via PnP Core
Token manually injected
No PropertyPane or GUI needed

🚀 What You Can Do Next

  • Add MSAL token cache to reduce re-logins
  • Support DeviceCode fallback for headless CLI
  • Automate provisioning with PnP templates
  • Extend this to copy lists, pages, libraries, permissions

Edvaldo Guimrães Filho Avatar

Published by