Search code examples
c#asp.net-corecookiesopenid-connect.net-8.0

.NET OpenIdConnectHandler - message.State is null or empty


I am trying to enabled OpenIdConnect and Cookies in a .NET 8 web API:

builder.Services
    .AddAuthentication(options =>
    {
        options.DefaultChallengeScheme = "MyPolicy";
        options.DefaultAuthenticateScheme = "MyPolicy";
    })
    .AddCookie(options =>
    {
        // add an instance of the patched manager to the options:
        options.CookieManager = new ChunkingCookieManager();
        options.Cookie.HttpOnly = true;
        options.Cookie.SameSite = SameSiteMode.None;
        options.Cookie.SecurePolicy = CookieSecurePolicy.None;
    })
    .AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options =>
    {
        options.Authority = "";
        options.ClientId = "";
        options.ClientSecret = "";
        options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        options.ResponseType = "code";
        options.Prompt = "login";
        options.GetClaimsFromUserInfoEndpoint = true;
        options.SaveTokens = true;
        options.RequireHttpsMetadata = false;
    })
    .AddJwtBearer(options =>
    {
        // some configuration
    })
    .AddPolicyScheme("MyPolicy", "My test Policy", options =>
    {
        options.ForwardDefaultSelector = context =>
        {
            // Decide which Authentication schema to use for each request
            string authorization = context.Request.Headers[HeaderNames.Authorization];
            if (authorization != null && authorization.StartsWith("Bearer", StringComparison.OrdinalIgnoreCase))
            {
                return BearerAuthenticationSchemeName;
            }

            if (context.Request.Path.StartsWithSegments("/swagger"))
            {
                return OpenIdConnectDefaults.AuthenticationScheme;
            }

            return CookieAuthenticationDefaults.AuthenticationScheme;
        };
    });

var app = builder.Build();
app.UseCookiePolicy(new CookiePolicyOptions
{
    MinimumSameSitePolicy = SameSiteMode.None,
    Secure = CookieSecurePolicy.Always,
});
app.UseAuthentication();
app.UseAuthorization();
app.UseSwagger();
app.UseSwaggerUI();
// more middleware

When I open my /swagger endpoint, it redirects me to the login page. I manage to log in and it then redirects me to /signin-oidc, which is expected. However, there is an exception thrown by the OpenIdConnectAuthenticationHandler stating that message.State is null or empty.

System.Exception: An error was encountered while handling the remote login.
 ---> System.Exception: OpenIdConnectAuthenticationHandler: message.State is null or empty.
   --- End of inner exception stack trace ---
   at Microsoft.AspNetCore.Authentication.RemoteAuthenticationHandler`1.HandleRequestAsync()
   at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
   at MyApp.Middlewares.LoggingMiddleware.InvokeAsync(HttpContext httpContext)
   at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context)

When I run this locally (e.g on http://localhost:5000/swagger), it works perfectly fine, but when I open it at https://myexampleurl.whatever/swagger/, I get the exception. I have no idea why this does not work nor what I am doing wrong here. I see that the cookie is being sent in the format: .AspNetCore.OpenIdConnect.Nonce.<somehugestring>; .AspNetCore.Correlation.<correlation>; <anotherbigstring> and it may be worth noting that the Authority and my myexampleurl are different domains.

It might also be worth noting that when I run this locally and when I get redirected to /signin-oidc, the browser does send 4 cookies: 2 AspNetCore.Correlation and 2 AspNetCore.OpenIdConnect.Nonce cookies, all with different values. For the other one, it doesn't send any cookies at all.


Solution

  • First was my cookie policy setup. When setting the SameSite property to None in the app.UseCookiePolicy() middleware, the cookie must be marked as secure (CookieSecurePolicy.Always), otherwise the browser will not store or send it in its consequent requests, which would lead to an HTTP 500 in my case. However, even after introducing said changes, I was still having trouble. So, after digging deeper, I came across a forum post on Auth0 titled "ASP.NET Core incorrect redirect url" which internally linked / led to another post here on StackOverflow "AspNetCore Azure AD Connect Callback URL is http, not https". Ultimately, the answer to it suggested to add app.UseForwardedHeaders() prior to invoking app.UseAuthentication() in the application's middleware. After doing that (by basically following the same logic as the post answer did), everything started to work properly.

    So, after everything has been changed, this is how it all looks in my Program.cs:

    builder.Services.AddAuthentication().AddCookie(options =>
    {
        options.Cookie.HttpOnly = true;
        options.Cookie.SameSite = SameSiteMode.None;
        options.Cookie.SecurePolicy = CookieSecurePolicy.None;
    }).AddOpenIdConnect(...).AddJwtBearer(...).AddPolicyScheme(...);
    
    var app = builder.Build();
    
    ForwardedHeadersOptions forwardedHeadersOptions = new()
    {
        ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
    };
    forwardedHeadersOptions.KnownNetworks.Clear();
    forwardedHeadersOptions.KnownProxies.Clear();
    
    app.UseCookiePolicy(new CookiePolicyOptions
    {
        MinimumSameSitePolicy = SameSiteMode.None,
        Secure = CookieSecurePolicy.Always,
    });
    app.UseForwardedHeaders(forwardedHeadersOptions);
    app.UseAuthentication();
    app.UseAuthorization();
    app.UseSwagger();
    app.UseSwaggerUI();
    // more middleware