<?xml version="1.0" encoding="utf-8"?><rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/"><channel><language>en</language><title>Blog posts by Andreas Ylivainio OMVP</title> <link>https://world.optimizely.com/blogs/andreas-ylivainio-omvp/</link><description></description><ttl>60</ttl><generator>Optimizely World</generator><item> <title>Keycloak Authentication with Optimizely CMS 12 and .NET Aspire</title>            <link>https://world.optimizely.com/blogs/andreas-ylivainio-omvp/dates/2026/4/keycloak-authentication-with-optimizely-cms-12-and-.net-aspire</link>            <description>&lt;div class=&quot;highlight highlight-source-cs notranslate position-relative overflow-auto&quot;&gt;
&lt;p&gt;This post walks through how to wire together Optimizely CMS 12, Keycloak and .NET Aspire to give you a single local development environment that handles both interactive browser login and machine-to-machine (M2M) API calls from the same ASP.NET Core application.&lt;br /&gt;&lt;br /&gt;For context read my previous blog post here: &lt;a href=&quot;/link/dd8a24c82520401ca8bbe14ffbd3859d.aspx&quot;&gt;Using Scalar with Optimizely CMS&lt;/a&gt;&lt;/p&gt;
&lt;div class=&quot;markdown-heading&quot;&gt;
&lt;h3 class=&quot;heading-element&quot;&gt;The source code for this blog post is available here: &lt;a href=&quot;https://github.com/andreas-valtech/Aspire.OptimizelyContentDeliveryAuth/&quot;&gt;andreas-valtech/Aspire.OptimizelyContentDeliveryAuth&lt;/a&gt;&lt;/h3&gt;
&lt;h2 class=&quot;heading-element&quot;&gt;How this project came about&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;This started as an excuse to play with several things at once: Aspire, Keycloak, and OpenAI Codex &amp;mdash; all things I wanted to get familiar with. Rather than building isolated toy demos, I wanted to see how they fit together while a real project was taking shape.&lt;/p&gt;
&lt;p&gt;While I was at it,&amp;nbsp;&lt;a href=&quot;https://www.avantibit.com/blog/avantibit-alloy-aspire-scaffold&quot;&gt;Avantibit published a great post on combining Alloy with an Aspire scaffold&lt;/a&gt;. That post is worth reading &amp;mdash; it covers similar ground around Aspire and the Optimizely ecosystem. The difference here is that I was not specifically interested in DXP or CMS 13 for this scenario. My goal was narrower and more concrete: get CMS 12, SQL Server and Keycloak running in the same Aspire-orchestrated setup and make the authentication actually work end-to-end, both from a browser and from a machine client.&lt;/p&gt;
&lt;p&gt;OpenAI Codex helped shape the implementation and documentation as the project grew, which made it a practical experiment for that too.&lt;/p&gt;
&lt;div class=&quot;markdown-heading&quot;&gt;
&lt;h2 class=&quot;heading-element&quot;&gt;Why this combination?&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;Running Optimizely CMS 12, a SQL Server database, and an identity provider side by side used to mean a lot of manual setup &amp;mdash; standing up containers by hand, clicking through Keycloak&#39;s admin UI, and maintaining notes about which credentials to use. .NET Aspire solves the orchestration problem: a single&amp;nbsp;&lt;code&gt;dotnet run&lt;/code&gt;&amp;nbsp;starts everything and injects connection strings and service URLs into the web application automatically.&lt;/p&gt;
&lt;p&gt;Keycloak fills the identity layer because it is a battle-tested, self-hostable OpenID Connect (OIDC) provider. With realm imports it also lets you version-control the entire identity configuration alongside the rest of the code &amp;mdash; no clicking required after a fresh clone.&lt;/p&gt;
&lt;hr /&gt;
&lt;div class=&quot;markdown-heading&quot;&gt;
&lt;h2 class=&quot;heading-element&quot;&gt;Repository structure&lt;/h2&gt;
&lt;/div&gt;
&lt;div class=&quot;snippet-clipboard-content notranslate position-relative overflow-auto&quot;&gt;
&lt;pre class=&quot;notranslate&quot;&gt;&lt;code&gt;Aspire.OptimizelyContentDeliveryAuth.AppHost/   &amp;larr; Aspire orchestrator
  AppHost.cs                                    &amp;larr; wires together SQL Server, Keycloak and the web app
  realm-export.json                             &amp;larr; versioned Keycloak realm import
  openid-configuration.json                     &amp;larr; static OIDC discovery doc for local dev

Aspire.OptimizelyContentDeliveryAuth.Web/       &amp;larr; Optimizely CMS 12 application
  Program.cs                                    &amp;larr; minimal hosting entry point
  Startup.cs                                    &amp;larr; authentication and CMS configuration
  UserInformationController.cs                  &amp;larr; endpoints for login, logout and userinfo

login.http                                      &amp;larr; test file for interactive login flow
machine-to-machine.http                         &amp;larr; test file for client credentials flow
http-client.env.json                            &amp;larr; shared variables for the .http test files
&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;zeroclipboard-container&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;hr /&gt;
&lt;div class=&quot;markdown-heading&quot;&gt;
&lt;h2 class=&quot;heading-element&quot;&gt;Aspire orchestration&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;The AppHost registers SQL Server, Keycloak, and the web project and wires them together so Aspire can inject service URLs as environment variables at startup:&lt;/p&gt;
&lt;div class=&quot;highlight highlight-source-cs notranslate position-relative overflow-auto&quot;&gt;
&lt;pre&gt;&lt;span class=&quot;pl-c&quot;&gt;// Aspire.OptimizelyContentDeliveryAuth.AppHost/AppHost.cs&lt;/span&gt;

&lt;span class=&quot;pl-k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;pl-s1&quot;&gt;builder&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;pl-s1&quot;&gt;DistributedApplication&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-en&quot;&gt;CreateBuilder&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;pl-k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;pl-s1&quot;&gt;sqlServer&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;pl-s1&quot;&gt;builder&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-en&quot;&gt;AddSqlServer&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;pl-s&quot;&gt;&quot;sqlserver&quot;&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-en&quot;&gt;WithDataVolume&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;pl-s&quot;&gt;&quot;aspireoptimizelycontentdeliveryauth-sql&quot;&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-en&quot;&gt;WithLifetime&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;ContainerLifetime&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;Persistent&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;pl-k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;pl-s1&quot;&gt;cmsDb&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;pl-s1&quot;&gt;sqlServer&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-en&quot;&gt;AddDatabase&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;pl-s&quot;&gt;&quot;EPiServerDB&quot;&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;pl-k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;pl-s1&quot;&gt;keyCloakUsername&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;pl-s1&quot;&gt;builder&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-en&quot;&gt;AddParameter&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;pl-s&quot;&gt;&quot;username&quot;&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;pl-s1&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;pl-c1&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;pl-s&quot;&gt;&quot;admin&quot;&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;pl-k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;pl-s1&quot;&gt;keycloakPassword&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;pl-s1&quot;&gt;builder&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-en&quot;&gt;AddParameter&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;pl-s&quot;&gt;&quot;password&quot;&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;pl-s1&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;pl-c1&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;pl-s&quot;&gt;&quot;abc123&quot;&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;pl-s1&quot;&gt;secret&lt;/span&gt;&lt;span class=&quot;pl-c1&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;pl-k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;pl-s1&quot;&gt;keycloak&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;pl-s1&quot;&gt;builder&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-en&quot;&gt;AddKeycloak&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;pl-s&quot;&gt;&quot;keycloak&quot;&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;8080&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;pl-s1&quot;&gt;keyCloakUsername&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;pl-s1&quot;&gt;keycloakPassword&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-en&quot;&gt;WithLifetime&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;ContainerLifetime&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;Persistent&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-en&quot;&gt;WithDataVolume&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;pl-s&quot;&gt;&quot;aspireoptimizelycontentdeliveryauth-keycloak&quot;&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-en&quot;&gt;WithRealmImport&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;pl-s&quot;&gt;&quot;realm-export.json&quot;&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;pl-s1&quot;&gt;builder&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-smi&quot;&gt;AddProject&lt;/span&gt;&lt;span class=&quot;pl-c1&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;pl-smi&quot;&gt;Aspire_OptimizelyContentDeliveryAuth_Web&lt;/span&gt;&lt;span class=&quot;pl-c1&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;pl-s&quot;&gt;&quot;web&quot;&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-en&quot;&gt;WithReference&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;cmsDb&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-en&quot;&gt;WithReference&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;keycloak&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-en&quot;&gt;WaitFor&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;keycloak&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;pl-s1&quot;&gt;builder&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-en&quot;&gt;Build&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-en&quot;&gt;Run&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;;&lt;/span&gt;&lt;/pre&gt;
&lt;div class=&quot;zeroclipboard-container&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Key points:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;WithRealmImport(&quot;realm-export.json&quot;)&lt;/code&gt;&amp;nbsp;tells Keycloak to import the realm configuration on first start, so no manual admin UI work is needed.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;WaitFor(keycloak)&lt;/code&gt;&amp;nbsp;ensures the web application only starts once Keycloak is healthy and has finished importing the realm.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;WithDataVolume&lt;/code&gt;&amp;nbsp;gives both containers a named Docker volume, making the data survive&amp;nbsp;&lt;code&gt;dotnet run&lt;/code&gt;&amp;nbsp;restarts without re-initialisation.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;div class=&quot;markdown-heading&quot;&gt;
&lt;h2 class=&quot;heading-element&quot;&gt;Authentication configuration&lt;/h2&gt;
&lt;/div&gt;
&lt;div class=&quot;markdown-heading&quot;&gt;
&lt;h3 class=&quot;heading-element&quot;&gt;The challenge: supporting both browsers and API clients on the same endpoint&lt;/h3&gt;
&lt;/div&gt;
&lt;p&gt;A CMS application typically has browser users who log in interactively&amp;nbsp;&lt;em&gt;and&lt;/em&gt;&amp;nbsp;back-end services that call APIs with a bearer token. ASP.NET Core&#39;s authentication middleware uses a single&amp;nbsp;&lt;em&gt;default scheme&lt;/em&gt;, which makes it awkward to support both styles.&lt;/p&gt;
&lt;p&gt;The solution is a&amp;nbsp;&lt;em&gt;policy scheme&lt;/em&gt;&amp;nbsp;&amp;mdash; a lightweight scheme that inspects each request and forwards it to the correct handler:&lt;/p&gt;
&lt;div class=&quot;highlight highlight-source-cs notranslate position-relative overflow-auto&quot;&gt;
&lt;pre&gt;&lt;span class=&quot;pl-c&quot;&gt;// Aspire.OptimizelyContentDeliveryAuth.Web/Startup.cs&lt;/span&gt;

&lt;span class=&quot;pl-s1&quot;&gt;services&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-en&quot;&gt;AddAuthentication&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;(&lt;/span&gt;options &lt;span class=&quot;pl-c1&quot;&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;pl-kos&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;pl-s1&quot;&gt;options&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;DefaultScheme&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;pl-s&quot;&gt;&quot;smart&quot;&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;pl-s1&quot;&gt;options&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;DefaultAuthenticateScheme&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;pl-s&quot;&gt;&quot;smart&quot;&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;pl-s1&quot;&gt;options&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;DefaultChallengeScheme&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;pl-s1&quot;&gt;OpenIdConnectDefaults&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;AuthenticationScheme&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;pl-s1&quot;&gt;options&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;DefaultSignInScheme&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;pl-s1&quot;&gt;CookieAuthenticationDefaults&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;AuthenticationScheme&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;pl-s1&quot;&gt;options&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;DefaultSignOutScheme&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;pl-s1&quot;&gt;CookieAuthenticationDefaults&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;AuthenticationScheme&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;pl-kos&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-en&quot;&gt;AddPolicyScheme&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;pl-s&quot;&gt;&quot;smart&quot;&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;pl-s&quot;&gt;&quot;Bearer or cookie&quot;&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;,&lt;/span&gt; options &lt;span class=&quot;pl-c1&quot;&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;pl-kos&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;pl-s1&quot;&gt;options&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;ForwardDefaultSelector&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;=&lt;/span&gt; context &lt;span class=&quot;pl-c1&quot;&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;pl-kos&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;pl-k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;pl-s1&quot;&gt;authorizationHeader&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;pl-s1&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;Request&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;Headers&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;Authorization&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-en&quot;&gt;ToString&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;;&lt;/span&gt;
            &lt;span class=&quot;pl-k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;pl-s1&quot;&gt;authorizationHeader&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-en&quot;&gt;StartsWith&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;pl-s&quot;&gt;&quot;Bearer &quot;&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;pl-s1&quot;&gt;StringComparison&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;OrdinalIgnoreCase&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;)&lt;/span&gt;
                &lt;span class=&quot;pl-c1&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;pl-s1&quot;&gt;JwtBearerDefaults&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;AuthenticationScheme&lt;/span&gt;
                &lt;span class=&quot;pl-c1&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;pl-s1&quot;&gt;CookieAuthenticationDefaults&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;AuthenticationScheme&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;pl-kos&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;pl-kos&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;)&lt;/span&gt;&lt;/pre&gt;
&lt;div class=&quot;zeroclipboard-container&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;When a request arrives the selector checks for a&amp;nbsp;&lt;code&gt;Bearer&lt;/code&gt;&amp;nbsp;header:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Present&lt;/strong&gt;&amp;nbsp;&amp;rarr; the request is forwarded to the JWT Bearer handler.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Absent&lt;/strong&gt;&amp;nbsp;&amp;rarr; the request is forwarded to the Cookie handler, and if no valid cookie exists, the challenge triggers an OIDC redirect.&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&quot;markdown-heading&quot;&gt;
&lt;h3 class=&quot;heading-element&quot;&gt;JWT Bearer &amp;mdash; for machine-to-machine clients&lt;/h3&gt;
&lt;/div&gt;
&lt;div class=&quot;highlight highlight-source-cs notranslate position-relative overflow-auto&quot;&gt;
&lt;pre&gt;&lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;AddJwtBearer&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;JwtBearerDefaults&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;AuthenticationScheme&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;,&lt;/span&gt; options &lt;span class=&quot;pl-c1&quot;&gt;=&amp;gt;&lt;/span&gt;
&lt;span class=&quot;pl-kos&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;pl-s1&quot;&gt;options&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;SaveToken&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;pl-s1&quot;&gt;options&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;Authority&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;pl-s1&quot;&gt;authority&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;;&lt;/span&gt;               &lt;span class=&quot;pl-c&quot;&gt;// http://localhost:8080/realms/optimizely&lt;/span&gt;
    &lt;span class=&quot;pl-s1&quot;&gt;options&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;MetadataAddress&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;pl-s1&quot;&gt;metadataAddress&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;;&lt;/span&gt;  &lt;span class=&quot;pl-c&quot;&gt;// points to openid-configuration.json during local dev&lt;/span&gt;
    &lt;span class=&quot;pl-s1&quot;&gt;options&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;MapInboundClaims&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;pl-s1&quot;&gt;options&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;RequireHttpsMetadata&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;pl-s1&quot;&gt;options&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;TokenValidationParameters&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;pl-k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;pl-smi&quot;&gt;TokenValidationParameters&lt;/span&gt;
    &lt;span class=&quot;pl-kos&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;pl-s1&quot;&gt;ValidateAudience&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;pl-s1&quot;&gt;ValidateIssuer&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;pl-s1&quot;&gt;ValidIssuer&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;pl-s1&quot;&gt;authority&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;pl-s1&quot;&gt;ValidateLifetime&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;pl-s1&quot;&gt;NameClaimType&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;pl-s&quot;&gt;&quot;preferred_username&quot;&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;pl-s1&quot;&gt;RoleClaimType&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;pl-s&quot;&gt;&quot;roles&quot;&lt;/span&gt;
    &lt;span class=&quot;pl-kos&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;pl-kos&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;)&lt;/span&gt;&lt;/pre&gt;
&lt;div class=&quot;zeroclipboard-container&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&lt;code&gt;MetadataAddress&lt;/code&gt;&amp;nbsp;points at a static&amp;nbsp;&lt;code&gt;openid-configuration.json&lt;/code&gt;&amp;nbsp;file served locally instead of hitting Keycloak&#39;s own discovery endpoint. This makes local development more resilient to timing issues on container startup.&lt;/p&gt;
&lt;div class=&quot;markdown-heading&quot;&gt;
&lt;h3 class=&quot;heading-element&quot;&gt;Cookie + OIDC &amp;mdash; for interactive browser users&lt;/h3&gt;
&lt;/div&gt;
&lt;div class=&quot;highlight highlight-source-cs notranslate position-relative overflow-auto&quot;&gt;
&lt;pre&gt;&lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;AddCookie&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;CookieAuthenticationDefaults&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;AuthenticationScheme&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;,&lt;/span&gt; options &lt;span class=&quot;pl-c1&quot;&gt;=&amp;gt;&lt;/span&gt;
&lt;span class=&quot;pl-kos&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;pl-s1&quot;&gt;options&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;Cookie&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;HttpOnly&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;pl-s1&quot;&gt;options&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;Cookie&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;IsEssential&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;pl-s1&quot;&gt;options&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;Cookie&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;Name&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;pl-s&quot;&gt;&quot;Aspire.OptimizelyContentDeliveryAuth&quot;&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;pl-s1&quot;&gt;options&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;ExpireTimeSpan&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;pl-s1&quot;&gt;TimeSpan&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-en&quot;&gt;FromHours&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;pl-c1&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;pl-s1&quot;&gt;options&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;SlidingExpiration&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;pl-s1&quot;&gt;options&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;Cookie&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;SecurePolicy&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;pl-s1&quot;&gt;webHostingEnvironment&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-en&quot;&gt;IsDevelopment&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;pl-c1&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;pl-s1&quot;&gt;CookieSecurePolicy&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;None&lt;/span&gt;
        &lt;span class=&quot;pl-c1&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;pl-s1&quot;&gt;CookieSecurePolicy&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;Always&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;pl-s1&quot;&gt;options&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;Events&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;OnRedirectToAccessDenied&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;=&lt;/span&gt; ctx &lt;span class=&quot;pl-c1&quot;&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;pl-kos&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;pl-s1&quot;&gt;ctx&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;Response&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;StatusCode&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;pl-s1&quot;&gt;StatusCodes&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;Status403Forbidden&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;pl-k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;pl-s1&quot;&gt;Task&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;CompletedTask&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;pl-kos&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;pl-kos&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-en&quot;&gt;AddOpenIdConnect&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;OpenIdConnectDefaults&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;AuthenticationScheme&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;,&lt;/span&gt; options &lt;span class=&quot;pl-c1&quot;&gt;=&amp;gt;&lt;/span&gt;
&lt;span class=&quot;pl-kos&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;pl-s1&quot;&gt;options&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;Authority&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;pl-s1&quot;&gt;authority&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;pl-s1&quot;&gt;options&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;MetadataAddress&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;pl-s1&quot;&gt;metadataAddress&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;pl-s1&quot;&gt;options&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;ClientId&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;pl-s1&quot;&gt;interactiveClientId&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;;&lt;/span&gt;     &lt;span class=&quot;pl-c&quot;&gt;// &quot;optimizely-web&quot;&lt;/span&gt;
    &lt;span class=&quot;pl-s1&quot;&gt;options&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;ClientSecret&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;pl-s1&quot;&gt;interactiveClientSecret&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;pl-s1&quot;&gt;options&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;RequireHttpsMetadata&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;pl-s1&quot;&gt;options&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;ResponseType&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;pl-s&quot;&gt;&quot;code&quot;&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;pl-s1&quot;&gt;options&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;UsePkce&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;pl-s1&quot;&gt;options&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;SaveTokens&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;pl-s1&quot;&gt;options&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;GetClaimsFromUserInfoEndpoint&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;pl-s1&quot;&gt;options&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;MapInboundClaims&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;pl-s1&quot;&gt;options&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;CallbackPath&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;pl-s&quot;&gt;&quot;/signin-oidc&quot;&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;pl-s1&quot;&gt;options&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;SignedOutCallbackPath&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;pl-s&quot;&gt;&quot;/signout-callback-oidc&quot;&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;pl-s1&quot;&gt;options&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;Scope&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-en&quot;&gt;Clear&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;pl-s1&quot;&gt;options&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;Scope&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-en&quot;&gt;Add&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;pl-s&quot;&gt;&quot;openid&quot;&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;pl-s1&quot;&gt;options&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;Scope&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-en&quot;&gt;Add&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;pl-s&quot;&gt;&quot;profile&quot;&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;pl-s1&quot;&gt;options&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;Scope&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-en&quot;&gt;Add&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;pl-s&quot;&gt;&quot;email&quot;&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;pl-s1&quot;&gt;options&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;TokenValidationParameters&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;pl-k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;pl-smi&quot;&gt;TokenValidationParameters&lt;/span&gt;
    &lt;span class=&quot;pl-kos&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;pl-s1&quot;&gt;NameClaimType&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;pl-s&quot;&gt;&quot;preferred_username&quot;&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;pl-s1&quot;&gt;RoleClaimType&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;pl-s&quot;&gt;&quot;roles&quot;&lt;/span&gt;
    &lt;span class=&quot;pl-kos&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;pl-kos&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;;&lt;/span&gt;&lt;/pre&gt;
&lt;div class=&quot;zeroclipboard-container&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;PKCE (&lt;code&gt;UsePkce = true&lt;/code&gt;) is enabled to meet current security best practices for public and confidential clients alike.&amp;nbsp;&lt;code&gt;GetClaimsFromUserInfoEndpoint = true&lt;/code&gt;&amp;nbsp;ensures that profile claims (name, email) are populated even when they are not included in the ID token directly.&lt;/p&gt;
&lt;hr /&gt;
&lt;div class=&quot;markdown-heading&quot;&gt;
&lt;h2 class=&quot;heading-element&quot;&gt;The userinfo endpoint&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;Both authentication paths land on the same controller action &amp;mdash; the only thing that changes is whether the&amp;nbsp;&lt;code&gt;ClaimsPrincipal&lt;/code&gt;&amp;nbsp;was populated from a cookie session or from a validated bearer token:&lt;/p&gt;
&lt;div class=&quot;highlight highlight-source-cs notranslate position-relative overflow-auto&quot;&gt;
&lt;pre&gt;&lt;span class=&quot;pl-c&quot;&gt;// Aspire.OptimizelyContentDeliveryAuth.Web/UserInformationController.cs&lt;/span&gt;

&lt;span class=&quot;pl-kos&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;pl-c1&quot;&gt;ApiController&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;pl-k&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;pl-k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;pl-smi&quot;&gt;UserInformationController&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;pl-smi&quot;&gt;Controller&lt;/span&gt;
&lt;span class=&quot;pl-kos&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;pl-kos&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;pl-c1&quot;&gt;AllowAnonymous&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;]&lt;/span&gt;
    &lt;span class=&quot;pl-kos&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;pl-c1&quot;&gt;Route&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;pl-s&quot;&gt;&quot;login&quot;&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;]&lt;/span&gt;
    &lt;span class=&quot;pl-k&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;pl-s1&quot;&gt;IActionResult&lt;/span&gt; &lt;span class=&quot;pl-en&quot;&gt;Login&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;pl-c1&quot;&gt;FromQuery&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;pl-smi&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;pl-s1&quot;&gt;returnUrl&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;pl-s&quot;&gt;&quot;/userinfo&quot;&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;pl-kos&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;pl-k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;pl-s1&quot;&gt;Challenge&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;pl-k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;pl-smi&quot;&gt;AuthenticationProperties&lt;/span&gt; &lt;span class=&quot;pl-kos&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;pl-s1&quot;&gt;RedirectUri&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;pl-s1&quot;&gt;returnUrl&lt;/span&gt; &lt;span class=&quot;pl-kos&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;pl-s1&quot;&gt;OpenIdConnectDefaults&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;AuthenticationScheme&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;pl-kos&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;pl-kos&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;pl-c1&quot;&gt;Authorize&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;]&lt;/span&gt;
    &lt;span class=&quot;pl-kos&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;pl-c1&quot;&gt;HttpPost&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;pl-s&quot;&gt;&quot;logout&quot;&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;]&lt;/span&gt;
    &lt;span class=&quot;pl-k&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;pl-s1&quot;&gt;IActionResult&lt;/span&gt; &lt;span class=&quot;pl-en&quot;&gt;Logout&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;pl-kos&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;pl-k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;pl-s1&quot;&gt;SignOut&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;pl-k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;pl-smi&quot;&gt;AuthenticationProperties&lt;/span&gt; &lt;span class=&quot;pl-kos&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;pl-s1&quot;&gt;RedirectUri&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;pl-s&quot;&gt;&quot;/&quot;&lt;/span&gt; &lt;span class=&quot;pl-kos&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;pl-s1&quot;&gt;CookieAuthenticationDefaults&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;AuthenticationScheme&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;pl-s1&quot;&gt;OpenIdConnectDefaults&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;AuthenticationScheme&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;pl-kos&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;pl-kos&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;pl-c1&quot;&gt;Authorize&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;]&lt;/span&gt;
    &lt;span class=&quot;pl-kos&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;pl-c1&quot;&gt;Route&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;pl-s&quot;&gt;&quot;userinfo&quot;&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;]&lt;/span&gt;
    &lt;span class=&quot;pl-k&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;pl-s1&quot;&gt;IActionResult&lt;/span&gt; &lt;span class=&quot;pl-en&quot;&gt;Get&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;pl-kos&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;pl-k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;pl-k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;pl-smi&quot;&gt;JsonResult&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;pl-k&quot;&gt;new&lt;/span&gt;
        &lt;span class=&quot;pl-kos&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;pl-s1&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;Identity&lt;/span&gt;&lt;span class=&quot;pl-c1&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;Name&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;pl-s1&quot;&gt;AuthenticationType&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;pl-s1&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;Identity&lt;/span&gt;&lt;span class=&quot;pl-c1&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;AuthenticationType&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;pl-s1&quot;&gt;Claims&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;pl-s1&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;Claims&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-en&quot;&gt;Select&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;(&lt;/span&gt;c &lt;span class=&quot;pl-c1&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;pl-k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;pl-kos&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;pl-s1&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;Type&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;pl-s1&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;Value&lt;/span&gt; &lt;span class=&quot;pl-kos&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;pl-kos&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;pl-kos&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;pl-kos&quot;&gt;}&lt;/span&gt;&lt;/pre&gt;
&lt;div class=&quot;zeroclipboard-container&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;The&amp;nbsp;&lt;code&gt;[Authorize]&lt;/code&gt;&amp;nbsp;attribute on&amp;nbsp;&lt;code&gt;Get()&lt;/code&gt;&amp;nbsp;is enough &amp;mdash; the policy scheme handles routing to the right handler, and both eventually resolve a&amp;nbsp;&lt;code&gt;ClaimsPrincipal&lt;/code&gt;&amp;nbsp;that satisfies the authorization requirement.&lt;/p&gt;
&lt;hr /&gt;
&lt;div class=&quot;markdown-heading&quot;&gt;
&lt;h2 class=&quot;heading-element&quot;&gt;Interactive login flow&lt;/h2&gt;
&lt;/div&gt;
&lt;div class=&quot;markdown-heading&quot;&gt;
&lt;h3 class=&quot;heading-element&quot;&gt;How it works&lt;/h3&gt;
&lt;/div&gt;
&lt;ol&gt;
&lt;li&gt;The browser opens&amp;nbsp;&lt;code&gt;/login&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The&amp;nbsp;&lt;code&gt;Login&lt;/code&gt;&amp;nbsp;action calls&amp;nbsp;&lt;code&gt;Challenge&lt;/code&gt;&amp;nbsp;with the OIDC scheme, redirecting the browser to Keycloak&#39;s authorization endpoint.&lt;/li&gt;
&lt;li&gt;The user enters their credentials in Keycloak&#39;s login page.&lt;/li&gt;
&lt;li&gt;Keycloak redirects back to&amp;nbsp;&lt;code&gt;/signin-oidc&lt;/code&gt;&amp;nbsp;with an authorization code.&lt;/li&gt;
&lt;li&gt;The OIDC middleware exchanges the code for tokens, fetches user info, builds the&amp;nbsp;&lt;code&gt;ClaimsPrincipal&lt;/code&gt;&amp;nbsp;and issues a cookie.&lt;/li&gt;
&lt;li&gt;The browser is redirected to the original&amp;nbsp;&lt;code&gt;returnUrl&lt;/code&gt;&amp;nbsp;(defaults to&amp;nbsp;&lt;code&gt;/userinfo&lt;/code&gt;).&lt;/li&gt;
&lt;/ol&gt;
&lt;div class=&quot;markdown-heading&quot;&gt;
&lt;h3 class=&quot;heading-element&quot;&gt;Test it with&amp;nbsp;&lt;code&gt;login.http&lt;/code&gt;&lt;/h3&gt;
&lt;/div&gt;
&lt;div class=&quot;highlight highlight-source-httpspec notranslate position-relative overflow-auto&quot;&gt;
&lt;pre&gt;&lt;span class=&quot;pl-ii&quot;&gt;@env = dev&lt;/span&gt;

&lt;span class=&quot;pl-ii&quot;&gt;### Interactive login through the web app&lt;/span&gt;
&lt;span class=&quot;pl-ii&quot;&gt;# Open this request in a browser. The app will challenge with OIDC&lt;/span&gt;
&lt;span class=&quot;pl-ii&quot;&gt;# and then redirect back to /userinfo.&lt;/span&gt;
&lt;span class=&quot;pl-k&quot;&gt;GET&lt;/span&gt; {{&lt;span class=&quot;pl-ii&quot;&gt;web_base_url&lt;/span&gt;}}&lt;span class=&quot;pl-ii&quot;&gt;/login?returnUrl=%2Fuserinfo&lt;/span&gt;

&lt;span class=&quot;pl-ii&quot;&gt;### Read user information from the authenticated browser session&lt;/span&gt;
&lt;span class=&quot;pl-k&quot;&gt;GET&lt;/span&gt; {{&lt;span class=&quot;pl-ii&quot;&gt;web_base_url&lt;/span&gt;}}&lt;span class=&quot;pl-ii&quot;&gt;/userinfo&lt;/span&gt;&lt;/pre&gt;
&lt;div class=&quot;zeroclipboard-container&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Variables (&lt;code&gt;http-client.env.json&lt;/code&gt;):&lt;/p&gt;
&lt;div class=&quot;highlight highlight-source-json notranslate position-relative overflow-auto&quot;&gt;
&lt;pre&gt;{
  &lt;span class=&quot;pl-ent&quot;&gt;&quot;dev&quot;&lt;/span&gt;: {
    &lt;span class=&quot;pl-ent&quot;&gt;&quot;web_base_url&quot;&lt;/span&gt;: &lt;span class=&quot;pl-s&quot;&gt;&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;https://localhost:5000&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;
  }
}&lt;/pre&gt;
&lt;div class=&quot;zeroclipboard-container&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Demo credentials (local development only):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Username:&lt;/strong&gt;&amp;nbsp;&lt;code&gt;editor&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Password:&lt;/strong&gt;&amp;nbsp;&lt;code&gt;Passw0rd!&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A successful response from&amp;nbsp;&lt;code&gt;/userinfo&lt;/code&gt;&amp;nbsp;looks like:&lt;/p&gt;
&lt;div class=&quot;highlight highlight-source-json notranslate position-relative overflow-auto&quot;&gt;
&lt;pre&gt;{
  &lt;span class=&quot;pl-ent&quot;&gt;&quot;name&quot;&lt;/span&gt;: &lt;span class=&quot;pl-s&quot;&gt;&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;editor&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;,
  &lt;span class=&quot;pl-ent&quot;&gt;&quot;authenticationType&quot;&lt;/span&gt;: &lt;span class=&quot;pl-s&quot;&gt;&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;Cookies&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;,
  &lt;span class=&quot;pl-ent&quot;&gt;&quot;claims&quot;&lt;/span&gt;: [
    { &lt;span class=&quot;pl-ent&quot;&gt;&quot;type&quot;&lt;/span&gt;: &lt;span class=&quot;pl-s&quot;&gt;&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;preferred_username&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;, &lt;span class=&quot;pl-ent&quot;&gt;&quot;value&quot;&lt;/span&gt;: &lt;span class=&quot;pl-s&quot;&gt;&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;editor&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; },
    { &lt;span class=&quot;pl-ent&quot;&gt;&quot;type&quot;&lt;/span&gt;: &lt;span class=&quot;pl-s&quot;&gt;&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;email&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;, &lt;span class=&quot;pl-ent&quot;&gt;&quot;value&quot;&lt;/span&gt;: &lt;span class=&quot;pl-s&quot;&gt;&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;editor@example.com&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; }
  ]
}&lt;/pre&gt;
&lt;div class=&quot;zeroclipboard-container&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;hr /&gt;
&lt;div class=&quot;markdown-heading&quot;&gt;
&lt;h2 class=&quot;heading-element&quot;&gt;Machine-to-machine (client credentials) flow&lt;/h2&gt;
&lt;/div&gt;
&lt;div class=&quot;markdown-heading&quot;&gt;
&lt;h3 class=&quot;heading-element&quot;&gt;How it works&lt;/h3&gt;
&lt;/div&gt;
&lt;ol&gt;
&lt;li&gt;A service (or a developer testing with an HTTP client) requests an access token directly from Keycloak using the&amp;nbsp;&lt;code&gt;client_credentials&lt;/code&gt;&amp;nbsp;grant.&lt;/li&gt;
&lt;li&gt;Keycloak validates the client ID and secret and returns a JWT access token.&lt;/li&gt;
&lt;li&gt;The service calls&amp;nbsp;&lt;code&gt;GET /userinfo&lt;/code&gt;&amp;nbsp;with an&amp;nbsp;&lt;code&gt;Authorization: Bearer &amp;lt;token&amp;gt;&lt;/code&gt;&amp;nbsp;header.&lt;/li&gt;
&lt;li&gt;The policy scheme detects the&amp;nbsp;&lt;code&gt;Bearer&lt;/code&gt;&amp;nbsp;prefix and forwards authentication to the JWT Bearer handler.&lt;/li&gt;
&lt;li&gt;The handler validates the token signature, issuer and lifetime, then builds the&amp;nbsp;&lt;code&gt;ClaimsPrincipal&lt;/code&gt;&amp;nbsp;from the token&#39;s claims.&lt;/li&gt;
&lt;/ol&gt;
&lt;div class=&quot;markdown-heading&quot;&gt;
&lt;h3 class=&quot;heading-element&quot;&gt;Test it with&amp;nbsp;&lt;code&gt;machine-to-machine.http&lt;/code&gt;&lt;/h3&gt;
&lt;/div&gt;
&lt;div class=&quot;highlight highlight-source-httpspec notranslate position-relative overflow-auto&quot;&gt;
&lt;pre&gt;&lt;span class=&quot;pl-ii&quot;&gt;@env = dev&lt;/span&gt;

&lt;span class=&quot;pl-ii&quot;&gt;### Step 1 &amp;mdash; Request a client credentials token from Keycloak&lt;/span&gt;
&lt;span class=&quot;pl-k&quot;&gt;POST&lt;/span&gt; {{&lt;span class=&quot;pl-ii&quot;&gt;keycloak_base_url&lt;/span&gt;}}&lt;span class=&quot;pl-ii&quot;&gt;/token&lt;/span&gt;
&lt;span class=&quot;pl-s&quot;&gt;&lt;span class=&quot;pl-v&quot;&gt;Content-Type:&lt;/span&gt; application/x-www-form-urlencoded&lt;/span&gt;

&lt;span class=&quot;pl-ii&quot;&gt;grant_type=client_credentials&amp;amp;&lt;/span&gt;
&lt;span class=&quot;pl-ii&quot;&gt;client_id={{machine_client_id}}&amp;amp;&lt;/span&gt;
&lt;span class=&quot;pl-ii&quot;&gt;client_secret={{machine_client_secret}}&lt;/span&gt;

&lt;span class=&quot;pl-ii&quot;&gt;### Step 2 &amp;mdash; Call the userinfo endpoint with the bearer token&lt;/span&gt;
&lt;span class=&quot;pl-k&quot;&gt;GET&lt;/span&gt; {{&lt;span class=&quot;pl-ii&quot;&gt;web_base_url&lt;/span&gt;}}&lt;span class=&quot;pl-ii&quot;&gt;/userinfo&lt;/span&gt;
&lt;span class=&quot;pl-s&quot;&gt;&lt;span class=&quot;pl-v&quot;&gt;Authorization:&lt;/span&gt; Bearer {{access_token}}&lt;/span&gt;&lt;/pre&gt;
&lt;div class=&quot;zeroclipboard-container&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Variables (&lt;code&gt;http-client.env.json&lt;/code&gt;):&lt;/p&gt;
&lt;div class=&quot;highlight highlight-source-json notranslate position-relative overflow-auto&quot;&gt;
&lt;pre&gt;{
  &lt;span class=&quot;pl-ent&quot;&gt;&quot;dev&quot;&lt;/span&gt;: {
    &lt;span class=&quot;pl-ent&quot;&gt;&quot;web_base_url&quot;&lt;/span&gt;: &lt;span class=&quot;pl-s&quot;&gt;&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;https://localhost:5000&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;,
    &lt;span class=&quot;pl-ent&quot;&gt;&quot;keycloak_base_url&quot;&lt;/span&gt;: &lt;span class=&quot;pl-s&quot;&gt;&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;http://localhost:8080/realms/optimizely/protocol/openid-connect&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;,
    &lt;span class=&quot;pl-ent&quot;&gt;&quot;machine_client_id&quot;&lt;/span&gt;: &lt;span class=&quot;pl-s&quot;&gt;&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;optimizely-api&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;,
    &lt;span class=&quot;pl-ent&quot;&gt;&quot;machine_client_secret&quot;&lt;/span&gt;: &lt;span class=&quot;pl-s&quot;&gt;&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;&amp;lt;see realm-export.json&amp;gt;&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;
  }
}&lt;/pre&gt;
&lt;div class=&quot;zeroclipboard-container&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt;&amp;nbsp;If your HTTP client does not automatically capture&amp;nbsp;&lt;code&gt;access_token&lt;/code&gt;&amp;nbsp;from the first response, copy the token value manually into the&amp;nbsp;&lt;code&gt;Authorization&lt;/code&gt;&amp;nbsp;header of the second request.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;A successful response from&amp;nbsp;&lt;code&gt;/userinfo&lt;/code&gt;&amp;nbsp;looks like:&lt;/p&gt;
&lt;div class=&quot;highlight highlight-source-json notranslate position-relative overflow-auto&quot;&gt;
&lt;pre&gt;{
  &lt;span class=&quot;pl-ent&quot;&gt;&quot;name&quot;&lt;/span&gt;: &lt;span class=&quot;pl-s&quot;&gt;&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;service-account-optimizely-api&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;,
  &lt;span class=&quot;pl-ent&quot;&gt;&quot;authenticationType&quot;&lt;/span&gt;: &lt;span class=&quot;pl-s&quot;&gt;&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;Bearer&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;,
  &lt;span class=&quot;pl-ent&quot;&gt;&quot;claims&quot;&lt;/span&gt;: [
    { &lt;span class=&quot;pl-ent&quot;&gt;&quot;type&quot;&lt;/span&gt;: &lt;span class=&quot;pl-s&quot;&gt;&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;preferred_username&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;, &lt;span class=&quot;pl-ent&quot;&gt;&quot;value&quot;&lt;/span&gt;: &lt;span class=&quot;pl-s&quot;&gt;&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;service-account-optimizely-api&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; },
    { &lt;span class=&quot;pl-ent&quot;&gt;&quot;type&quot;&lt;/span&gt;: &lt;span class=&quot;pl-s&quot;&gt;&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;azp&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;, &lt;span class=&quot;pl-ent&quot;&gt;&quot;value&quot;&lt;/span&gt;: &lt;span class=&quot;pl-s&quot;&gt;&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;optimizely-api&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; }
  ]
}&lt;/pre&gt;
&lt;div class=&quot;zeroclipboard-container&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;hr /&gt;
&lt;div class=&quot;markdown-heading&quot;&gt;
&lt;h2 class=&quot;heading-element&quot;&gt;Keycloak realm configuration&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;The realm is fully version-controlled in&amp;nbsp;&lt;code&gt;realm-export.json&lt;/code&gt;. On first start, Keycloak imports this file automatically &amp;mdash; no admin UI setup is needed. The realm contains:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Resource&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;optimizely-web&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Public OIDC client&lt;/td&gt;
&lt;td&gt;Interactive browser login via PKCE authorization code flow&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;optimizely-api&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Confidential client&lt;/td&gt;
&lt;td&gt;Machine-to-machine token requests via client credentials grant&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;editor&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;User&lt;/td&gt;
&lt;td&gt;Demo account for testing interactive login locally&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class=&quot;markdown-heading&quot;&gt;
&lt;h3 class=&quot;heading-element&quot;&gt;&lt;code&gt;optimizely-web&lt;/code&gt;&amp;nbsp;&amp;mdash; public client&lt;/h3&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Grant type:&lt;/strong&gt;&amp;nbsp;Authorization code with PKCE&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Redirect URI:&lt;/strong&gt;&amp;nbsp;&lt;code&gt;https://localhost:5000/signin-oidc&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Post-logout redirect URI:&lt;/strong&gt;&amp;nbsp;&lt;code&gt;https://localhost:5000/signout-callback-oidc&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Web origins:&lt;/strong&gt;&amp;nbsp;&lt;code&gt;https://localhost:5000&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&quot;markdown-heading&quot;&gt;
&lt;h3 class=&quot;heading-element&quot;&gt;&lt;code&gt;optimizely-api&lt;/code&gt;&amp;nbsp;&amp;mdash; confidential client&lt;/h3&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Grant type:&lt;/strong&gt;&amp;nbsp;Client credentials&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Service accounts enabled:&lt;/strong&gt;&amp;nbsp;yes&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Secret:&lt;/strong&gt;&amp;nbsp;stored in&amp;nbsp;&lt;code&gt;realm-export.json&lt;/code&gt;&amp;nbsp;and referenced in&amp;nbsp;&lt;code&gt;http-client.env.json&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Security note:&lt;/strong&gt;&amp;nbsp;The credentials in&amp;nbsp;&lt;code&gt;realm-export.json&lt;/code&gt;,&amp;nbsp;&lt;code&gt;http-client.env.json&lt;/code&gt;&amp;nbsp;and&amp;nbsp;&lt;code&gt;AppHost.cs&lt;/code&gt;&amp;nbsp;are intentionally hardcoded for local development convenience. They exist only to make the setup reproducible on a fresh clone. Never use these values in a shared, public, or production-adjacent environment. Replace passwords and client secrets with strong, unique values if you expose the environment beyond your local machine.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;div class=&quot;markdown-heading&quot;&gt;
&lt;h2 class=&quot;heading-element&quot;&gt;Prerequisites&lt;/h2&gt;
&lt;/div&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Requirement&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;.NET 10 SDK&lt;/td&gt;
&lt;td&gt;Required by the&amp;nbsp;&lt;code&gt;AppHost&lt;/code&gt;&amp;nbsp;project and Aspire tooling. Aspire&#39;s orchestration APIs target .NET 10.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;.NET 8 SDK&lt;/td&gt;
&lt;td&gt;Required by the&amp;nbsp;&lt;code&gt;Web&lt;/code&gt;&amp;nbsp;project. Optimizely CMS 12 targets .NET 8, so both SDKs must be installed side by side.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Docker Desktop&lt;/td&gt;
&lt;td&gt;For SQL Server and Keycloak containers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ASP.NET Core HTTPS dev certificate&lt;/td&gt;
&lt;td&gt;Run&amp;nbsp;&lt;code&gt;dotnet dev-certs https --trust&lt;/code&gt;&amp;nbsp;if not already trusted&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class=&quot;markdown-heading&quot;&gt;
&lt;h2 class=&quot;heading-element&quot;&gt;Starting the solution&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;From the repository root:&lt;/p&gt;
&lt;div class=&quot;highlight highlight-source-powershell notranslate position-relative overflow-auto&quot;&gt;
&lt;pre&gt;dotnet run &lt;span class=&quot;pl-k&quot;&gt;--&lt;/span&gt;project .\Aspire.OptimizelyContentDeliveryAuth.AppHost&lt;/pre&gt;
&lt;div class=&quot;zeroclipboard-container&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Aspire starts SQL Server, Keycloak and the web application in the right order. The Aspire dashboard (&lt;a href=&quot;http://localhost:15888/&quot;&gt;http://localhost:15888&lt;/a&gt;&amp;nbsp;by default) shows the status of all resources and aggregates their logs.&lt;/p&gt;
&lt;hr /&gt;
&lt;div class=&quot;markdown-heading&quot;&gt;
&lt;h2 class=&quot;heading-element&quot;&gt;Troubleshooting&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Keycloak ignores the realm import&lt;/strong&gt;&amp;nbsp;Containers use persistent volumes, so stale state from a previous run may survive restarts. Remove the named Keycloak volume (&lt;code&gt;aspireoptimizelycontentdeliveryauth-keycloak&lt;/code&gt;) in Docker Desktop or via&amp;nbsp;&lt;code&gt;docker volume rm&lt;/code&gt;&amp;nbsp;and restart the AppHost.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Login redirects to the wrong URL&lt;/strong&gt;&amp;nbsp;Verify that the web application is running on&amp;nbsp;&lt;code&gt;https://localhost:5000&lt;/code&gt;&amp;nbsp;and that the&amp;nbsp;&lt;code&gt;optimizely-web&lt;/code&gt;&amp;nbsp;client in Keycloak still lists&amp;nbsp;&lt;code&gt;https://localhost:5000/signin-oidc&lt;/code&gt;&amp;nbsp;as an allowed redirect URI.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Bearer token is rejected&lt;/strong&gt;&amp;nbsp;Confirm that the token was issued by the&amp;nbsp;&lt;code&gt;optimizely&lt;/code&gt;&amp;nbsp;realm (check the&amp;nbsp;&lt;code&gt;iss&lt;/code&gt;&amp;nbsp;claim) and that you requested it using the&amp;nbsp;&lt;code&gt;optimizely-api&lt;/code&gt;&amp;nbsp;client with the&amp;nbsp;&lt;code&gt;client_credentials&lt;/code&gt; grant.&lt;/p&gt;
&lt;/div&gt;</description>            <guid>https://world.optimizely.com/blogs/andreas-ylivainio-omvp/dates/2026/4/keycloak-authentication-with-optimizely-cms-12-and-.net-aspire</guid>            <pubDate>Mon, 13 Apr 2026 07:35:45 GMT</pubDate>           <category>Blog post</category></item><item> <title>Using Scalar with Optimizely CMS</title>            <link>https://world.optimizely.com/blogs/andreas-ylivainio-omvp/dates/2026/2/using-scalar-with-optimizely-cms-in-.net-8</link>            <description>&lt;h3&gt;OpenAPI, Content Delivery API, and Modern API Documentation&lt;/h3&gt;
&lt;p&gt;Modern Optimizely CMS solutions are increasingly API-first. Whether you are building a headless frontend, integrating external services, or exposing internal platform APIs, having &lt;strong&gt;clear and discoverable API contracts&lt;/strong&gt; is essential. With .NET 8, this becomes even more relevant, as the way API documentation is handled has subtly but importantly changed.&lt;/p&gt;
&lt;p&gt;This article shows how to combine &lt;strong&gt;OpenAPI&lt;/strong&gt;, &lt;strong&gt;Scalar&lt;/strong&gt;, and &lt;strong&gt;Optimizely CMS&lt;/strong&gt; to create a clean, future-proof API documentation setup. We&amp;rsquo;ll look at custom APIs, Optimizely Content Delivery, and how everything fits together in a single developer-friendly UI.&lt;/p&gt;
&lt;p&gt;A complete working example is available here:&lt;br /&gt;&lt;a class=&quot;decorated-link&quot; title=&quot;Optimizely Scalar Content Delivery&quot; href=&quot;https://github.com/andreas-valtech/OptimizelyScalarContentDelivery/&quot;&gt;github.com/andreas-valtech/OptimizelyScalarContentDelivery&lt;/a&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Swagger UI, .NET 8, and What Actually Changed&lt;/h2&gt;
&lt;p&gt;When upgrading to .NET 8+, many teams notice that Swagger UI no longer appears automatically in new ASP.NET Core projects. This can feel like something has been removed, but in practice it reflects a deliberate architectural shift.&lt;/p&gt;
&lt;p&gt;In recent ASP.NET Core versions, Microsoft has separated &lt;strong&gt;OpenAPI document generation&lt;/strong&gt; from &lt;strong&gt;API documentation user interfaces&lt;/strong&gt;. The framework now focuses on producing a standards-compliant OpenAPI specification, while leaving the choice of UI entirely up to the application. Swagger UI is still fully supported via libraries such as Swashbuckle, but it is no longer assumed to be the default.&lt;/p&gt;
&lt;p&gt;This separation reinforces an important idea: &lt;strong&gt;OpenAPI is the contract, not the UI&lt;/strong&gt;. Once that contract exists, it can be rendered using Swagger UI, Scalar, or any other compatible tool. For Optimizely CMS solutions&amp;mdash;where custom APIs, content delivery, search, and external integrations often live side by side&amp;mdash;this flexibility is a clear advantage.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Generating OpenAPI in .NET 8&lt;/h2&gt;
&lt;p&gt;Swashbuckle remains the most common way to generate OpenAPI documents in ASP.NET Core.&lt;/p&gt;
&lt;p&gt;After adding the &lt;code&gt;Swashbuckle.AspNetCore&lt;/code&gt; package, you can configure OpenAPI generation directly in &lt;code&gt;Startup.cs&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Startup(IWebHostEnvironment webHostingEnvironment)
{
    public void ConfigureServices(IServiceCollection services)
    {&lt;br /&gt;        ...
        services.&lt;/code&gt;&lt;code&gt;AddCms();
        
        // Content Delivery API
        services.AddContentDeliveryApi(options =&amp;gt; {
            options.SiteDefinitionApiEnabled = true;
        });

        // Content Delivery Search API
        services.AddContentSearchApi(options =&amp;gt; {
            options.MaximumSearchResults = 10;
        });

        // Swashbuckle
        services.AddEndpointsApiExplorer();
        services.AddSwaggerGen();
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        ...
        
        app.UseSwagger();
        app.UseSwaggerUI();

        app.UseEndpoints(endpoints =&amp;gt;
        {
            endpoints.MapContent();
            
            // Maps both Content Delivery API and Commerce Content Delivery API
            endpoints.MapSwagger(&quot;/openapi/{documentName}.json&quot;, options =&amp;gt;
            {
                options.OpenApiVersion = OpenApiSpecVersion.OpenApi3_1;
            });
            
            endpoints.MapScalarApiReference(options =&amp;gt;
            {
                options.WithTitle(&quot;Alloy Example API&quot;);
            });
        });
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;At this stage, it is worth clarifying what is happening in the pipeline. The &lt;code&gt;MapSwagger()&lt;/code&gt; call comes from Microsoft&amp;rsquo;s OpenAPI support in ASP.NET Core and is responsible for exposing the generated OpenAPI document as JSON. It does not provide any user interface. Scalar is added separately and uses its &lt;code&gt;MapScalarApiReference()&lt;/code&gt; extension method to build an API documentation UI on top of that contract. This includes all API controllers defined in the application, as well as any external OpenAPI (swagger.json) documents added to the project, such as those provided by Optimizely or other services. In this way, ASP.NET Core owns the generation of the OpenAPI specification, while Scalar is responsible for presenting both internal and external APIs in a single, unified view.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Scalar as the API Documentation UI&lt;/h2&gt;
&lt;p&gt;Scalar is a modern OpenAPI UI that consumes the same OpenAPI JSON but presents it in a cleaner, faster, and more developer-focused way. Because Scalar only depends on the OpenAPI specification, it fits perfectly with the direction ASP.NET Core has taken.&lt;br /&gt;&lt;br /&gt;To set up scalar you can follow their getting started guide for ASP.NET Core here: &lt;a href=&quot;https://scalar.com/products/api-references/integrations/aspnetcore/integration&quot;&gt;https://scalar.com/products/api-references/integrations/aspnetcore/integration&lt;/a&gt; or use my linked GitHub project for reference.&lt;/p&gt;
&lt;p&gt;Once Scalar is wired up, it automatically renders everything described in your OpenAPI document, including custom controllers, schemas, and authentication definitions.&lt;/p&gt;
&lt;p&gt;To confirm everything works, a simple controller is enough:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
[ApiController] 
[Route(&quot;api/hello&quot;)]
public class HelloController : ControllerBase
{ 
    [HttpGet]
    public IActionResult Get() =&amp;gt; Ok(new { message = &quot;Hello from Optimizely + Scalar&quot; });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;br /&gt;This endpoint is auto-discovered, included in the OpenAPI document, and immediately visible in Scalar without any additional configuration.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Bringing in Optimizely Content Delivery API&lt;/h2&gt;
&lt;p&gt;Optimizely provides official OpenAPI (swagger.json) definitions for its Content Delivery APIs. These can be downloaded from the Optimizely developer documentation:&lt;/p&gt;
&lt;p&gt;&lt;a class=&quot;decorated-link&quot; href=&quot;https://docs.developers.optimizely.com/content-management-system/v1.5.0-content-delivery-api/reference/content-delivery-class-libraries-and-apis&quot;&gt;https://docs.developers.optimizely.com/content-management-system/v1.5.0-content-delivery-api/reference/content-delivery-class-libraries-and-apis&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;By adding these JSON files directly to your project and serving them as static OpenAPI documents, Scalar can display Optimizely&amp;rsquo;s APIs alongside your own. This creates a single API surface where frontend and integration developers can explore both custom endpoints and content delivery endpoints in one place.&lt;/p&gt;
&lt;p&gt;The Content Delivery API is central to headless Optimizely solutions, exposing pages, blocks, and content structures as JSON. Documenting it explicitly makes content contracts clearer and reduces friction between backend and frontend teams.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Content Management and Content Search APIs&lt;/h2&gt;
&lt;p&gt;In addition to content delivery, Optimizely&amp;rsquo;s Content Management and Content Search APIs are often part of larger integrations and automation workflows. Including their OpenAPI definitions in the same documentation UI improves internal developer experience and makes search queries, filters, and response structures easier to reason about.&lt;/p&gt;
&lt;p&gt;Scalar handles multiple OpenAPI documents well, which makes it suitable as a lightweight internal API portal for Optimizely-based platforms.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;External APIs and a Unified API View&lt;/h2&gt;
&lt;p&gt;Many Optimizely solutions integrate with external services such as commerce platforms, DAM systems, or personalization engines. When those services provide OpenAPI definitions, they can be included alongside Optimizely and custom APIs, giving teams a single, consistent documentation experience.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Example Repository&lt;/h2&gt;
&lt;p&gt;All of this is demonstrated in a runnable .NET 8 example here:&lt;br /&gt;&lt;a class=&quot;decorated-link&quot; href=&quot;https://github.com/andreas-valtech/OptimizelyScalarContentDelivery/&quot;&gt;https://github.com/andreas-valtech/OptimizelyScalarContentDelivery/&lt;/a&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;What&amp;rsquo;s Next: Authentication with Keycloak&lt;/h2&gt;
&lt;p&gt;This article focuses on unauthenticated APIs. In the next part of the series, we&amp;rsquo;ll introduce authentication using &lt;strong&gt;Keycloak&lt;/strong&gt;, covering OAuth 2.0 and OpenID Connect, securing both custom APIs and Optimizely Content Delivery endpoints, and documenting authentication flows in OpenAPI and Scalar.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Closing Thoughts&lt;/h2&gt;
&lt;p&gt;By treating OpenAPI as the primary contract and using a modern UI like Scalar, Optimizely CMS solutions align naturally with the direction of ASP.NET Core and modern headless architectures. The result is clearer APIs, better developer experience, and a setup that scales well as platforms grow more composable.&lt;/p&gt;</description>            <guid>https://world.optimizely.com/blogs/andreas-ylivainio-omvp/dates/2026/2/using-scalar-with-optimizely-cms-in-.net-8</guid>            <pubDate>Fri, 06 Feb 2026 09:59:32 GMT</pubDate>           <category>Blog post</category></item></channel>
</rss>