<?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>Sun, 16 Aug 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: Proxy It 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><guid>https://jeppe-spanggaard.dk/blogs/graph-sdk-authentication-csharp/</guid><description>Learn how to authenticate to Microsoft Graph in C# with ClientCertificateCredential, with working snippets for both the v4 SDK and the v5/v6 SDK.</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, Entra ID, 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>Reach for the <strong>Graph SDK</strong> when you need Teams, OneDrive, Exchange or Entra ID data, when you want the built-in retry and batching, or when the feature spans several Microsoft 365 services.</p>
<p>Reach for <strong>PnP.Framework</strong> when you need the SharePoint-specific surface: site templates, provisioning, taxonomy, search refiners, managed metadata, content types.</p>
<p>Graph won&rsquo;t help you with SharePoint classic features like master pages and web parts, its search is thinner than the SharePoint Search API, and it can&rsquo;t talk to SharePoint Server on-premises at all.</p>
<h2 id="which-sdk-version-are-you-on">Which SDK Version Are You On?</h2>
<p>This is the part that costs people an afternoon, so it comes before the code.</p>
<p>The .NET SDK was rewritten in <strong>v5</strong>. It moved to Kiota-generated clients, and <code>.Request()</code> was removed from every call. That one change means a v4 sample does not compile on v5, and a v5 sample does not compile on v4. Most of the samples you&rsquo;ll find online, including the first version of this post, are v4.</p>
<p><strong>v6 is the current major, and v5 code compiles on it unchanged.</strong> I checked: the same file builds clean against <code>5.105.0</code> and <code>6.5.0</code>. So there are really two dialects to care about, not three.</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><span style="color:#75715e"># current: v5 syntax, v6 package</span>
</span></span><span style="display:flex;"><span>dotnet add package Microsoft.Graph --version 6.5.0
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># legacy: only if you&#39;re pinned to v4 already</span>
</span></span><span style="display:flex;"><span>dotnet add package Microsoft.Graph --version 4.54.0
</span></span></code></pre></div><p>Pin it. <code>Install-Package Microsoft.Graph</code> with no version gives you the newest major, and if you then paste a v4 sample into it you get a wall of <code>does not contain a definition for 'Request'</code>.</p>
<p>One thing to get out of the way: <strong>there is no <code>Microsoft.Graph.Auth</code> package</strong>. It never had a stable release and the namespace doesn&rsquo;t exist in v4, v5 or v6. If a sample tells you to add <code>using Microsoft.Graph.Auth;</code>, that sample predates all of this. You want <code>Azure.Identity</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-bash" data-lang="bash"><span style="display:flex;"><span>dotnet add package Azure.Identity
</span></span></code></pre></div><h2 id="add-graph-permissions">Add Graph Permissions</h2>
<p>In your existing app registration, go to &ldquo;API permissions&rdquo; then &ldquo;Add a permission&rdquo; then &ldquo;Microsoft Graph&rdquo; then &ldquo;Application permissions&rdquo;:</p>
<ul>
<li><strong>Sites.ReadWrite.All</strong>: SharePoint sites and lists access</li>
</ul>
<p>Click &ldquo;Grant admin consent&rdquo; after adding permissions, or every call comes back 403.</p>
<h2 id="the-code-v5-and-v6">The Code, v5 and v6</h2>
<p>Same certificate and app registration as the <a href="https://jeppe-spanggaard.dk/blogs/pnp-framework-authentication-csharp/">PnP.Framework post</a>.</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> Azure.Identity;
</span></span><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.Models;
</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;&lt;tenant-id&gt;&#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;&lt;client-id&gt;&#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\appcert.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;&lt;password&gt;&#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">var</span> certificate = X509CertificateLoader.LoadPkcs12FromFile(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> credential = <span style="color:#66d9ef">new</span> ClientCertificateCredential(TenantId, ClientId, certificate, options);
</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(credential, scopes);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">var</span> org = <span style="color:#66d9ef">await</span> graphClient.Organization.GetAsync();
</span></span><span style="display:flex;"><span>        Console.WriteLine(<span style="color:#e6db74">$&#34;Connected to: {org?.Value?.FirstOrDefault()?.DisplayName}&#34;</span>);
</span></span><span style="display:flex;"><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>].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.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?.Value ?? [])
</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:#66d9ef">var</span> docLib = (lists?.Value ?? []).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.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]
</span></span><span style="display:flex;"><span>                .Items[<span style="color:#e6db74">&#34;root:/sample-document.txt:&#34;</span>]
</span></span><span style="display:flex;"><span>                .Content
</span></span><span style="display:flex;"><span>                .PutAsync(<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><p><strong>What&rsquo;s happening here?</strong></p>
<ol>
<li><code>ClientCertificateCredential</code> comes from <code>Azure.Identity</code>, not from Graph. It&rsquo;s a <code>TokenCredential</code>, and <code>GraphServiceClient</code> takes one directly. No auth provider, no handler, no wrapper.</li>
<li><code>.default</code> as the scope is what tells Entra ID to issue every application permission you consented to. You don&rsquo;t list individual scopes for app-only auth.</li>
<li>No <code>.Request()</code> anywhere. In v5 the request builder ends in the verb, so it&rsquo;s <code>.GetAsync()</code>, <code>.PutAsync()</code>, <code>.PostAsync()</code> straight off the path.</li>
<li>Collections come back wrapped. It&rsquo;s <code>lists.Value</code>, not <code>lists</code>, and everything is nullable, so the compiler will nag you until you handle it. That nagging is correct: <code>Sites[&quot;root&quot;]</code> really can hand you back a null.</li>
<li><code>X509CertificateLoader</code> is .NET 9 and later. On .NET 8 or earlier use <code>new X509Certificate2(path, password)</code>, which still works but is marked obsolete (<code>SYSLIB0057</code>) on newer targets.</li>
</ol>
<h2 id="the-same-code-v4">The Same Code, v4</h2>
<p>If you&rsquo;re pinned to <code>4.54.0</code>, the credential setup is identical and only the calls change.</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> Azure.Identity;
</span></span><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> System.Security.Cryptography.X509Certificates;
</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> 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> credential = <span style="color:#66d9ef">new</span> ClientCertificateCredential(TenantId, ClientId, certificate, options);
</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(credential, scopes);
</span></span><span style="display:flex;"><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:#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><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:#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></code></pre></div><p>Three differences worth naming, because they&rsquo;re the ones that break a paste:</p>
<table>
  <thead>
      <tr>
          <th></th>
          <th>v4</th>
          <th>v5 and v6</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Call shape</td>
          <td><code>.Request().GetAsync()</code></td>
          <td><code>.GetAsync()</code></td>
      </tr>
      <tr>
          <td>Collections</td>
          <td>iterate the result directly</td>
          <td>iterate <code>result.Value</code></td>
      </tr>
      <tr>
          <td>Upload path</td>
          <td><code>.Root.ItemWithPath(&quot;x&quot;)</code></td>
          <td><code>.Items[&quot;root:/x:&quot;]</code></td>
      </tr>
  </tbody>
</table>
<p><code>GraphServiceClient(credential, scopes)</code> is the same in both, which is the good news: the authentication half of this post doesn&rsquo;t change between versions. Only the calls do.</p>
<h2 id="key-differences-from-pnpframework">Key Differences from PnP.Framework</h2>
<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.Items[&quot;root:/name:&quot;].Content.PutAsync()</code></td>
      </tr>
      <tr>
          <td><strong>Authentication</strong></td>
          <td><code>AuthenticationManager</code></td>
          <td><code>ClientCertificateCredential</code></td>
      </tr>
  </tbody>
</table>
<p><strong>Graph advantages:</strong> access to Teams, OneDrive and Exchange data with the same client.
<strong>PnP advantages:</strong> more SharePoint-specific features like content types and site columns.</p>
<h2 id="troubleshooting">Troubleshooting</h2>
<ul>
<li><strong><code>The type or namespace name 'Auth' does not exist in the namespace 'Microsoft.Graph'</code></strong>: you copied a sample with <code>using Microsoft.Graph.Auth;</code>. Delete the line, add <code>Azure.Identity</code>.</li>
<li><strong><code>'OrganizationRequestBuilder' does not contain a definition for 'Request'</code></strong>: v4 code on a v5 or v6 package. Drop the <code>.Request()</code> calls.</li>
<li><strong><code>SYSLIB0057</code></strong>: the <code>X509Certificate2</code> constructor is obsolete on .NET 9 and later. Use <code>X509CertificateLoader.LoadPkcs12FromFile</code>.</li>
<li><strong>&ldquo;Insufficient privileges&rdquo;</strong>: permissions added but admin consent not granted.</li>
<li><strong>&ldquo;Certificate not found&rdquo;</strong>: check the path and password before you suspect anything cleverer.</li>
</ul>
<h2 id="next-steps">Next Steps</h2>
<p>You now have the Graph SDK working with your existing app registration, on whichever major version you&rsquo;re pinned to. To reach more of Microsoft 365, add the matching application permission:</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>If you&rsquo;re still on v4, the upgrade is mostly mechanical: delete every <code>.Request()</code>, add <code>.Value</code> where you iterate, and fix the upload path. The authentication code you just wrote carries over untouched.</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="what-does-pnpframework-actually-do-for-authentication">What does PnP.Framework actually do for authentication?</h2>
<p>It gets you a <code>ClientContext</code> without a human clicking a login prompt. That&rsquo;s the whole job. You hand
<code>PnP.Framework.AuthenticationManager</code> a client ID, a certificate and a tenant ID, and it deals with the
OAuth flow so you don&rsquo;t have to. Everything after that is ordinary CSOM.</p>
<p>Which is exactly what you want for a background service or a console app - anything that has to reach
SharePoint at three in the morning with nobody around to sign in. Here&rsquo;s the setup end to end.</p>
<p>If you arrived here because an app-only client secret suddenly stopped working: that&rsquo;s Azure ACS. It was
retired for SharePoint Online on 2 April 2026, with no extension, and certificate-based authentication
against a Microsoft Entra app registration is the replacement. That&rsquo;s what this post sets up - so this is
the destination, not a detour.</p>
<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>: <code>PnP.Framework.AuthenticationManager</code> handles the OAuth flow using the certificate. The overload used here takes <code>(clientId, certificate, tenantId)</code>; there are others for secrets, interactive login and loading the certificate from the store by thumbprint</li>
<li><strong>Context Creation</strong>: <code>GetContext(siteUrl)</code> 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>Certificate auth fails in a small number of ways, and the error messages are unhelpfully similar. Here&rsquo;s the triage order, cheapest check first.</p>
<p><strong><code>The remote server returned an error: (401) Unauthorized.</code></strong> (also seen as <code>Connect-PnPOnline : The remote server returned an error: (401) Unauthorized.</code>)</p>
<p>Work down this list in order - the first two account for most of them:</p>
<ol>
<li><strong>Admin consent was never granted.</strong> The permission is listed on the app registration but nobody approved it. Azure Portal shows this clearly: the permission needs to read &ldquo;Granted for [Your Organization]&rdquo;, not just be present.</li>
<li><strong>The tenant has custom app authentication disabled.</strong> This one is nasty because nothing about the error hints at tenant configuration - your app registration is perfect and it still 401s. The switch is <code>Set-SPOTenant -DisableCustomAppAuthentication $true</code> to block it, <code>$false</code> to allow it again.</li>
<li><strong>Wrong token audience.</strong> A Microsoft Graph token does not work against SharePoint&rsquo;s <code>/_api</code>, and vice versa. SharePoint wants <code>https://&lt;tenant&gt;.sharepoint.com/.default</code>.</li>
<li><strong>The certificate isn&rsquo;t actually associated with the app registration.</strong> Check you uploaded the <code>.cer</code> and that the thumbprint in the portal matches the certificate your code is loading.</li>
</ol>
<p><strong><code>Attempted to perform an unauthorized operation.</code></strong> and <strong><code>Access denied. You do not have permission to perform this action or access this resource.</code></strong></p>
<p>You authenticated fine - the token is good, the operation is not allowed. Usually the permission is narrower than the thing you&rsquo;re doing (<code>Sites.Read.All</code> when you need write), or the site is locked down beyond what tenant-level consent covers.</p>
<p><strong><code>Access is denied. (Exception from HRESULT: 0x80070005 (E_ACCESSDENIED))</code></strong></p>
<p>Same family, thrown from deeper in CSOM. Common on site-scoped operations and on template application, where one handler in the template needs a permission the rest of the run didn&rsquo;t.</p>
<p><strong><code>Microsoft.SharePoint.Client.ServerException: No User or App Context found</code></strong></p>
<p>The <code>ClientContext</code> was created without a working authentication path. Check that <code>GetContext</code> actually returned before you used it, and that the certificate loaded.</p>
<p><strong><code>The sign-in name or password does not match one in the Microsoft account system.</code></strong></p>
<p>This one lies. It usually has nothing to do with the password. It&rsquo;s what you get from <code>SharePointOnlineCredentials</code> when MFA is enabled, when Conditional Access blocks legacy authentication, or when you&rsquo;re on .NET Standard where that class doesn&rsquo;t exist at all. The fix isn&rsquo;t a better password, it&rsquo;s the app-only certificate setup on this page.</p>
<p><strong><code>Cryptography Next Generation (CNG) is not supported on this platform.</code></strong></p>
<p>The <code>.pfx</code> was generated by <code>New-SelfSignedCertificate</code> on Windows and is now being loaded somewhere that isn&rsquo;t - a Linux build agent, or a container. Regenerate it with OpenSSL for that platform.</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>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>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>Learn how to build an SPFx wrapper component that shows or hides content based on Microsoft 365 group membership, using getMemberGroups and a session cache.</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>