This article is your reusable base to start any SharePoint Online automation in C# with interactive MSAL authentication. It does one thing: sign you in and prove the connection by reading the site title via the SharePoint REST API. From here, you can branch into new utilities (query lists, upload files, migrate content, etc.).
Building a Minimal C# .NET Console App to Connect to SharePoint Online (Interactive Login + REST)
This article is your reusable base to start any SharePoint Online automation in C# with interactive MSAL authentication. It does one thing: sign you in and prove the connection by reading the site title via the SharePoint REST API. From here, you can branch into new utilities (query lists, upload files, migrate content, etc.).
Why start here? A clean “connect only” app lets you validate auth, consent, and policies before adding business logic.
What you’ll build
- A single-file .NET console app (
Program.cs) - Interactive sign-in using MSAL (system browser)
- A Bearer token to SharePoint with a scope that’s derived dynamically from your site URL (no tenant name hardcoded)
- A tiny probe call:
/_api/web?$select=Title→ prints your site title on success
Prerequisites
- .NET SDK 6.0+ (8.0 recommended):
dotnet --version - Ability to sign into your tenant and access the target SharePoint site
- An App registration in Entra ID (Azure AD) configured as a Public client (native)
1) App Registration (Entra ID) – once per project/tenant
- Entra ID → App registrations → New registration
- Name:
SpConnectOnly(or any) - Supported account types: Single tenant (or as needed)
- Register
- Authentication (left menu):
- Add a platform: Mobile and desktop applications
- Add redirect URI:
http://localhost - Enable public client flows (a.k.a. “Allow public client flows”)
- Save
- Note the Application (client) ID and Directory (tenant) ID
For this delegated interactive flow, consent happens at sign-in (unless your org requires admin pre-consent).
2) Create the console project
dotnet new console -n SpConnectOnly
cd SpConnectOnly
dotnet add package Microsoft.Identity.Client
3) Paste the full code (single file, tenant-agnostic scope)
Replace Program.cs with the code below and fill the three placeholders: tenantId, clientId, siteUrl.
Note: the scope is built from siteUrl → https://<your-tenant>.sharepoint.com/AllSites.FullControl, without hardcoding any client/tenant name.
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.Identity.Client;
namespace SpConnectOnly
{
internal class Program
{
// ========================= USER SETTINGS =========================
// Entra ID (Azure AD)
private static readonly string tenantId = "YOUR_TENANT_ID_GUID";
private static readonly string clientId = "YOUR_PUBLIC_CLIENT_APP_ID_GUID";
// SharePoint site to validate the connection against
private static readonly string siteUrl = "https://contoso.sharepoint.com/sites/YourSite";
// =================================================================
static async Task<int> Main()
{
Console.WriteLine("=== SharePoint Interactive Connect (Base App) ===");
try
{
// Build a tenant-agnostic scope from the siteUrl authority:
// e.g., "https://contoso.sharepoint.com/AllSites.FullControl"
var scopes = BuildScopesFromSite(siteUrl);
// 1) Acquire token interactively
var accessToken = await AcquireAccessTokenInteractiveAsync(tenantId, clientId, scopes);
if (string.IsNullOrWhiteSpace(accessToken))
{
Console.WriteLine("Failed to acquire token.");
return 1;
}
// 2) Call a tiny SharePoint endpoint to prove connectivity
var siteTitle = await GetSiteTitleAsync(siteUrl, accessToken);
if (string.IsNullOrWhiteSpace(siteTitle))
{
Console.WriteLine("Connected, but could not read site title (check permissions/siteUrl).");
return 2;
}
Console.WriteLine($"Connected to: {siteTitle}");
Console.WriteLine("Base connectivity successful. You’re ready for the next step.");
return 0;
}
catch (Exception ex)
{
Console.WriteLine("Error: " + ex.Message);
Console.WriteLine(ex);
return 3;
}
}
// ---------------------------- Auth ----------------------------
private static async Task<string> AcquireAccessTokenInteractiveAsync(
string tenantId,
string clientId,
string[] scopes)
{
var app = PublicClientApplicationBuilder.Create(clientId)
.WithAuthority(AzureCloudInstance.AzurePublic, tenantId)
.WithRedirectUri("http://localhost")
.Build();
try
{
var result = await app.AcquireTokenInteractive(scopes)
.WithPrompt(Prompt.SelectAccount)
.ExecuteAsync();
return result.AccessToken;
}
catch (MsalException mex)
{
Console.WriteLine("MSAL error: " + mex.Message);
return string.Empty;
}
}
// ---------------------- Connectivity Probe ---------------------
private static async Task<string?> GetSiteTitleAsync(string siteUrl, string accessToken)
{
using (var http = new HttpClient())
{
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
http.DefaultRequestHeaders.Accept.Clear();
http.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
var endpoint = $"{TrimEndSlash(siteUrl)}/_api/web?$select=Title";
using (var req = new HttpRequestMessage(HttpMethod.Get, endpoint))
{
req.Headers.Add("Accept", "application/json;odata=nometadata");
using (var res = await http.SendAsync(req))
{
var body = await res.Content.ReadAsStringAsync();
if (!res.IsSuccessStatusCode)
{
throw new Exception($"SharePoint error {(int)res.StatusCode}: {res.ReasonPhrase}\n{body}");
}
using (var doc = JsonDocument.Parse(body))
{
if (doc.RootElement.TryGetProperty("Title", out var titleProp))
return titleProp.GetString();
}
}
}
}
return null;
}
// --------------------------- Helpers --------------------------
private static string[] BuildScopesFromSite(string siteUrl)
{
var resource = new Uri(siteUrl).GetLeftPart(UriPartial.Authority); // https://contoso.sharepoint.com
return new[] { $"{resource}/AllSites.FullControl" };
}
private static string TrimEndSlash(string url)
{
if (string.IsNullOrWhiteSpace(url)) return url;
return url.EndsWith("/") ? url.TrimEnd('/') : url;
}
}
}
4) Run it
dotnet run
- A browser opens for sign-in.
- Consent to the requested permissions (you may need admin approval, depending on policy).
- On success, the console prints:
Connected to: <Your Site Title>
Base connectivity successful. You’re ready for the next step.
How the code works (short tour)
- MSAL Public Client
PublicClientApplicationBuilderwithWithAuthority(...tenantId)+WithRedirectUri("http://localhost")enables the system browser interactive flow. - Tenant-agnostic scope
Instead of hardcoding any tenant/client name, the code derives the resource from yoursiteUrland appends/AllSites.FullControl.
Example:https://contoso.sharepoint.com/AllSites.FullControl. - Connectivity Probe
CallsGET {siteUrl}/_api/web?$select=Titlewith a Bearer token and readsTitlefrom the JSON. - One file, small helpers
Everything lives inProgram.csto keep this a clean, reusable template.
Troubleshooting
| Symptom | Likely Cause | Fix |
|---|---|---|
MSAL error: AADSTS65001 / admin_consent_required | Tenant requires admin consent for SPO delegated scopes | Have an admin grant consent or pre-consent the scope |
| Browser opens but shows error | Redirect URI mismatch | Ensure app has http://localhost under Mobile and desktop applications and code uses the same |
401 Unauthorized from SharePoint REST | Wrong scope/authority, token missing, or user lacks site access | Keep the dynamic scope pattern, verify Authorization: Bearer, and confirm user access |
invalid_client | Wrong clientId or app not configured as public client | Check Application (client) ID, enable public client flows |
| Hangs waiting for login | Browser/profile quirks | Try InPrivate/Incognito or another default browser |
What to build next (same project)
- Query a list item by Title using
/_api/web/lists/getbytitle('X')/items?$filter=Title eq '...' - Create items (POST to
.../itemswith metadata payload) - Upload attachments (
.../AttachmentFiles/add(FileName='...')) - Copy files with versions (doc libraries)
- Switch to PnP Core or Graph if/when you prefer SDK ergonomics
Summary Tables
A) Step-by-Step
| # | Step | Command / Action |
|---|---|---|
| 1 | Create console app | dotnet new console -n SpConnectOnly |
| 2 | Add MSAL | dotnet add package Microsoft.Identity.Client |
| 3 | Configure | Set tenantId, clientId, siteUrl in Program.cs |
| 4 | Scope | Built dynamically from siteUrl → <tenant>/AllSites.FullControl |
| 5 | Run | dotnet run |
| 6 | Verify | Console prints Connected to: <Site Title> |
B) Technical Cheatsheet
| Topic | Value |
|---|---|
| Auth flow | MSAL interactive (public client, system browser) |
| Authority | AzurePublic + your tenantId |
| Redirect URI | http://localhost |
| Scope (resource) | <siteUrl authority>/AllSites.FullControl (derived at runtime) |
| Test endpoint | /_api/web?$select=Title |
| HTTP headers | Authorization: Bearer <token>, Accept: application/json;odata=nometadata |
| Output | Site title on success |
| Code style | Single-file, small helpers, plain REST |
