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"
}
⚠️
TenantNamemust be just the host — nohttps://.
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, yourTenantNameis empty or incorrect inconfig.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
| Capability | Supported |
|---|---|
| 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
DeviceCodefallback for headless CLI - Automate provisioning with PnP templates
- Extend this to copy lists, pages, libraries, permissions
