Fixing Blazor Server OIDC State Null Error with SameSite Cookies

Summary

A Blazor Server application utilizing Okta via OpenID Connect (OIDC) failed during the authentication callback phase. Despite correct configuration of HTTPS, client IDs, and client secrets, the application threw the error OpenIdConnectAuthenticationHandler: message.State is null or empty. This error prevents the application from completing the handshake, effectively locking users out of the system after they successfully authenticate with the Identity Provider (IdP).

Root Cause

The failure is caused by a state mismatch and cookie loss during the OIDC redirection flow. Specifically:

  • The OIDC Protocol Flow: When a user initiates authentication, the middleware generates a state parameter and stores it in a temporary correlation cookie to prevent Cross-Site Request Forgery (CSRF).
  • SameSite Cookie Restrictions: When the Identity Provider (Okta) redirects the user back to the application via a POST request (due to ResponseMode = FormPost), modern browsers apply strict SameSite policies.
  • The Conflict: Even though the developer attempted to set SameSiteMode.None, the middleware’s internal handling of the Correlation Cookie and the Nonce Cookie failed to persist across the cross-site redirect. If the browser refuses to send the correlation cookie back during the callback, the middleware cannot find the state it previously saved, leading to the “State is null or empty” exception.

Why This Happens in Real Systems

In distributed or highly secure environments, this is a classic “Cross-Site Redirect” problem:

  • Browser Evolution: Browsers (Chrome, Edge, Safari) have moved toward SameSite=Lax by default. Any authentication flow that relies on a POST callback from a third-party domain (the IdP) will strip cookies unless they are explicitly marked as SameSite=None and Secure.
  • Middleware Lifecycle: The OIDC middleware is highly sensitive to the availability of the Correlation Cookie. If the cookie is dropped due to site isolation or privacy settings, the security handshake is broken by design to prevent hijacking.
  • Blazor Server Architecture: Since Blazor Server relies heavily on a persistent SignalR connection and specific HTTP context initialization, any interruption in the initial HTTP authentication handshake prevents the circuit from ever establishing an authenticated state.

Real-World Impact

  • Authentication Deadlocks: Users are stuck in a loop where they log in successfully on the IdP page, are redirected back, but immediately see an error page.
  • Broken User Experience: The application appears functional but is effectively useless for any secured resource.
  • Production Outages: This often surfaces only when moving from local development (HTTP/Localhost) to production (HTTPS/Real Domains), making it a high-risk deployment failure.

Example or Code

The following snippet demonstrates the correct way to configure the OpenIdConnectOptions to ensure cookies are accessible during the cross-site POST callback.

builder.Services.AddAuthentication(options =>
{
    options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie(options =>
{
    options.Cookie.SameSite = SameSiteMode.None;
    options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
})
.AddOpenIdConnect(options =>
{
    options.Authority = builder.Configuration["Okta:Issuer"];
    options.ClientId = builder.Configuration["Okta:ClientId"];
    options.ClientSecret = builder.Configuration["Okta:ClientSecret"];
    options.ResponseType = "code";
    options.ResponseMode = OpenIdConnectResponseMode.FormPost;

    // CRITICAL: Ensure correlation and nonce cookies are not blocked by SameSite policies
    options.CorrelationCookie.SameSite = SameSiteMode.None;
    options.CorrelationCookie.SecurePolicy = CookieSecurePolicy.Always;
    options.NonceCookie.SameSite = SameSiteMode.None;
    options.NonceCookie.SecurePolicy = CookieSecurePolicy.Always;

    options.SaveTokens = true;
    options.UsePkce = true;
});

How Senior Engineers Fix It

A senior engineer approaches this by looking at the entire transport layer, not just the application code:

  1. Audit the Cookie Policy: Ensure app.UseCookiePolicy() is called before app.UseAuthentication().
  2. Synchronize SameSite Settings: It is not enough to set the OIDC options; the global CookiePolicyOptions must also allow SameSiteMode.None to prevent the middleware from overriding settings.
  3. Verify Response Mode: If FormPost continues to cause issues due to aggressive browser policies, a senior engineer might evaluate switching to ResponseMode.Query (if the IdP supports it) to use a GET request, which is more lenient with SameSite=Lax cookies.
  4. Inspect Network Headers: Use Browser DevTools to verify if the Set-Cookie header for .AspNetCore.Correlation... actually includes SameSite=None; Secure. If it doesn’t, the issue is in the middleware configuration.

Why Juniors Miss It

  • Focus on Logic, Not Transport: Juniors often focus on the “logic” of the Okta configuration (ClientIDs, Scopes) while ignoring the “transport” (how cookies travel between domains).
  • Localhost Fallacy: The issue often doesn’t appear on localhost because browsers are more lenient with cookies on non-production domains, leading to a “it works on my machine” scenario.
  • Incomplete Configuration: They might set SameSiteMode.None on the main Authentication Cookie but forget to apply it to the Correlation and Nonce cookies, which are managed internally by the OIDC handler.

Leave a Comment