<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Authentication and Authorization on Jeppe Spanggaard - Software Developer | .NET, Azure &amp; Microsoft 365</title><link>https://jeppe-spanggaard.dk/tags/authentication/</link><description>Recent content in Authentication and Authorization on Jeppe Spanggaard - Software Developer | .NET, Azure &amp; Microsoft 365</description><generator>Hugo</generator><language>en-US</language><lastBuildDate>Sat, 25 Jul 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://jeppe-spanggaard.dk/tags/authentication/index.xml" rel="self" type="application/rss+xml"/><item><title>F12 Shows Your API Key: Hiding Third-Party Tokens Behind an Azure Function</title><link>https://jeppe-spanggaard.dk/blogs/spfx-azure-function-api-proxy-hide-token/</link><pubDate>Sat, 25 Jul 2026 00:00:00 +0000</pubDate><guid>https://jeppe-spanggaard.dk/blogs/spfx-azure-function-api-proxy-hide-token/</guid><description>Learn how to keep third-party API keys out of the browser by proxying SPFx web part calls through an Azure Function that swaps in the secret server-side.</description><content:encoded><![CDATA[<p>I was building an SPFx web part that shows data from a third-party system - one of those practice-management/CRM-style products with a REST API. The API authenticates the simple way: one tenant-wide key in an <code>X-Api-Key</code> header. Our key, for all of our data.</p>
<p>First instinct: call the API straight from the web part. It works in twenty minutes. Then you press F12, open the network tab, click any request&hellip; and there it is. The company&rsquo;s master API key, in plain text, readable by every single user who ever loads that intranet page.</p>
<p>And it&rsquo;s not &ldquo;they can see the data the web part shows anyway&rdquo; - the web part shows a filtered slice. The <em>key</em> unlocks the whole API: every customer, every record, every write operation the key is licensed for. Anyone who copies it out of the inspector can call the API from Postman on their couch.</p>
<h2 id="why-the-browser-cant-keep-a-secret">Why the Browser Can&rsquo;t Keep a Secret</h2>
<p>It&rsquo;s worth being blunt about this, because the temptation to &ldquo;just obfuscate it a bit&rdquo; is real:</p>
<ul>
<li>Everything the browser <em>sends</em> is in the network tab.</li>
<li>Everything the bundle <em>contains</em> is in the sources tab (source maps or not, strings are strings).</li>
<li>Everything the app <em>holds in memory</em> is one breakpoint away.</li>
</ul>
<p>There is no hiding place in the client. And &ldquo;it&rsquo;s only our internal SharePoint&rdquo; doesn&rsquo;t help - internal users are exactly the people who shouldn&rsquo;t be walking around with the master key to a system they&rsquo;re only supposed to see a corner of.</p>
<h2 id="the-proxy-function">The Proxy Function</h2>
<p>The fix is a small reverse proxy. One catch-all Azure Function fronts the entire third-party API:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">private</span> <span style="color:#66d9ef">static</span> <span style="color:#66d9ef">readonly</span> HashSet&lt;<span style="color:#66d9ef">string</span>&gt; _hopByHop = <span style="color:#66d9ef">new</span>(StringComparer.OrdinalIgnoreCase)
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;Connection&#34;</span>, <span style="color:#e6db74">&#34;Keep-Alive&#34;</span>, <span style="color:#e6db74">&#34;Proxy-Authenticate&#34;</span>, <span style="color:#e6db74">&#34;Proxy-Authorization&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;TE&#34;</span>, <span style="color:#e6db74">&#34;Trailer&#34;</span>, <span style="color:#e6db74">&#34;Transfer-Encoding&#34;</span>, <span style="color:#e6db74">&#34;Upgrade&#34;</span>
</span></span><span style="display:flex;"><span>};
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">[Function(&#34;ApiProxy&#34;)]</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">async</span> Task&lt;IActionResult&gt; Run(
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">    [HttpTrigger(AuthorizationLevel.Anonymous, &#34;get&#34;, &#34;post&#34;, &#34;put&#34;, &#34;delete&#34;,
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">                 Route = &#34;api/{*path}&#34;)]</span> HttpRequest request,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">string?</span> path,
</span></span><span style="display:flex;"><span>    CancellationToken ct)
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> targetUri = _settings.BaseUrl.TrimEnd(<span style="color:#e6db74">&#39;/&#39;</span>) + <span style="color:#e6db74">&#34;/&#34;</span> + (path ?? <span style="color:#e6db74">&#34;&#34;</span>)
</span></span><span style="display:flex;"><span>                  + (request.QueryString.Value ?? <span style="color:#e6db74">&#34;&#34;</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">using</span> var upstream = <span style="color:#66d9ef">new</span> HttpRequestMessage(<span style="color:#66d9ef">new</span> HttpMethod(request.Method), targetUri);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> (request.Method <span style="color:#66d9ef">is</span> <span style="color:#e6db74">&#34;POST&#34;</span> or <span style="color:#e6db74">&#34;PUT&#34;</span> || request.ContentLength <span style="color:#66d9ef">is</span> &gt; <span style="color:#ae81ff">0</span>)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        upstream.Content = <span style="color:#66d9ef">new</span> StreamContent(request.Body);
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> (request.ContentType <span style="color:#66d9ef">is</span> not <span style="color:#66d9ef">null</span>)
</span></span><span style="display:flex;"><span>            upstream.Content.Headers.TryAddWithoutValidation(<span style="color:#e6db74">&#34;Content-Type&#34;</span>, request.ContentType);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">foreach</span> (<span style="color:#66d9ef">var</span> (key, values) <span style="color:#66d9ef">in</span> request.Headers)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// Never forward the client&#39;s Host, any API key they try to sneak in,</span>
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// or hop-by-hop headers that belong to *this* connection only.</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> (key.Equals(<span style="color:#e6db74">&#34;Host&#34;</span>, StringComparison.OrdinalIgnoreCase) ||
</span></span><span style="display:flex;"><span>            key.Equals(<span style="color:#e6db74">&#34;X-Api-Key&#34;</span>, StringComparison.OrdinalIgnoreCase) ||
</span></span><span style="display:flex;"><span>            _hopByHop.Contains(key))
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">continue</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> (!upstream.Headers.TryAddWithoutValidation(key, (IEnumerable&lt;<span style="color:#66d9ef">string?</span>&gt;)values!))
</span></span><span style="display:flex;"><span>            upstream.Content?.Headers.TryAddWithoutValidation(key, (IEnumerable&lt;<span style="color:#66d9ef">string?</span>&gt;)values!);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// The swap. The secret exists only here, on the outbound leg.</span>
</span></span><span style="display:flex;"><span>    upstream.Headers.Add(<span style="color:#e6db74">&#34;X-Api-Key&#34;</span>, _settings.ApiKey);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> client = _httpClientFactory.CreateClient(<span style="color:#e6db74">&#34;upstream-api&#34;</span>);
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">using</span> var response = <span style="color:#66d9ef">await</span> client.SendAsync(upstream, HttpCompletionOption.ResponseHeadersRead, ct);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    _logger.LogInformation(<span style="color:#e6db74">&#34;Proxy: {Method} {Path} → {StatusCode}&#34;</span>, request.Method, path, (<span style="color:#66d9ef">int</span>)response.StatusCode);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> httpResponse = request.HttpContext.Response;
</span></span><span style="display:flex;"><span>    httpResponse.StatusCode = (<span style="color:#66d9ef">int</span>)response.StatusCode;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">foreach</span> (<span style="color:#66d9ef">var</span> (key, values) <span style="color:#66d9ef">in</span> response.Headers.Concat(response.Content.Headers))
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> (_hopByHop.Contains(key)) <span style="color:#66d9ef">continue</span>;
</span></span><span style="display:flex;"><span>        httpResponse.Headers.Append(key, values.ToArray());
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">await</span> response.Content.CopyToAsync(httpResponse.Body, ct);
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">new</span> EmptyResult();
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p><strong>What&rsquo;s happening here?</strong></p>
<ol>
<li><code>Route = &quot;api/{*path}&quot;</code> is a catch-all. <code>GET /api/customers/123/tasks</code> becomes <code>GET https://the-api.example/customers/123/tasks</code>, query string included. One function fronts the whole API surface - when the vendor adds endpoints, the proxy needs zero changes.</li>
<li>The header loop deliberately <strong>strips any inbound <code>X-Api-Key</code></strong>. A caller can&rsquo;t inject their own key or override yours - whatever they send in that header dies at the proxy.</li>
<li>The hop-by-hop set (<code>Connection</code>, <code>Transfer-Encoding</code>, &hellip;) is the detail naive proxies get wrong. Those headers describe <em>one</em> connection, not the request; forwarding them causes wonderfully confusing breakage.</li>
<li>The real key is added to the <strong>outbound request only</strong>. Response headers get copied back to the browser, but request headers are never echoed - so the key physically cannot appear in the inspector. It&rsquo;s not hidden; it&rsquo;s <em>absent</em>.</li>
<li><code>ResponseHeadersRead</code> + <code>CopyToAsync</code> streams the upstream response straight through without buffering it in the function&rsquo;s memory, and upstream status codes pass through untouched so the web part can react to a 404 or a 429 honestly.</li>
</ol>
<h2 id="where-the-key-lives">Where the Key Lives</h2>
<p>Server-side config, strongly typed, validated at boot:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span>services.AddOptions&lt;ApiSettings&gt;()
</span></span><span style="display:flex;"><span>    .Bind(configuration.GetSection(<span style="color:#e6db74">&#34;UpstreamApi&#34;</span>))
</span></span><span style="display:flex;"><span>    .Validate(s =&gt; IsConfigured(s.BaseUrl), <span style="color:#e6db74">&#34;UpstreamApi:BaseUrl must be configured.&#34;</span>)
</span></span><span style="display:flex;"><span>    .Validate(s =&gt; IsConfigured(s.ApiKey), <span style="color:#e6db74">&#34;UpstreamApi:ApiKey must be configured.&#34;</span>)
</span></span><span style="display:flex;"><span>    .ValidateOnStart();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">private</span> <span style="color:#66d9ef">static</span> <span style="color:#66d9ef">bool</span> IsConfigured(<span style="color:#66d9ef">string</span> <span style="color:#66d9ef">value</span>) =&gt;
</span></span><span style="display:flex;"><span>    !<span style="color:#66d9ef">string</span>.IsNullOrWhiteSpace(<span style="color:#66d9ef">value</span>) &amp;&amp; !<span style="color:#66d9ef">value</span>.StartsWith(<span style="color:#e6db74">&#39;&lt;&#39;</span>);
</span></span></code></pre></div><p>The value itself comes from Function App settings - ideally as a Key Vault reference - never from a committed file. <code>ValidateOnStart()</code> plus the <code>&lt;placeholder&gt;</code> guard means a misconfigured secret fails the deployment at boot, not at the first user&rsquo;s click three days later.</p>
<h2 id="hiding--authorizing">Hiding ≠ Authorizing</h2>
<p>Here&rsquo;s the part that&rsquo;s easy to skip and shouldn&rsquo;t be: as shown so far, the proxy hides the key from the browser, but <strong>anyone who discovers the function URL can call it</strong> - and burn your API quota with your key. We&rsquo;ve moved the secret, not secured the door.</p>
<p>Lock the function to Entra ID. Create an app registration for the function API, expose a scope (the convention is <code>access_as_user</code>), and have the function validate incoming JWTs - signature, issuer, and audience, not just parsing the claims out. Parsing is reading the name tag; validating is checking the ID.</p>
<p>On the SPFx side, this is pleasantly little work. Declare the permission in <code>package-solution.json</code>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-json" data-lang="json"><span style="display:flex;"><span><span style="color:#e6db74">&#34;webApiPermissionRequests&#34;</span><span style="color:#960050;background-color:#1e0010">:</span> [
</span></span><span style="display:flex;"><span>  {
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;resource&#34;</span>: <span style="color:#e6db74">&#34;my-function-api&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;scope&#34;</span>: <span style="color:#e6db74">&#34;access_as_user&#34;</span>
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>]
</span></span></code></pre></div><p>And call the function with <code>AadHttpClient</code> - SPFx acquires and attaches the user&rsquo;s token for you:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-typescript" data-lang="typescript"><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">client</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">await</span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">aadHttpClientFactory</span>
</span></span><span style="display:flex;"><span>  .<span style="color:#a6e22e">getClient</span>(<span style="color:#e6db74">&#34;api://&lt;function-app-registration-client-id&gt;&#34;</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">response</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">client</span>.<span style="color:#66d9ef">get</span>(
</span></span><span style="display:flex;"><span>  <span style="color:#e6db74">`https://my-function.azurewebsites.net/api/customers/</span><span style="color:#e6db74">${</span><span style="color:#a6e22e">customerCode</span><span style="color:#e6db74">}</span><span style="color:#e6db74">/tasks`</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">AadHttpClient</span>.<span style="color:#a6e22e">configurations</span>.<span style="color:#a6e22e">v1</span>
</span></span><span style="display:flex;"><span>);
</span></span></code></pre></div><p>Now open the inspector again. There <em>is</em> a token in the request - but it&rsquo;s the <strong>user&rsquo;s own</strong> token: short-lived, scoped to your function only, and useless against the third-party API. That&rsquo;s the whole difference between a secret and an identity. A stolen API key is everyone&rsquo;s master key forever; a stolen user token is one person&rsquo;s function access for an hour.</p>
<h2 id="gotchas">Gotchas</h2>
<ul>
<li><strong>Watch what you log.</strong> Log method, path, and status - never headers. Otherwise the key you carefully kept out of the browser ends up in Application Insights, readable by everyone with portal access.</li>
<li><strong>You now front someone else&rsquo;s quota.</strong> The vendor&rsquo;s rate limits hit <em>your</em> key for <em>all</em> users combined. If the web part is chatty, add caching or throttling in the proxy before the vendor does it for you.</li>
<li><strong>CORS is on you.</strong> The browser is calling your function from a SharePoint origin - configure CORS on the Function App for your tenant&rsquo;s SharePoint domain, not <code>*</code>.</li>
<li><strong>Consider narrowing the surface.</strong> A catch-all proxy forwards <em>everything</em>, including endpoints the web part never needed. If the API has destructive operations, whitelist the methods and paths you actually use.</li>
</ul>
<h2 id="wrapping-up">Wrapping Up</h2>
<p>A shared API key belongs on a server. Full stop. The moment it ships to a browser, every user has it, and you can&rsquo;t take it back - most vendors will happily rotate the key for you, but you&rsquo;ll be doing that dance on their schedule, not yours.</p>
<p>The proxy costs about fifty lines: catch-all route, header hygiene, one server-side header swap, and Entra ID on the front door. That turns &ldquo;every intranet user carries the master key&rdquo; into &ldquo;every request is a named user, calling a locked endpoint, seeing exactly what the web part shows them&rdquo;. 🔒</p>
]]></content:encoded></item><item><title>There's No Popup on a Phone: Nested App Auth for Outlook Add-ins</title><link>https://jeppe-spanggaard.dk/blogs/outlook-addin-nested-app-auth-mobile/</link><pubDate>Thu, 28 May 2026 00:00:00 +0000</pubDate><guid>https://jeppe-spanggaard.dk/blogs/outlook-addin-nested-app-auth-mobile/</guid><description>Learn how to get MSAL tokens inside Outlook on mobile with Nested App Auth (NAA), from the brk-multihub redirect URI to acquireTokenSilent with a loginHint.</description><content:encoded><![CDATA[<p>This is the third post in what accidentally became a trilogy about getting my Outlook add-in to work on mobile. <a href="https://jeppe-spanggaard.dk/blogs/outlook-addin-manifest-requirement-sets-mobile/">Post one</a> made the add-in <em>visible</em> on the phone. <a href="https://jeppe-spanggaard.dk/blogs/outlook-addin-graph-fallback-mobile/">Post two</a> made it <em>functional</em> by falling back to a Microsoft Graph call when <code>getAsFileAsync</code> wasn&rsquo;t there.</p>
<p>And in that second post I wrote one very casual line: <em>&ldquo;The token comes from MSAL.&rdquo;</em></p>
<p>Yeah. About that.</p>
<p>On desktop, MSAL gets a token by opening a browser popup where the user signs in. On mobile there is no browser. Your add-in runs in a WebView <em>inside</em> the Outlook app. There&rsquo;s no window to pop, no tab to redirect, nowhere for the classic OAuth dance to happen. My first attempt at calling Graph from the phone died right there, not on the API call, on the sign-in before it.</p>
<p>The fix has a name: <strong>Nested App Auth (NAA)</strong>.</p>
<h2 id="why-normal-msal-dies-on-mobile">Why Normal MSAL Dies on Mobile</h2>
<p>The standard MSAL flows (<code>loginPopup</code>, <code>loginRedirect</code>) both assume your code lives in a browser. A popup needs <code>window.open</code>. A redirect needs the page to navigate away to login.microsoftonline.com and come back with your app still knowing what it was doing.</p>
<p>Inside Outlook on iOS or Android, neither is true. You&rsquo;re in a WKWebView (or the Android equivalent) that Outlook controls. <code>window.open</code> goes nowhere useful, and a redirect would navigate the task pane itself into oblivion.</p>
<p>But think about what&rsquo;s <em>around</em> your WebView: the Outlook app. A fully signed-in, Microsoft-identity-aware native app that already knows exactly who the user is. NAA is the mechanism that lets your nested add-in say &ldquo;hey host, you handle the identity stuff&rdquo; and get tokens brokered through Outlook itself. When silent brokering isn&rsquo;t enough, the host hands off to Microsoft Authenticator for the interactive part, a real native auth experience instead of a popup that can&rsquo;t exist.</p>
<h2 id="the-azure-part-one-weird-redirect-uri">The Azure Part: One Weird Redirect URI</h2>
<p>Before any code, your Entra app registration needs to opt in to being brokered. That&rsquo;s done with a redirect URI of type <strong>Single-page application</strong> in this format:</p>
<pre tabindex="0"><code>brk-multihub://your-add-in-domain.com
</code></pre><p>Origin only, no subpaths:</p>
<ul>
<li>✅ <code>brk-multihub://contoso.com</code></li>
<li>✅ <code>brk-multihub://localhost:3000</code> (for dev)</li>
<li>❌ <code>brk-multihub://contoso.com/taskpane</code></li>
</ul>
<p>This URI is you telling the Microsoft identity platform: <em>&ldquo;I consent to having my auth brokered by trusted Microsoft 365 hosts.&rdquo;</em> The <code>multihub</code> part means it&rsquo;s not just Outlook, the same registration works when your add-in runs brokered inside Word, Excel, PowerPoint, or Teams.</p>
<p>One extra note if your add-in also runs in Word/Excel/PowerPoint <strong>on the web</strong>: those need an additional plain SPA redirect URI pointing at the actual page that requests tokens (your taskpane HTML), because the browser there still uses a standard flow.</p>
<h2 id="the-msal-part-one-config-two-worlds">The MSAL Part: One Config, Two Worlds</h2>
<p>Here&rsquo;s the neat thing about how NAA lands in code: you don&rsquo;t write a mobile version and a desktop version. You write one init function and let it fall back.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-typescript" data-lang="typescript"><span style="display:flex;"><span><span style="color:#66d9ef">import</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">createNestablePublicClientApplication</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">PublicClientApplication</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">type</span> <span style="color:#a6e22e">IPublicClientApplication</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">type</span> <span style="color:#a6e22e">Configuration</span>,
</span></span><span style="display:flex;"><span>} <span style="color:#66d9ef">from</span> <span style="color:#e6db74">&#34;@azure/msal-browser&#34;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">let</span> <span style="color:#a6e22e">isNAAMode</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">false</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">async</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">initMsal</span>()<span style="color:#f92672">:</span> <span style="color:#a6e22e">Promise</span>&lt;<span style="color:#f92672">IPublicClientApplication</span>&gt; {
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">msalConfig</span>: <span style="color:#66d9ef">Configuration</span> <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">auth</span><span style="color:#f92672">:</span> {
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">authority</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#34;https://login.microsoftonline.com/common&#34;</span>,
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">clientId</span>: <span style="color:#66d9ef">clientId</span>,
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">supportsNestedAppAuth</span>: <span style="color:#66d9ef">true</span>,
</span></span><span style="display:flex;"><span>    },
</span></span><span style="display:flex;"><span>  };
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">try</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">app</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">createNestablePublicClientApplication</span>(<span style="color:#a6e22e">msalConfig</span>);
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">isNAAMode</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">app</span>;
</span></span><span style="display:flex;"><span>  } <span style="color:#66d9ef">catch</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// NAA unavailable - fall back to standard MSAL
</span></span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">isNAAMode</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">false</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">app</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">PublicClientApplication</span>(<span style="color:#a6e22e">msalConfig</span>);
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">app</span>.<span style="color:#a6e22e">initialize</span>();
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">app</span>;
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p><strong>What&rsquo;s happening here?</strong></p>
<ol>
<li><code>supportsNestedAppAuth: true</code> flags the config as NAA-capable. Without it, nothing brokers.</li>
<li><code>createNestablePublicClientApplication</code> tries to hook into the host&rsquo;s broker. Inside Outlook mobile, it succeeds. In a plain browser tab or an older host, it throws.</li>
<li>The <code>catch</code> <em>is</em> the desktop path. Same fallback philosophy as the Graph post: don&rsquo;t ask &ldquo;am I on mobile?&rdquo;, try the capability and let the failure route you. One codebase, two auth worlds, zero platform sniffing.</li>
<li>Small but important: initialize this lazily, after <code>Office.onReady()</code> has resolved. The broker hookup needs the Office context to actually be there.</li>
</ol>
<h2 id="getting-a-token-let-the-host-do-the-remembering">Getting a Token: Let the Host Do the Remembering</h2>
<p>In classic MSAL you manage accounts yourself: call <code>getAllAccounts()</code>, pick one, pass it to <code>acquireTokenSilent</code>. In NAA mode you don&rsquo;t have to, the broker owns the accounts. You just give it a hint about who you expect:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-typescript" data-lang="typescript"><span style="display:flex;"><span><span style="color:#66d9ef">function</span> <span style="color:#a6e22e">getLoginHint</span>()<span style="color:#f92672">:</span> <span style="color:#66d9ef">string</span> <span style="color:#f92672">|</span> <span style="color:#66d9ef">undefined</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">Office</span>.<span style="color:#a6e22e">context</span><span style="color:#f92672">?</span>.<span style="color:#a6e22e">mailbox</span><span style="color:#f92672">?</span>.<span style="color:#a6e22e">userProfile</span><span style="color:#f92672">?</span>.<span style="color:#a6e22e">emailAddress</span> <span style="color:#f92672">??</span> <span style="color:#66d9ef">undefined</span>;
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">async</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">getToken</span>(<span style="color:#a6e22e">scopes</span>: <span style="color:#66d9ef">string</span>[])<span style="color:#f92672">:</span> <span style="color:#a6e22e">Promise</span>&lt;<span style="color:#f92672">string</span>&gt; {
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">app</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">getMsalInstance</span>();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">isNAAMode</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">try</span> {
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">result</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">app</span>.<span style="color:#a6e22e">acquireTokenSilent</span>({
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">scopes</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">loginHint</span>: <span style="color:#66d9ef">getLoginHint</span>(),
</span></span><span style="display:flex;"><span>      });
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">result</span>.<span style="color:#a6e22e">accessToken</span>;
</span></span><span style="display:flex;"><span>    } <span style="color:#66d9ef">catch</span> {
</span></span><span style="display:flex;"><span>      <span style="color:#75715e">// Silent failed - interactive fallback
</span></span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// On mobile this doesn&#39;t open a browser popup -
</span></span></span><span style="display:flex;"><span>    <span style="color:#75715e">// the host hands off to the Microsoft Authenticator app
</span></span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">result</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">app</span>.<span style="color:#a6e22e">acquireTokenPopup</span>({ <span style="color:#a6e22e">scopes</span> });
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">result</span>.<span style="color:#a6e22e">accessToken</span>;
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e">// Non-NAA (desktop/web): the classic dance -
</span></span></span><span style="display:flex;"><span>  <span style="color:#75715e">// getAllAccounts() → acquireTokenSilent(account) → popup on interaction_required
</span></span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">getTokenClassic</span>(<span style="color:#a6e22e">app</span>, <span style="color:#a6e22e">scopes</span>);
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p><strong>What&rsquo;s happening here?</strong></p>
<ol>
<li>The <code>loginHint</code> is just the signed-in mailbox owner&rsquo;s email, straight from <code>Office.context.mailbox.userProfile</code>. The broker uses it to resolve the right account, which is exactly the account the user is already signed into Outlook with. No account picker, no cache management.</li>
<li><code>acquireTokenSilent</code> with that hint succeeds almost every time, because of course it does, the user <em>is</em> signed in, that&rsquo;s how they&rsquo;re reading their email.</li>
<li>When interaction is genuinely needed (first consent, revoked session), <code>acquireTokenPopup</code> doesn&rsquo;t open a popup on mobile despite its name. The host routes it to Microsoft Authenticator, and the user gets a native sign-in experience that actually works on a phone.</li>
<li>The non-NAA branch keeps doing what MSAL apps have always done on desktop. Two branches in one function, and the caller just says <code>getToken(scopes)</code> and never knows which world it&rsquo;s in.</li>
</ol>
<p>The first time I watched the Authenticator app slide up over Outlook, approve, and slide away with my Graph call succeeding behind it, it felt like cheating. In a good way. 🎉</p>
<h2 id="gotchas-i-hit-along-the-way">Gotchas I Hit Along the Way</h2>
<ul>
<li><strong>The redirect URI must be SPA type.</strong> Adding <code>brk-multihub://...</code> under &ldquo;Web&rdquo; or &ldquo;Mobile and desktop applications&rdquo; in the app registration silently doesn&rsquo;t work. It goes under Single-page application, even though nothing about it looks like a page.</li>
<li><strong>Origin only.</strong> <code>brk-multihub://contoso.com/taskpane</code> fails validation. Trim it to the origin.</li>
<li><strong>MSAL.js v3+.</strong> <code>createNestablePublicClientApplication</code> doesn&rsquo;t exist in v2. Check your <code>@azure/msal-browser</code> version before you spend an hour on confusing import errors.</li>
<li><strong>No B2C.</strong> NAA supports Microsoft accounts and Entra ID work/school accounts. If you&rsquo;re on Azure AD B2C, this door is closed.</li>
</ul>
<h2 id="wrapping-up">Wrapping Up</h2>
<p>That&rsquo;s the trilogy done. Three posts, one theme: on mobile you don&rsquo;t get to demand things, you get to <em>ask and adapt</em>.</p>
<ul>
<li>The <a href="https://jeppe-spanggaard.dk/blogs/outlook-addin-manifest-requirement-sets-mobile/">manifest post</a> got us in the door: declare a low requirement floor so mobile shows the add-in at all.</li>
<li>The <a href="https://jeppe-spanggaard.dk/blogs/outlook-addin-graph-fallback-mobile/">Graph fallback post</a> got us the bytes: when the Office.js API isn&rsquo;t there, fetch the same email from Graph.</li>
<li>This post got us the token: when there&rsquo;s no browser to pop, let Outlook broker the auth and Authenticator handle the interaction.</li>
</ul>
<p>Same shape every time: try the capability, catch the failure, take the other road. Mobile support isn&rsquo;t one big feature, it&rsquo;s a stack of small fallbacks that each pretend nothing happened.</p>
]]></content:encoded></item><item><title>Microsoft Graph SDK Authentication in C#: Quick Start Guide</title><link>https://jeppe-spanggaard.dk/blogs/graph-sdk-authentication-csharp/</link><pubDate>Fri, 20 Jun 2025 00:00:00 +0000</pubDate><author>Jeppe</author><guid>https://jeppe-spanggaard.dk/blogs/graph-sdk-authentication-csharp/</guid><description>Quick setup guide for Microsoft Graph SDK using your existing app registration</description><content:encoded><![CDATA[<h2 id="what-is-microsoft-graph">What is Microsoft Graph?</h2>
<p>Microsoft Graph is the unified REST API for Microsoft 365. Think of it as a single gateway to access data across SharePoint, Teams, OneDrive, Outlook, Azure AD, and more - all through one consistent API.</p>
<p><strong>Graph vs CSOM/PnP.Framework:</strong></p>
<ul>
<li><strong>Graph</strong>: Modern REST API, works across all Microsoft 365 services</li>
<li><strong>CSOM/PnP</strong>: SharePoint-specific, more SharePoint features available</li>
</ul>
<h2 id="when-to-use-graph-sdk-vs-pnpframework">When to Use Graph SDK vs PnP.Framework</h2>
<p><strong>✅ Use Graph SDK when:</strong></p>
<ul>
<li>Building modern cloud applications</li>
<li>Need access to Teams, OneDrive, Exchange, or Azure AD</li>
<li>Want built-in retry logic, batching, and performance optimizations</li>
<li>Building apps that integrate multiple Microsoft 365 services</li>
</ul>
<p><strong>✅ Use PnP.Framework when:</strong></p>
<ul>
<li>Need advanced SharePoint features (site templates, provisioning, taxonomy)</li>
<li>Working with SharePoint-specific APIs not yet in Graph</li>
<li>Building SharePoint-focused solutions</li>
<li>Need features like search refiners, managed metadata, or custom actions</li>
</ul>
<p><strong>❌ Graph SDK limitations:</strong></p>
<ul>
<li>No support for SharePoint classic features (master pages, web parts)</li>
<li>Limited search capabilities compared to SharePoint Search API</li>
<li>Some advanced list/library features not available</li>
<li>Can&rsquo;t access SharePoint on-premises (SharePoint Server)</li>
</ul>
<h2 id="quick-start">Quick Start</h2>
<p>Building on the <a href="https://jeppe-spanggaard.dk/pnp-framework-authentication-csharp/">PnP.Framework authentication setup</a>, let&rsquo;s add Microsoft Graph SDK to access all Microsoft 365 services with the same certificate.</p>
<h2 id="add-graph-permissions">Add Graph Permissions</h2>
<p>In your existing app registration, go to &ldquo;API permissions&rdquo; → &ldquo;Add a permission&rdquo; → &ldquo;Microsoft Graph&rdquo; → &ldquo;Application permissions&rdquo;:</p>
<ul>
<li><strong>Sites.ReadWrite.All</strong>: SharePoint sites and lists access</li>
</ul>
<p><strong>Important</strong>: Click &ldquo;Grant admin consent&rdquo; after adding permissions!</p>
<h2 id="install-and-code">Install and Code</h2>
<p>Install the NuGet package:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>Install-Package Microsoft.Graph
</span></span></code></pre></div><p>Here&rsquo;s the same example from the PnP.Framework post, but using Graph SDK:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">using</span> Microsoft.Graph;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">using</span> Microsoft.Graph.Auth;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">using</span> System.Security.Cryptography.X509Certificates;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Program</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">static</span> <span style="color:#66d9ef">readonly</span> <span style="color:#66d9ef">string</span> TenantId = <span style="color:#e6db74">&#34;your-tenant-id&#34;</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">static</span> <span style="color:#66d9ef">readonly</span> <span style="color:#66d9ef">string</span> ClientId = <span style="color:#e6db74">&#34;your-client-id&#34;</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">static</span> <span style="color:#66d9ef">readonly</span> <span style="color:#66d9ef">string</span> CertificatePath = <span style="color:#e6db74">@&#34;C:\Temp\cert\pnpappcert.pfx&#34;</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">static</span> <span style="color:#66d9ef">readonly</span> <span style="color:#66d9ef">string</span> CertificatePassword = <span style="color:#e6db74">&#34;MySuperStrongPassword!&#34;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">static</span> <span style="color:#66d9ef">async</span> Task Main(<span style="color:#66d9ef">string</span>[] args)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// Create Graph client (same certificate as PnP.Framework)</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">var</span> certificate = <span style="color:#66d9ef">new</span> X509Certificate2(CertificatePath, CertificatePassword);
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">var</span> options = <span style="color:#66d9ef">new</span> ClientCertificateCredentialOptions
</span></span><span style="display:flex;"><span>        {
</span></span><span style="display:flex;"><span>            AuthorityHost = AzureAuthorityHosts.AzurePublicCloud,
</span></span><span style="display:flex;"><span>        };
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">var</span> clientCertCredential = <span style="color:#66d9ef">new</span> ClientCertificateCredential(
</span></span><span style="display:flex;"><span>            TenantId, ClientId, certificate, options);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">var</span> scopes = <span style="color:#66d9ef">new</span>[] { <span style="color:#e6db74">&#34;https://graph.microsoft.com/.default&#34;</span> };
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">var</span> graphClient = <span style="color:#66d9ef">new</span> GraphServiceClient(clientCertCredential, scopes);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// Test connection</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">var</span> org = <span style="color:#66d9ef">await</span> graphClient.Organization.Request().GetAsync();
</span></span><span style="display:flex;"><span>        Console.WriteLine(<span style="color:#e6db74">$&#34;Connected to: {org.First().DisplayName}&#34;</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// Get all lists (like PnP.Framework example)</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">var</span> site = <span style="color:#66d9ef">await</span> graphClient.Sites[<span style="color:#e6db74">&#34;root&#34;</span>].Request().GetAsync();
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">var</span> lists = <span style="color:#66d9ef">await</span> graphClient.Sites[site.Id].Lists.Request().GetAsync();
</span></span><span style="display:flex;"><span>        
</span></span><span style="display:flex;"><span>        Console.WriteLine(<span style="color:#e6db74">&#34;Lists in this site:&#34;</span>);
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">foreach</span> (<span style="color:#66d9ef">var</span> list <span style="color:#66d9ef">in</span> lists)
</span></span><span style="display:flex;"><span>        {
</span></span><span style="display:flex;"><span>            Console.WriteLine(<span style="color:#e6db74">$&#34;- {list.DisplayName}&#34;</span>);
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// Upload a document (like PnP.Framework example)</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">var</span> docLib = lists.FirstOrDefault(l =&gt; l.DisplayName == <span style="color:#e6db74">&#34;Documents&#34;</span>);
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> (docLib != <span style="color:#66d9ef">null</span>)
</span></span><span style="display:flex;"><span>        {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">var</span> content = System.Text.Encoding.UTF8.GetBytes(<span style="color:#e6db74">&#34;Hello from Graph SDK!&#34;</span>);
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">var</span> drive = <span style="color:#66d9ef">await</span> graphClient.Sites[site.Id].Lists[docLib.Id].Drive.Request().GetAsync();
</span></span><span style="display:flex;"><span>            
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">var</span> file = <span style="color:#66d9ef">await</span> graphClient.Drives[drive.Id].Root
</span></span><span style="display:flex;"><span>                .ItemWithPath(<span style="color:#e6db74">&#34;sample-document.txt&#34;</span>)
</span></span><span style="display:flex;"><span>                .Content
</span></span><span style="display:flex;"><span>                .Request()
</span></span><span style="display:flex;"><span>                .PutAsync&lt;DriveItem&gt;(<span style="color:#66d9ef">new</span> MemoryStream(content));
</span></span><span style="display:flex;"><span>            
</span></span><span style="display:flex;"><span>            Console.WriteLine(<span style="color:#e6db74">$&#34;Uploaded file: {file.Name}&#34;</span>);
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h2 id="key-differences-from-pnpframework">Key Differences from PnP.Framework</h2>
<p><strong>Same operations, different syntax:</strong></p>
<table>
  <thead>
      <tr>
          <th>Operation</th>
          <th>PnP.Framework</th>
          <th>Graph SDK</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>Get Lists</strong></td>
          <td><code>context.Web.Lists</code></td>
          <td><code>graphClient.Sites[id].Lists</code></td>
      </tr>
      <tr>
          <td><strong>Upload File</strong></td>
          <td><code>docLib.RootFolder.Files.Add()</code></td>
          <td><code>drive.Root.ItemWithPath().Content.Put()</code></td>
      </tr>
      <tr>
          <td><strong>Authentication</strong></td>
          <td><code>AuthenticationManager</code></td>
          <td><code>ClientCertificateProvider</code></td>
      </tr>
  </tbody>
</table>
<p><strong>Graph advantages:</strong> Access to Teams, OneDrive, Exchange data with same client<br>
<strong>PnP advantages:</strong> More SharePoint-specific features like content types, site columns</p>
<h2 id="troubleshooting">Troubleshooting</h2>
<ul>
<li><strong>&ldquo;Insufficient privileges&rdquo;</strong>: Check permissions and admin consent</li>
<li><strong>&ldquo;Certificate not found&rdquo;</strong>: Verify certificate path and password</li>
<li><strong>&ldquo;Unauthorized&rdquo;</strong>: Ensure app has correct Graph permissions</li>
</ul>
<h2 id="next-steps">Next Steps</h2>
<p>You now have Graph SDK working with your existing app registration! With the basic setup, you can:</p>
<ul>
<li>Access SharePoint sites and lists</li>
<li>Upload and manage files in document libraries</li>
<li>Get organization information</li>
</ul>
<p>To access more Microsoft 365 services, add the appropriate permissions:</p>
<ul>
<li><strong>User.Read.All</strong> for user data</li>
<li><strong>Group.Read.All</strong> for Teams and groups</li>
<li><strong>Files.ReadWrite.All</strong> for broader file operations</li>
<li><strong>Mail.Read</strong> for Exchange data</li>
</ul>
<p>Perfect foundation for building comprehensive Microsoft 365 integrations.</p>
]]></content:encoded></item><item><title>PnP.Framework Authentication in C#: A Beginner's Guide</title><link>https://jeppe-spanggaard.dk/blogs/pnp-framework-authentication-csharp/</link><pubDate>Tue, 03 Jun 2025 00:00:00 +0000</pubDate><author>Jeppe</author><guid>https://jeppe-spanggaard.dk/blogs/pnp-framework-authentication-csharp/</guid><description>Learn to authenticate C# applications with PnP.Framework and Azure using certificates in this beginner's guide to app registration and permissions.</description><content:encoded><![CDATA[<h2 id="app-registration">App registration</h2>
<p>Go to <a href="https://portal.azure.com">https://portal.azure.com</a> and search for &ldquo;App registrations&rdquo;:
<img src="https://jeppe-spanggaard.dk/images/AzurePortalSearchAppReg_hu_2be0317d5be28fdd.webp" srcset="/images/AzurePortalSearchAppReg_hu_2be0317d5be28fdd.webp 480w" sizes="(max-width: 760px) 100vw, 720px"
    width="480" height="156"
    alt="alt text" style="background:url(data:image/webp;base64,UklGRmIAAABXRUJQVlA4IFYAAABQBACdASoYAAgAP1mQvk0pJKMhMAgBJisJ5wAu/8CHgZbNgyH&#43;07aNzahIAP6Qpu6EMwIw2MnqeHDQTfFMo3hrSUOQmegSAO34/GABBE7jsmqsuAAAAA==) center/cover no-repeat" loading="lazy" decoding="async"></p>
<p>Click &ldquo;New registration&rdquo; and fill out the name, and leave the &ldquo;Supported account types&rdquo; to default for now (Default: Accounts in this organizational directory only (xxx - Single tenant))</p>
<p>And now just press &ldquo;Register&rdquo;</p>
<p>Just like so, you have created your app registration! Take note of the <strong>Application (client) ID</strong> and <strong>Directory (tenant) ID</strong> from the overview page - you&rsquo;ll need these values later in your C# code.</p>
<p><img src="https://jeppe-spanggaard.dk/images/AppRegOverviewPage_hu_c2869f9b03e36559.webp" srcset="/images/AppRegOverviewPage_hu_e913716f99ceec37.webp 480w, /images/AppRegOverviewPage_hu_c2869f9b03e36559.webp 720w" sizes="(max-width: 760px) 100vw, 720px"
    width="720" height="285"
    alt="App Registration Overview Page" style="background:url(data:image/webp;base64,UklGRkgAAABXRUJQVlA4IDwAAADwAwCdASoYAAoAP1mKtkspJKKYBACTFYT0gAAzJ2esdzwpc/l&#43;ifwAAP7T4zOKUnyOrAypj7iJC/RgEAA=) center/cover no-repeat" loading="lazy" decoding="async"></p>
<h3 id="generate-certificate">Generate certificate</h3>
<p>We need a certificate on the app registration, and in our code to authenticate our C# application. Certificates are more secure than client secrets because they&rsquo;re harder to extract and can be managed through your organization&rsquo;s PKI infrastructure.</p>
<p>Open PowerShell as Administrator and run the following commands:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-PowerShell" data-lang="PowerShell"><span style="display:flex;"><span>$cert = New-SelfSignedCertificate `
</span></span><span style="display:flex;"><span>    -Subject <span style="color:#e6db74">&#34;CN=MyPnPAppCert&#34;</span> `
</span></span><span style="display:flex;"><span>    -CertStoreLocation <span style="color:#e6db74">&#34;cert:\CurrentUser\My&#34;</span> `
</span></span><span style="display:flex;"><span>    -KeyExportPolicy Exportable `
</span></span><span style="display:flex;"><span>    -KeySpec Signature `
</span></span><span style="display:flex;"><span>    -KeyLength <span style="color:#ae81ff">2048</span> `
</span></span><span style="display:flex;"><span>    -NotAfter (Get-Date).AddYears(<span style="color:#ae81ff">3</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>$plainText = <span style="color:#e6db74">&#34;MySuperStrongPassword!&#34;</span>
</span></span><span style="display:flex;"><span>$secureString = ConvertTo-SecureString $plainText -AsPlainText -Force
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>Export-PfxCertificate `
</span></span><span style="display:flex;"><span>    -Cert $cert `
</span></span><span style="display:flex;"><span>    -FilePath <span style="color:#e6db74">&#34;C:\Temp\cert\pnpappcert.pfx&#34;</span> `
</span></span><span style="display:flex;"><span>    -Password $secureString
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>Export-Certificate `
</span></span><span style="display:flex;"><span>    -Cert $cert `
</span></span><span style="display:flex;"><span>    -FilePath <span style="color:#e6db74">&#34;C:\Temp\cert\pnpappcert.cer&#34;</span>        
</span></span></code></pre></div><p>This script creates a self-signed certificate that&rsquo;s valid for 3 years. The certificate gets stored in your personal certificate store, and we export both the .pfx file (containing the private key) and the .cer file (public key only). Make sure the <code>C:\Temp\cert\</code> directory exists before running this script.</p>
<h3 id="upload-certificate">Upload certificate</h3>
<p>We need to upload the generated certificate - the .cer file (NOT the .pfx), to the app registration. The .cer file contains only the public key, while the .pfx contains the private key that should never be shared.</p>
<p>Navigate to your app registration in Azure Portal, go to &ldquo;Certificates &amp; secrets&rdquo;, then click &ldquo;Upload certificate&rdquo;:
<img src="https://jeppe-spanggaard.dk/images/AppRegCertUpload_hu_beb98805c0b9f235.webp" srcset="/images/AppRegCertUpload_hu_4dddca826d70ddb0.webp 480w, /images/AppRegCertUpload_hu_8391d4a5ff602c8a.webp 720w, /images/AppRegCertUpload_hu_beb98805c0b9f235.webp 1200w" sizes="(max-width: 760px) 100vw, 720px"
    width="1200" height="461"
    alt="alt text" style="background:url(data:image/webp;base64,UklGRkAAAABXRUJQVlA4IDQAAACwAwCdASoYAAkAP1mkvk&#43;pJqMhMAgBJisJ6QAAZk9VL9Z6OYRmAAD&#43;0z8ijK3FkkbjgAAA) center/cover no-repeat" loading="lazy" decoding="async"></p>
<p>After uploading, you&rsquo;ll see the certificate listed with its thumbprint. The thumbprint can be useful for identifying which certificate to use if you have multiple certificates.</p>
<h3 id="add-permissions">Add permissions</h3>
<p>Our app registration needs permission to be able to do stuff. There are two types of permissions: Delegated and Application.
The differences between them are:</p>
<ul>
<li><strong>Delegated permissions</strong>: Your app acts on behalf of a signed-in user. The app can only access what the user has access to.</li>
<li><strong>Application permissions</strong>: Your app runs as itself without a signed-in user. The app has access based on what you grant it.</li>
</ul>
<p>For this example we&rsquo;ll use the SharePoint permission &ldquo;Sites.FullControl.All&rdquo; as an application permission, which gives our app full access to all SharePoint sites.</p>
<p>After adding the permission, don&rsquo;t forget to click &ldquo;Grant admin consent&rdquo; - this is crucial! Without admin consent, your app won&rsquo;t be able to use the permissions even if they&rsquo;re assigned.</p>
<p>Now our app registration is setup and ready to be used in our C# code.</p>
<h2 id="c-implementation">C# Implementation</h2>
<p>Now let&rsquo;s write the C# code to authenticate and connect to SharePoint using our certificate. I&rsquo;ll show you a complete working example that you can use as a starting point for your own projects.</p>
<p>First, create a new Console Application in Visual Studio and install the PnP.Framework NuGet package:</p>
<ul>
<li><a href="https://www.nuget.org/packages/PnP.Framework/1.18.0">https://www.nuget.org/packages/PnP.Framework/1.18.0</a></li>
</ul>
<p>You can install it via Package Manager Console: <code>Install-Package PnP.Framework</code></p>
<p>Here&rsquo;s a complete example that connects to SharePoint and demonstrates several common operations:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">using</span> PnP.Framework;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">using</span> System.Security.Cryptography.X509Certificates;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Program</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">static</span> <span style="color:#66d9ef">readonly</span> <span style="color:#66d9ef">string</span> TenantId = <span style="color:#e6db74">&#34;your-tenant-id&#34;</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">static</span> <span style="color:#66d9ef">readonly</span> <span style="color:#66d9ef">string</span> ClientId = <span style="color:#e6db74">&#34;your-client-id&#34;</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">static</span> <span style="color:#66d9ef">readonly</span> <span style="color:#66d9ef">string</span> CertificatePath = <span style="color:#e6db74">@&#34;C:\Temp\cert\pnpappcert.pfx&#34;</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">static</span> <span style="color:#66d9ef">readonly</span> <span style="color:#66d9ef">string</span> CertificatePassword = <span style="color:#e6db74">&#34;MySuperStrongPassword!&#34;</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">static</span> <span style="color:#66d9ef">readonly</span> <span style="color:#66d9ef">string</span> SiteUrl = <span style="color:#e6db74">&#34;https://yourtenant.sharepoint.com/sites/yoursite&#34;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">static</span> <span style="color:#66d9ef">async</span> Task Main(<span style="color:#66d9ef">string</span>[] args)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">try</span>
</span></span><span style="display:flex;"><span>        {
</span></span><span style="display:flex;"><span>            <span style="color:#75715e">// Load the certificate</span>
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">var</span> certificate = <span style="color:#66d9ef">new</span> X509Certificate2(CertificatePath, CertificatePassword);
</span></span><span style="display:flex;"><span>            
</span></span><span style="display:flex;"><span>            <span style="color:#75715e">// Create authentication manager</span>
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">var</span> authManager = <span style="color:#66d9ef">new</span> AuthenticationManager(ClientId, certificate, TenantId);
</span></span><span style="display:flex;"><span>            
</span></span><span style="display:flex;"><span>            <span style="color:#75715e">// Get context</span>
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">using</span> var context = authManager.GetContext(SiteUrl);
</span></span><span style="display:flex;"><span>            
</span></span><span style="display:flex;"><span>            <span style="color:#75715e">// Load web properties</span>
</span></span><span style="display:flex;"><span>            context.Load(context.Web, w =&gt; w.Title, w =&gt; w.Url);
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">await</span> context.ExecuteQueryAsync();
</span></span><span style="display:flex;"><span>            
</span></span><span style="display:flex;"><span>            Console.WriteLine(<span style="color:#e6db74">$&#34;Connected to: {context.Web.Title}&#34;</span>);
</span></span><span style="display:flex;"><span>            Console.WriteLine(<span style="color:#e6db74">$&#34;Site URL: {context.Web.Url}&#34;</span>);
</span></span><span style="display:flex;"><span>            
</span></span><span style="display:flex;"><span>            <span style="color:#75715e">// Example: Get all lists</span>
</span></span><span style="display:flex;"><span>            context.Load(context.Web.Lists, lists =&gt; lists.Include(l =&gt; l.Title, l =&gt; l.ItemCount));
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">await</span> context.ExecuteQueryAsync();
</span></span><span style="display:flex;"><span>            
</span></span><span style="display:flex;"><span>            Console.WriteLine(<span style="color:#e6db74">&#34;\nLists in this site:&#34;</span>);
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">foreach</span> (<span style="color:#66d9ef">var</span> list <span style="color:#66d9ef">in</span> context.Web.Lists)
</span></span><span style="display:flex;"><span>            {
</span></span><span style="display:flex;"><span>                Console.WriteLine(<span style="color:#e6db74">$&#34;- {list.Title} ({list.ItemCount} items)&#34;</span>);
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>            <span style="color:#75715e">// Example: Create a new list</span>
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">await</span> CreateSampleList(context);
</span></span><span style="display:flex;"><span>            
</span></span><span style="display:flex;"><span>            <span style="color:#75715e">// Example: Upload a document</span>
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">await</span> UploadDocument(context);
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">catch</span> (Exception ex)
</span></span><span style="display:flex;"><span>        {
</span></span><span style="display:flex;"><span>            Console.WriteLine(<span style="color:#e6db74">$&#34;Error: {ex.Message}&#34;</span>);
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">static</span> <span style="color:#66d9ef">async</span> Task CreateSampleList(ClientContext context)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">try</span>
</span></span><span style="display:flex;"><span>        {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">var</span> listCreationInfo = <span style="color:#66d9ef">new</span> ListCreationInformation
</span></span><span style="display:flex;"><span>            {
</span></span><span style="display:flex;"><span>                Title = <span style="color:#e6db74">&#34;Sample List&#34;</span>,
</span></span><span style="display:flex;"><span>                TemplateType = (<span style="color:#66d9ef">int</span>)ListTemplateType.GenericList
</span></span><span style="display:flex;"><span>            };
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">var</span> list = context.Web.Lists.Add(listCreationInfo);
</span></span><span style="display:flex;"><span>            context.Load(list);
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">await</span> context.ExecuteQueryAsync();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>            Console.WriteLine(<span style="color:#e6db74">$&#34;\nCreated list: {list.Title}&#34;</span>);
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">catch</span> (Exception ex)
</span></span><span style="display:flex;"><span>        {
</span></span><span style="display:flex;"><span>            Console.WriteLine(<span style="color:#e6db74">$&#34;List creation error: {ex.Message}&#34;</span>);
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">static</span> <span style="color:#66d9ef">async</span> Task UploadDocument(ClientContext context)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">try</span>
</span></span><span style="display:flex;"><span>        {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">var</span> docLib = context.Web.Lists.GetByTitle(<span style="color:#e6db74">&#34;Documents&#34;</span>);
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">var</span> fileCreationInfo = <span style="color:#66d9ef">new</span> FileCreationInformation
</span></span><span style="display:flex;"><span>            {
</span></span><span style="display:flex;"><span>                Content = System.Text.Encoding.UTF8.GetBytes(<span style="color:#e6db74">&#34;Hello from PnP Framework!&#34;</span>),
</span></span><span style="display:flex;"><span>                Url = <span style="color:#e6db74">&#34;sample-document.txt&#34;</span>,
</span></span><span style="display:flex;"><span>                Overwrite = <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>            };
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">var</span> uploadFile = docLib.RootFolder.Files.Add(fileCreationInfo);
</span></span><span style="display:flex;"><span>            context.Load(uploadFile);
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">await</span> context.ExecuteQueryAsync();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>            Console.WriteLine(<span style="color:#e6db74">$&#34;\nUploaded file: {uploadFile.Name}&#34;</span>);
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">catch</span> (Exception ex)
</span></span><span style="display:flex;"><span>        {
</span></span><span style="display:flex;"><span>            Console.WriteLine(<span style="color:#e6db74">$&#34;File upload error: {ex.Message}&#34;</span>);
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h3 id="understanding-the-code">Understanding the Code</h3>
<p>Let&rsquo;s break down what this code does:</p>
<ol>
<li><strong>Certificate Loading</strong>: We load the .pfx file with the password we set earlier</li>
<li><strong>Authentication Manager</strong>: This handles the OAuth flow using the certificate</li>
<li><strong>Context Creation</strong>: Gets a <code>ClientContext</code> object that represents our connection to SharePoint</li>
<li><strong>Basic Operations</strong>: Shows how to read site properties, list all lists, create a new list, and upload a document</li>
</ol>
<p>The beauty of PnP.Framework is that it abstracts away all the complex authentication details. Once you have the <code>ClientContext</code>, you can use it just like the regular SharePoint CSOM (Client Side Object Model).</p>
<h3 id="important-notes">Important Notes</h3>
<ul>
<li>Replace <code>your-tenant-id</code>, <code>your-client-id</code>, and the site URL with your actual values from Azure Portal</li>
<li>The certificate path should point to your .pfx file (not the .cer file)</li>
<li>Make sure your app has the necessary permissions granted and admin consent given</li>
<li>Store sensitive information like certificates and passwords securely in production (use Azure Key Vault or similar)</li>
<li>Consider using certificate thumbprints instead of file paths in production environments</li>
<li>The <code>using</code> statement ensures proper disposal of the ClientContext</li>
</ul>
<h3 id="production-considerations">Production Considerations</h3>
<p>For production applications, consider these improvements:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#75715e">// Load certificate from certificate store instead of file</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">var</span> store = <span style="color:#66d9ef">new</span> X509Store(StoreName.My, StoreLocation.CurrentUser);
</span></span><span style="display:flex;"><span>store.Open(OpenFlags.ReadOnly);
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">var</span> certificate = store.Certificates.Find(X509FindType.FindByThumbprint, <span style="color:#e6db74">&#34;your-cert-thumbprint&#34;</span>, <span style="color:#66d9ef">false</span>)[<span style="color:#ae81ff">0</span>];
</span></span><span style="display:flex;"><span>store.Close();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Use configuration instead of hardcoded values</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">var</span> tenantId = Configuration[<span style="color:#e6db74">&#34;Azure:TenantId&#34;</span>];
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">var</span> clientId = Configuration[<span style="color:#e6db74">&#34;Azure:ClientId&#34;</span>];
</span></span></code></pre></div><h2 id="troubleshooting">Troubleshooting</h2>
<p>If you run into issues, here are some common problems and their solutions:</p>
<p><strong>Certificate not found</strong>: Make sure the certificate path is correct and the password matches what you used when creating it. You can verify the certificate exists by checking it in the Windows Certificate Manager (certmgr.msc).</p>
<p><strong>Access denied</strong>: Verify that your app registration has the correct permissions and that admin consent has been granted. Check the Azure Portal to ensure the permissions show as &ldquo;Granted for [Your Organization]&rdquo;.</p>
<p><strong>Tenant ID issues</strong>: You can find your tenant ID in the Azure Portal under Azure Active Directory &gt; Properties. It&rsquo;s also visible in the app registration overview page.</p>
<p><strong>&ldquo;The remote server returned an error: (401) Unauthorized&rdquo;</strong>: This usually means the certificate isn&rsquo;t properly associated with the app registration, or the app doesn&rsquo;t have sufficient permissions. Double-check both the certificate upload and permission configuration.</p>
<p><strong>Certificate validation errors</strong>: If you&rsquo;re using a self-signed certificate in a corporate environment, your organization&rsquo;s security policies might block it. Consider using a certificate issued by your internal CA.</p>
<h3 id="testing-your-setup">Testing Your Setup</h3>
<p>Before diving into complex operations, test your connection with this minimal code:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">try</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> certificate = <span style="color:#66d9ef">new</span> X509Certificate2(CertificatePath, CertificatePassword);
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> authManager = <span style="color:#66d9ef">new</span> AuthenticationManager(ClientId, certificate, TenantId);
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">using</span> var context = authManager.GetContext(SiteUrl);
</span></span><span style="display:flex;"><span>    
</span></span><span style="display:flex;"><span>    context.Load(context.Web, w =&gt; w.Title);
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">await</span> context.ExecuteQueryAsync();
</span></span><span style="display:flex;"><span>    
</span></span><span style="display:flex;"><span>    Console.WriteLine(<span style="color:#e6db74">$&#34;Success! Connected to: {context.Web.Title}&#34;</span>);
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">catch</span> (Exception ex)
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    Console.WriteLine(<span style="color:#e6db74">$&#34;Connection failed: {ex.Message}&#34;</span>);
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h2 id="wrapping-up">Wrapping Up</h2>
<p>You now have a working setup for authenticating with SharePoint using PnP.Framework and certificates. This method is perfect for background services, console applications, or any scenario where you need unattended access to SharePoint.</p>
<p>The certificate-based authentication is more secure than using client secrets since certificates are harder to compromise and can be managed through your organization&rsquo;s certificate infrastructure.</p>
<p>In my next post, I&rsquo;ll show you how to perform common SharePoint operations like creating lists, uploading files, and managing permissions using this authentication setup.</p>
]]></content:encoded></item><item><title>SPFx Audience targeting</title><link>https://jeppe-spanggaard.dk/blogs/role-based-rendering/</link><pubDate>Tue, 09 Apr 2024 00:00:00 +0000</pubDate><author>Jeppe</author><guid>https://jeppe-spanggaard.dk/blogs/role-based-rendering/</guid><description>How to easy implement audience targeting for for one's components</description><content:encoded><![CDATA[<h2 id="audience-targeting-">Audience Targeting 🎯</h2>
<p>Audience targeting allows content administrators to direct content toward specific groups of users. This is particularly useful in organizations with a wide array of users, where not all content is relevant for everyone. By using audience targeting, you can improve the relevance of the content each user sees, leading to a more personalized and focused user experience.</p>
<h2 id="audience-targeted-wrapper-">Audience Targeted Wrapper 🔧</h2>
<p>Let&rsquo;s dive into how to build an SPFx component that employs audience targeting. This component acts as a wrapper, showing or hiding content based on the user&rsquo;s membership in specific groups.</p>
<h3 id="group-check-logic-">Group Check Logic 🔍</h3>
<p>At the heart of our component is the logic that determines if the current user is a member of any of the specified groups.</p>
<p><strong>Group Check Logic</strong></p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-JSX" data-lang="JSX"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">async</span> <span style="color:#a6e22e">isMember</span>(<span style="color:#a6e22e">group</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">any</span>, <span style="color:#a6e22e">context</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">any</span>)<span style="color:#f92672">:</span> Promise&lt;<span style="color:#f92672">boolean</span>&gt; {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">_cacheName</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;CacheName_memberGroups&#39;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">group</span> <span style="color:#f92672">==</span> <span style="color:#66d9ef">null</span> <span style="color:#f92672">||</span> <span style="color:#a6e22e">group</span> <span style="color:#f92672">==</span> <span style="color:#e6db74">&#39;&#39;</span>) {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">true</span>;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">graph</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">graphfi</span>().<span style="color:#a6e22e">using</span>(<span style="color:#a6e22e">SPFx</span>(<span style="color:#a6e22e">context</span>));
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">let</span> <span style="color:#a6e22e">_cachedMemberGroups</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">sessionStorage</span>.<span style="color:#a6e22e">getItem</span>(<span style="color:#a6e22e">_cacheName</span>);
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">let</span> <span style="color:#a6e22e">memberGroups</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">string</span>[] <span style="color:#f92672">=</span> [];
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> (<span style="color:#f92672">!</span><span style="color:#a6e22e">_cachedMemberGroups</span>) {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">memberGroups</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">graph</span>.<span style="color:#a6e22e">me</span>.<span style="color:#a6e22e">getMemberGroups</span>();
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">sessionStorage</span>.<span style="color:#a6e22e">setItem</span>(<span style="color:#a6e22e">_cacheName</span>, <span style="color:#a6e22e">JSON</span>.<span style="color:#a6e22e">stringify</span>(<span style="color:#a6e22e">memberGroups</span>));
</span></span><span style="display:flex;"><span>    } <span style="color:#66d9ef">else</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">memberGroups</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">JSON</span>.<span style="color:#a6e22e">parse</span>(<span style="color:#a6e22e">_cachedMemberGroups</span>);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// &#34;c:0o.c|federateddirectoryclaimprovider|xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx&#34;
</span></span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">groupID</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">group</span>.<span style="color:#a6e22e">id</span>.<span style="color:#a6e22e">split</span>(<span style="color:#e6db74">&#39;|&#39;</span>)[<span style="color:#ae81ff">2</span>];
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">memberGroups</span>.<span style="color:#a6e22e">filter</span>((<span style="color:#a6e22e">g</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">string</span>) =&gt; <span style="color:#a6e22e">g</span> <span style="color:#f92672">===</span> <span style="color:#a6e22e">groupID</span>).<span style="color:#a6e22e">length</span> <span style="color:#f92672">&gt;</span> <span style="color:#ae81ff">0</span>) {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">true</span>;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">throw</span> <span style="color:#66d9ef">new</span> Error(<span style="color:#e6db74">&#39;User not found&#39;</span>);
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The wrapper component is visually minimalistic, primarily designed to function based on the logic of accessing group IDs from an array of <strong>IPropertyFieldGroupOrPerson</strong>. Its simplicity belies its utility, enabling dynamic content visibility tailored to user or group permissions with little visual footprint.</p>
<p><strong>TargetAudience wrapper</strong></p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-JSX" data-lang="JSX"><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">interface</span> <span style="color:#a6e22e">ITargetAudienceProps</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">pageContext</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">PageContext</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">context</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">any</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">groupIds</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">IPropertyFieldGroupOrPerson</span>[];
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">TargetAudience</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">React</span>.<span style="color:#a6e22e">FC</span>&lt;<span style="color:#f92672">ITargetAudienceProps</span>&gt; <span style="color:#f92672">=</span> ({ <span style="color:#a6e22e">pageContext</span>, <span style="color:#a6e22e">groupIds</span>, <span style="color:#a6e22e">children</span>, <span style="color:#a6e22e">context</span> }) =&gt; {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">const</span> [<span style="color:#a6e22e">canView</span>, <span style="color:#a6e22e">setCanView</span>] <span style="color:#f92672">=</span> <span style="color:#a6e22e">useState</span>&lt;<span style="color:#f92672">boolean</span>&gt;(<span style="color:#66d9ef">false</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">useEffect</span>(() =&gt; {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">checkUserIsAllowedToViewWebpart</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">async</span> () =&gt; {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">let</span> <span style="color:#a6e22e">proms</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">any</span>[] <span style="color:#f92672">=</span> [];
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">errors</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">any</span>[] <span style="color:#f92672">=</span> [];
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">groupIds</span><span style="color:#f92672">?</span>.<span style="color:#a6e22e">map</span>((<span style="color:#a6e22e">item</span> <span style="color:#f92672">:</span> <span style="color:#a6e22e">any</span>) =&gt; {
</span></span><span style="display:flex;"><span>                <span style="color:#a6e22e">proms</span>.<span style="color:#a6e22e">push</span>(
</span></span><span style="display:flex;"><span>                    <span style="color:#a6e22e">isMember</span>(<span style="color:#a6e22e">item</span>, <span style="color:#a6e22e">context</span>)
</span></span><span style="display:flex;"><span>                );
</span></span><span style="display:flex;"><span>            });
</span></span><span style="display:flex;"><span>            Promise.<span style="color:#a6e22e">race</span>(
</span></span><span style="display:flex;"><span>                <span style="color:#a6e22e">proms</span>.<span style="color:#a6e22e">map</span>(<span style="color:#a6e22e">p</span> =&gt; {
</span></span><span style="display:flex;"><span>                    <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">p</span>.<span style="color:#66d9ef">catch</span>((<span style="color:#a6e22e">err</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">any</span>) =&gt; {
</span></span><span style="display:flex;"><span>                        <span style="color:#a6e22e">errors</span>.<span style="color:#a6e22e">push</span>(<span style="color:#a6e22e">err</span>);
</span></span><span style="display:flex;"><span>                        <span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">errors</span>.<span style="color:#a6e22e">length</span> <span style="color:#f92672">&gt;=</span> <span style="color:#a6e22e">proms</span>.<span style="color:#a6e22e">length</span>){
</span></span><span style="display:flex;"><span>                            <span style="color:#66d9ef">throw</span> <span style="color:#a6e22e">errors</span>;
</span></span><span style="display:flex;"><span>                        } 
</span></span><span style="display:flex;"><span>                        <span style="color:#66d9ef">return</span> Promise.<span style="color:#a6e22e">race</span>(<span style="color:#66d9ef">null</span>);
</span></span><span style="display:flex;"><span>                    });
</span></span><span style="display:flex;"><span>                }))
</span></span><span style="display:flex;"><span>                .<span style="color:#a6e22e">then</span>(<span style="color:#a6e22e">val</span> =&gt; {
</span></span><span style="display:flex;"><span>                    <span style="color:#a6e22e">setCanView</span>(<span style="color:#66d9ef">true</span>);
</span></span><span style="display:flex;"><span>                });
</span></span><span style="display:flex;"><span>        };
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">checkUserIsAllowedToViewWebpart</span>();
</span></span><span style="display:flex;"><span>    }, [<span style="color:#a6e22e">groupIds</span>, <span style="color:#a6e22e">pageContext</span>]);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> (
</span></span><span style="display:flex;"><span>        &lt;<span style="color:#f92672">div</span>&gt;
</span></span><span style="display:flex;"><span>            {<span style="color:#a6e22e">groupIds</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">null</span> <span style="color:#f92672">&amp;&amp;</span> <span style="color:#a6e22e">groupIds</span>.<span style="color:#a6e22e">length</span> <span style="color:#f92672">&gt;=</span> <span style="color:#ae81ff">1</span> <span style="color:#f92672">?</span> (<span style="color:#a6e22e">canView</span> <span style="color:#f92672">?</span> <span style="color:#a6e22e">children</span> <span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;&#39;</span>) <span style="color:#f92672">:</span> <span style="color:#a6e22e">children</span>}
</span></span><span style="display:flex;"><span>        &lt;/<span style="color:#f92672">div</span>&gt;
</span></span><span style="display:flex;"><span>    );
</span></span><span style="display:flex;"><span>};
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">default</span> <span style="color:#a6e22e">TargetAudience</span>;
</span></span></code></pre></div><p><strong>How to use it</strong></p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-JSX" data-lang="JSX"><span style="display:flex;"><span>&lt;<span style="color:#f92672">TargetAudience</span> <span style="color:#a6e22e">groupIds</span><span style="color:#f92672">=</span>{<span style="color:#a6e22e">GroupsIDS</span>} <span style="color:#a6e22e">pageContext</span><span style="color:#f92672">=</span>{<span style="color:#a6e22e">pageContext</span>} <span style="color:#a6e22e">context</span><span style="color:#f92672">=</span>{<span style="color:#a6e22e">context</span>}&gt;
</span></span><span style="display:flex;"><span>    &lt;<span style="color:#f92672">h1</span>&gt;
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">This</span> <span style="color:#a6e22e">is</span> <span style="color:#a6e22e">top</span> <span style="color:#a6e22e">secret</span> <span style="color:#66d9ef">for</span> <span style="color:#a6e22e">non</span> <span style="color:#a6e22e">admins</span>...
</span></span><span style="display:flex;"><span>    &lt;/<span style="color:#f92672">h1</span>&gt;
</span></span><span style="display:flex;"><span>&lt;/<span style="color:#f92672">TargetAudience</span>&gt;
</span></span></code></pre></div><h2 id="final-thoughts-">Final Thoughts 💭</h2>
<p>Could the logic be extended to check that only selected users can view the content, instead of it being limited to groups? Absolutely, this is entirely possible.</p>
<p>However, I&rsquo;ve chosen to focus on the group aspect because that&rsquo;s often what my solutions revolve around. This approach emphasizes the flexibility of SPFx solutions in catering to diverse requirements, allowing for both broad group-based targeting and the precision of user-specific content visibility.</p>
<p>It highlights the potential to tailor your SharePoint solutions even more closely to your organization&rsquo;s needs, ensuring that every piece of content reaches exactly the right audience.</p>
]]></content:encoded></item></channel></rss>