<p>We have a .NET 5 (Blazor Server) app running in Azure Kubernetes that uses OpenID Connect to authenticate with a 3rd party.  The app is running behind Ingress.  Ingress uses https.  The app is only http.  After we authenticate with OIDC and get redirected back to /signin-oidc, we get a .NET error that we haven't been able to solve.</p>
<pre><code>warn: Microsoft.AspNetCore.Http.ResponseCookies[1]
      The cookie '.AspNetCore.OpenIdConnect.Nonce.CfDJ8EYehvsxFBVNtGDsitGDhE8K9FHQZVQwqqr1YO-zVntEtRgpfb_0cHpxfZp77AdGnS35iGRKYV54DTgx2O6ZO_3gq98pbP_XcbHnJmBDtZg2g5hhPakTrRirxDb-Qab0diaLMFKdmDrNTqGkVmqiGWpQkSxcnmxzVGGE0Cg_l930hk6TYgU0qmkzSO9WS16UBOYiub32GF4I9_qPwIiYlCq5dMTtUJaMxGlo8AdAqknxTzYz4UsrrPBi_RiWUKaF6heQitbOD4V-auHmdXQm4LE' has set 'SameSite=None' and must also set 'Secure'.
warn: Microsoft.AspNetCore.Http.ResponseCookies[1]
      The cookie '.AspNetCore.Correlation.MMrYZ2WKyYiV4hMC6bhQbGZozpubcF2tYsKq748YH44' has set 'SameSite=None' and must also set 'Secure'.
warn: Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler[15]
      '.AspNetCore.Correlation.MMrYZ2WKyYiV4hMC6bhQbGZozpubcF2tYsKq748YH44' cookie not found.
fail: Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware[1]
      An unhandled exception has occurred while executing the request.
      System.Exception: An error was encountered while handling the remote login.
       ---&gt; System.Exception: Correlation failed.
         --- End of inner exception stack trace ---
         at Microsoft.AspNetCore.Authentication.RemoteAuthenticationHandler`1.HandleRequestAsync()
         at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
         at Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware.&lt;Invoke&gt;g__Awaited|6_0(ExceptionHandlerMiddleware middleware, HttpContext context, Task task)
</code></pre>
<hr />
<pre><code>public class Startup
{
    private static readonly object refreshLock = new object();
    private IConfiguration Configuration;
    private readonly IWebHostEnvironment Env;

    public Startup(IConfiguration configuration, IWebHostEnvironment env)
    {
        Console.WriteLine($&quot;LogQAApp Version: {Assembly.GetExecutingAssembly().GetName().Version}&quot;);
        // We apparently need to set a CultureInfo or some of the Razor pages dealing with DateTimes, like LogErrorCountByTime fails with JavaScript errors.
        // I wanted to set it to CultureInvariant, but that wouldn't take.  Didn't complain, but wouldn't actually set it.
        CultureInfo.DefaultThreadCurrentCulture = new CultureInfo(&quot;en-US&quot;);
        CultureInfo.DefaultThreadCurrentUICulture = new CultureInfo(&quot;en-US&quot;);
        Configuration = configuration;
        Env = env;
    }

    // This method gets called by the runtime. Use this method to add services to the container.
    // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
    public void ConfigureServices(IServiceCollection services)
    {
        services.Configure&lt;ForwardedHeadersOptions&gt;(options =&gt;
        {
            options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
        });
        Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);  // Needed for 1252 code page encoding.
        Syncfusion.Licensing.SyncfusionLicenseProvider.RegisterLicense(&quot;&quot;);
        services.AddSignalR(e =&gt;
        {
            e.MaximumReceiveMessageSize = 102400000;
        });
        services.AddBlazoredSessionStorage();
        services.AddCors();
        services.AddSyncfusionBlazor();
        services.AddRazorPages();
        services.AddServerSideBlazor();
        services.AddHttpContextAccessor();

        ServiceConfigurations.LoadFromConfiguration(Configuration);

        #region Authentication


        services.AddAuthentication(options =&gt;
        {
            options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
            options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
        })
        .AddCookie(
            options =&gt;
            {
                options.Events = GetCookieAuthenticationEvents();
            }
        )
        .AddOpenIdConnect(&quot;SlbOIDC&quot;, options =&gt;
        {
            options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
            options.Authority = Configuration[&quot;SlbOIDC:Authority&quot;];
            if (Env.IsDevelopment())
            {
                options.ClientId = Configuration[&quot;SlbOIDC:ClientID&quot;];
                options.ClientSecret = Configuration[&quot;SlbOIDC:ClientSecret&quot;];
            }
            else
            {
                options.ClientId = Configuration.GetValue&lt;string&gt;(&quot;slbclientid&quot;);
                options.ClientSecret = Configuration.GetValue&lt;string&gt;(&quot;slbclientsecret&quot;);
            }
            options.ResponseType = OpenIdConnectResponseType.Code;
            options.UsePkce = true;
            options.SaveTokens = true;
            options.ClaimsIssuer = &quot;SlbOIDC&quot;;
            // Azure is communicating to us over http, but we need to tell SLB to respond back to us on https.
            options.Events = new OpenIdConnectEvents()
            {
                OnRedirectToIdentityProvider = context =&gt;
                {
                    Console.WriteLine($&quot;Before: {context.ProtocolMessage.RedirectUri}&quot;);
                    context.ProtocolMessage.RedirectUri = context.ProtocolMessage.RedirectUri.Replace(&quot;http://&quot;, &quot;https://&quot;);
                    Console.WriteLine($&quot;After: {context.ProtocolMessage.RedirectUri}&quot;);
                    return Task.FromResult(0);
                }
            };
        });

        services.AddSession(options =&gt;
        {
            options.Cookie.SameSite = SameSiteMode.None;
            options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
            options.Cookie.IsEssential = true;
        });
        #endregion

        services.AddScoped&lt;BrowserService&gt;();
        services.AddSingleton&lt;ConcurrentSessionStatesSingleton&gt;();
        services.AddSingleton&lt;URLConfiguration&gt;();
        services.AddScoped&lt;CircuitHandler&gt;((sp) =&gt; new CircuitHandlerScoped(sp.GetRequiredService&lt;ConcurrentSessionStatesSingleton&gt;(), sp.GetRequiredService&lt;BrowserService&gt;(), sp.GetRequiredService&lt;IJSRuntime&gt;()));
        services.AddScoped&lt;SessionServiceScoped&gt;();
        services.AddScoped&lt;LogEditorScoped&gt;();
        services.AddSingleton&lt;ModalService&gt;();
        services.AddFlexor();
        services.AddScoped&lt;ResizeListener&gt;();
        services.AddScoped&lt;ApplicationLogSingleton&gt;();
        services.AddScoped&lt;LogChartsSingleton&gt;();
        services.AddScoped&lt;CurveNameClassificationSingleton&gt;();
        services.AddScoped&lt;HubClientSingleton&gt;();
        services.AddScoped((sp) =&gt; new LogAquisitionScopedService(
            sp.GetRequiredService&lt;URLConfiguration&gt;(),
            sp.GetRequiredService&lt;HubClientSingleton&gt;(),
            sp.GetRequiredService&lt;ApplicationLogSingleton&gt;(),
            sp.GetRequiredService&lt;IConfiguration&gt;(),
            sp.GetRequiredService&lt;SessionServiceScoped&gt;(),
            sp.GetRequiredService&lt;AuthenticationStateProvider&gt;(),
            sp.GetRequiredService&lt;IHttpContextAccessor&gt;(),
            sp.GetRequiredService&lt;IJSRuntime&gt;()
            )
        );
        services.AddScoped&lt;UnitSingleton&gt;();
        services.AddServerSideBlazor().AddCircuitOptions(options =&gt; { options.DetailedErrors = true; });
        services.AddScoped&lt;TimeZoneService&gt;();
        services.AddHostedService&lt;ExcelBackgroundService&gt;();
        services.AddHostedService&lt;LogEditorBackgroundService&gt;();
    }

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        else
        {
            app.UseExceptionHandler(&quot;/Error&quot;);
            //app.UseHsts();
        }

        //app.UseHttpsRedirection();
        app.UseStaticFiles();
        app.UseRouting();
        app.UseCors();
        app.UseAuthorization();
        app.UseCookiePolicy();
        app.UseForwardedHeaders(new ForwardedHeadersOptions
        {
            ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
        });
        app.UseAuthentication();

        if (!Env.IsDevelopment())
        {
            app.UseTrafficManager();
        }

        app.UseEndpoints(endpoints =&gt;
        {
            endpoints.MapDefaultControllerRoute();
            endpoints.MapBlazorHub();
            endpoints.MapFallbackToPage(&quot;/_Host&quot;);
        });
    }

    private CookieAuthenticationEvents GetCookieAuthenticationEvents()
    {
        return new CookieAuthenticationEvents()
        {
            OnValidatePrincipal = context =&gt;
            {
                lock (refreshLock)
                {
                    if (context.Properties.Items.ContainsKey(&quot;.Token.expires_at&quot;))
                    {
                        DateTime expire = DateTime.Parse(context.Properties.Items[&quot;.Token.expires_at&quot;]);
                        if (expire.AddMinutes(-20) &lt; DateTime.Now)
                        {
                        try
                            {
                                CloudAuthentication cloudAuthentication = new CloudAuthentication();
                                TokenResponse tokenResponse = cloudAuthentication.GetRefreshToken(context.Properties.Items[&quot;.Token.refresh_token&quot;]);

                                context.Properties.Items[&quot;.Token.access_token&quot;] = tokenResponse.access_token;
                                context.Properties.Items[&quot;.Token.refresh_token&quot;] = tokenResponse.refresh_token;
                                context.Properties.Items[&quot;.Token.expires_at&quot;] = DateTime.Now.AddSeconds(tokenResponse.expires_in).ToString();

                                context.ShouldRenew = true;
                            }
                            catch (Exception ex)
                            {
                                context.RejectPrincipal();
                            }
                        }
                    }
                    return Task.FromResult(0);
                }
            }
        };
    }
}
</code></pre>

Jeff Huber
<p>It's a good question - there are a couple of interesting points here that I've expanded on since they are related to SameSite cookies.</p>
<p><strong>REVERSE PROXY SETUP</strong></p>
<p>By default the Microsoft stack <a href="https://learn.microsoft.com/en-us/aspnet/core/security/samesite/rp31?view=aspnetcore-3.1#sampleCode" rel="nofollow noreferrer">requires you to run on HTTPS</a> if using cookies that require an SSL connection. However, you are providing SSL via a Kubernetes ingress, which is a form of reverse proxy.</p>
<p>The <a href="https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/proxy-load-balancer?view=aspnetcore-5.0#scenarios-and-use-cases" rel="nofollow noreferrer">Microsoft .Net Core Reverse Proxy Docs</a> may provide a solution. The doc suggests that you can inform the runtime that there is an SSL context, even though you are listening on HTTP:</p>
<pre class="lang-cs prettyprint-override"><code>app.Use((context, next) =&gt;
{
    context.Request.Scheme = &quot;https&quot;;
    return next();
});
</code></pre>
<p>I would be surprised if Microsoft did not support your setup, since it is a pretty mainstream hosting option. If this does not work then you can try:</p>
<ul>
<li>Further searching around Blazor and 'reverse proxy hosting'</li>
<li>Worst case you may have to use SSL inside the cluster for this particular component, as Johan indicates</li>
</ul>
<p><strong>WIDER INFO - API DRIVEN OAUTH</strong></p>
<p>Many companies want to develop Single Page Apps, but use a website based back end in order to manage the OAuth security. Combining serving of web content with OAuth security adds complexity. It is often not understood that the OAuth SPA security works better if developed in an API driven manner.</p>
<p>The below resources show how the SPA code can be simplified and in this example the API will issue cookies however it is configured. This would enable it to listen over HTTP inside the cluster (if needed) but to also issue secure cookies:</p>
<ul>
<li><a href="https://github.com/curityio/web-oauth-via-bff/blob/main/code/spa/src/oauth/oauthClient.ts" rel="nofollow noreferrer">API driven OpenID Connect code</a></li>
<li><a href="https://curity.io/resources/learn/back-end-for-front-end-example/" rel="nofollow noreferrer">Curity Blog Post</a></li>
</ul>
<p><strong>WIDER INFO: SAMESITE COOKIES</strong></p>
<p>It is recommended to use <code>SameSite=strict</code> as the most secure option, rather than <code>SameSite=none</code>. There are sometimes usability problems with the strict option however, which can cause cookies to be dropped after redirects or navigation from email links.</p>
<p>This can result in companies downgrading their web security to a less secure SameSite option. These problems do not occur when an API driven solution is used, and you can then use the strongest <code>SameSite=strict</code> option.</p>
