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

  1. Entra ID → App registrationsNew registration
  2. Name: SpConnectOnly (or any)
  3. Supported account types: Single tenant (or as needed)
  4. Register
  5. 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
  6. 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 siteUrlhttps://<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
    PublicClientApplicationBuilder with WithAuthority(...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 your siteUrl and appends /AllSites.FullControl.
    Example: https://contoso.sharepoint.com/AllSites.FullControl.
  • Connectivity Probe
    Calls GET {siteUrl}/_api/web?$select=Title with a Bearer token and reads Title from the JSON.
  • One file, small helpers
    Everything lives in Program.cs to keep this a clean, reusable template.

Troubleshooting

SymptomLikely CauseFix
MSAL error: AADSTS65001 / admin_consent_requiredTenant requires admin consent for SPO delegated scopesHave an admin grant consent or pre-consent the scope
Browser opens but shows errorRedirect URI mismatchEnsure app has http://localhost under Mobile and desktop applications and code uses the same
401 Unauthorized from SharePoint RESTWrong scope/authority, token missing, or user lacks site accessKeep the dynamic scope pattern, verify Authorization: Bearer, and confirm user access
invalid_clientWrong clientId or app not configured as public clientCheck Application (client) ID, enable public client flows
Hangs waiting for loginBrowser/profile quirksTry InPrivate/Incognito or another default browser

What to build next (same project)

  1. Query a list item by Title using /_api/web/lists/getbytitle('X')/items?$filter=Title eq '...'
  2. Create items (POST to .../items with metadata payload)
  3. Upload attachments (.../AttachmentFiles/add(FileName='...'))
  4. Copy files with versions (doc libraries)
  5. Switch to PnP Core or Graph if/when you prefer SDK ergonomics

Summary Tables

A) Step-by-Step

#StepCommand / Action
1Create console appdotnet new console -n SpConnectOnly
2Add MSALdotnet add package Microsoft.Identity.Client
3ConfigureSet tenantId, clientId, siteUrl in Program.cs
4ScopeBuilt dynamically from siteUrl<tenant>/AllSites.FullControl
5Rundotnet run
6VerifyConsole prints Connected to: <Site Title>

B) Technical Cheatsheet

TopicValue
Auth flowMSAL interactive (public client, system browser)
AuthorityAzurePublic + your tenantId
Redirect URIhttp://localhost
Scope (resource)<siteUrl authority>/AllSites.FullControl (derived at runtime)
Test endpoint/_api/web?$select=Title
HTTP headersAuthorization: Bearer <token>, Accept: application/json;odata=nometadata
OutputSite title on success
Code styleSingle-file, small helpers, plain REST

Edvaldo Guimrães Filho Avatar

Published by