I’m experiencing an issue with my .NET 6 application where correlation cookies are lost after authentication when deployed behind a reverse proxy. The reverse proxy handles HTTPS, so the application itself doesn’t manage HTTPS directly.
The application works correctly when running locally, but in the production environment (behind the reverse proxy), the correlation cookies seem to be lost after authentication.
var builder = WebApplication.CreateBuilder(args);
builder.Logging.AddConsole();
var config = new ConfigurationBuilder()
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.Build();
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowAll",
builder =>
{
builder.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader();
});
});
string SecurityLevelClaimPolicy = "SecurityLevelClaimPolicy";
string SecurityLevelClaim = "Claims";
Settings _settings = new Settings();
_settings.IdConfiguration = new IdConfiguration();
//_settings.ClientType = ClientType.ApiAccess;
_settings.ClientType = ClientType.ApiAccessForMultiTenantClient;
_settings.ApiAudience1 = "URL";
_settings.IdConfiguration = new IdConfiguration();
_settings.IdConfiguration.ClientId = "clientid";
_settings.IdConfiguration.RsaPrivateKeyJwk = new TestIT.Authenticator.API.Helpers.SecurityKey("{'d':'XXXXXX','kid':'XXXXXX','use':'XXXXX','alg':'XXXXX'}", "XXXXX");
_settings.IdConfiguration.Scope = "scope";
_settings.IdConfiguration.StsUrl = "URL";
builder.Services.AddHttpContextAccessor();
builder.Services.AddMvc()
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.PropertyNamingPolicy = new LowercaseContractResolver();
options.JsonSerializerOptions.PropertyNameCaseInsensitive = true;
options.JsonSerializerOptions.Converters.Add(new TestIT.Authenticator.API.Helpers.DateTimeHelper("dd.MM.yyyyTHH.mm.ss"));
})
.AddViewLocalization(LanguageViewLocationExpanderFormat.Suffix, opts =>
{
opts.ResourcesPath = "Resources";
})
.AddDataAnnotationsLocalization();
builder.Services.AddControllers();
builder.Services.AddRouting(options => options.LowercaseUrls = true);
builder.Services.AddEndpointsApiExplorer();
builder.Services.Configure<CookiePolicyOptions>(options =>
{
options.MinimumSameSitePolicy = SameSiteMode.None;
options.Secure = CookieSecurePolicy.Always; // Ensure cookies are marked as Secure
});
// Create a settings instance. These can be injected into other objects, for instance the HomeController
builder.Services.AddSingleton<Settings>(_settings);
// We need the IdConfiguration instance as a service as well:
builder.Services.AddSingleton<IdConfiguration>(_settings.IdConfiguration);
// Services for calculating the expiration time for tokens
var dateTimeService = new DateTimeService();
builder.Services.AddSingleton<IDateTimeService>(dateTimeService);
builder.Services.AddTransient<IExpirationTimeCalculator, ExpirationTimeCalculator>();
builder.Services.AddSingleton<IHelseServices, HelseServices>();
// No request object is needed, so we inject a null object for payload claims creation instead
builder.Services.AddSingleton<IPayloadClaimsCreatorForRequestObjects>(new NullPayloadClaimsCreatorForRequestObjects());
//var clientAssertionPayloadClaimsCreator = new ClientAssertionPayloadClaimsCreator(dateTimeService);
//builder.Services.AddSingleton<IPayloadClaimsCreatorForClientAssertion>(clientAssertionPayloadClaimsCreator);
var clientAssertionPayloadClaimsCreator = new ClientAssertionPayloadClaimsCreator(dateTimeService);
// We need payload claims for the token request, both the "default" type and for the multi-tenant organization number:
var compositePayloadClaimsCreator = new CompositePayloadClaimsCreator(new List<IPayloadClaimsCreator>
{
clientAssertionPayloadClaimsCreator,
new PayloadClaimsCreatorForMultiTenantClient()
});
// We add this object as an instance of IPayloadClaimsCreatorForClientAssertion
builder.Services.AddSingleton<IPayloadClaimsCreatorForClientAssertion>(compositePayloadClaimsCreator);
builder.Services.AddTransient<IJwtPayloadCreator, JwtPayloadCreator>();
builder.Services.AddSingleton<ISigningJwtTokenCreator, SigningJwtTokenCreator>();
builder.Services.AddTransient<IClientAssertionsBuilder, ClientAssertionsBuilder>();
// Builder for client assertions payloads
// Builder for JWT tokens used for client assertions
builder.Services.AddSingleton<IDiscoveryDocumentGetter>(new DiscoveryDocumentGetter(_settings.IdConfiguration.StsUrl));
builder.Services.AddSingleton<IIdEndpointsDiscoverer, IdEndpointsDiscoverer>();
// Builds token requests (in our case, refresh token requests)
builder.Services.AddTransient<ITokenRequestBuilder, TokenRequestBuilder>();
// Register services here
builder.Services.AddTransient<IAccessTokenUpdater, AccessTokenUpdaterForMultiTenantRequests>();
builder.Services.AddScoped<IUserSessionDataStore, MemoryUserSessionDataStore>();
// A getter of user session data, uses the user session data store
builder.Services.AddTransient<IUserSessionGetter, UserSessionGetter>();
builder.Services.AddAuthentication(options =>
{
// Configure default authentication schemes for the application
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
//options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
// Configure JWT bearer authentication
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(config.GetValue<string>("Secret"))),
ValidateIssuer = true,
ValidIssuer = config.GetValue<string>("Issuer"),
ValidateAudience = true,
ValidAudience = config.GetValue<string>("Audience"),
ValidateLifetime = true,
};
});
builder.Services.AddSwaggerGen(c =>
{
// Configure Swagger documentation
c.SwaggerDoc("v1", new OpenApiInfo { Title = "TestIt .Net middleware", Version = "v1" });
// Add JWT bearer authentication to Swagger
var securityScheme = new OpenApiSecurityScheme
{
Name = "Authorization",
BearerFormat = "JWT",
Scheme = "bearer",
Description = "JWT Authorization header using the Bearer scheme.",
In = ParameterLocation.Header,
Type = SecuritySchemeType.Http,
};
c.AddSecurityDefinition("Bearer", securityScheme);
c.AddSecurityRequirement(new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "Bearer"
}
},
Array.Empty<string>()
}
});
});
builder.Services.ConfigureApplicationCookie(options =>
{
options.Cookie.HttpOnly = true;
options.Cookie.SecurePolicy = CookieSecurePolicy.Always; // Ensure cookies are sent over HTTPS
options.Cookie.SameSite = SameSiteMode.None; // Set the SameSite attribute to None
});
builder.Services.AddTransient<IConfigureOptions<AuthenticationOptions>, AuthenticationOptionsInitializer>();
builder.Services.AddTransient<IConfigureNamedOptions<OpenIdConnectOptions>, UserAuthendication>();
builder.Services.AddTransient<AuthenticationsController>();
builder.Services.Configure<CookiePolicyOptions>(OpenIdConnectOptions =>
{
OpenIdConnectOptions.MinimumSameSitePolicy = SameSiteMode.None;
OpenIdConnectOptions.Secure = CookieSecurePolicy.Always; // Ensure cookies are marked as Secure
});
//// Set authentication options (these will call the AuthenticationOptionsInitializer and OpenIdConnectOptionsInitializer instances)
builder.Services.AddAuthentication()
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, OpenIdConnectOptions =>
{
// Path to access denied endpoint. Used when authorization fails
OpenIdConnectOptions.AccessDeniedPath = "/authorization/access-denied";
OpenIdConnectOptions.AccessDeniedPath = "/authorization/access-denied";
// Ensure cookies are only sent over HTTPS
OpenIdConnectOptions.Cookie.SecurePolicy = CookieSecurePolicy.Always;
// Set SameSite policy. Adjust as necessary based on your requirements
OpenIdConnectOptions.Cookie.SameSite = SameSiteMode.None;
// Mark the cookie as HttpOnly
OpenIdConnectOptions.Cookie.HttpOnly = true;
})
.AddOpenIdConnect(openIdConnectOptions =>
{
// We need to extract the OpenID Connect options initializer from the service provider:
var serviceProvider = builder.Services.BuildServiceProvider();
var initializer = serviceProvider.GetService<IConfigureNamedOptions<OpenIdConnectOptions>>();
initializer!.Configure(nameof(UserAuthendication), openIdConnectOptions);
openIdConnectOptions.CallbackPath = "/signin-oidc";
openIdConnectOptions.NonceCookie.SecurePolicy = CookieSecurePolicy.Always;
openIdConnectOptions.CorrelationCookie.SecurePolicy = CookieSecurePolicy.Always;
});
var securityLevelClaimPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.RequireClaim(IdSecurityLevelClaim, "4")
.Build();
builder.Services.AddDataProtection();
builder.Services.AddAuthorization(config =>
{
config.AddPolicy(SecurityLevelClaimPolicy, securityLevelClaimPolicy);
});
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
}
app.UseRouting();
app.UseForwardedHeaders(new ForwardedHeadersOptions
{
ForwardedHeaders = Microsoft.AspNetCore.HttpOverrides.ForwardedHeaders.XForwardedFor | Microsoft.AspNetCore.HttpOverrides.ForwardedHeaders.XForwardedProto
});
app.UseCookiePolicy(new CookiePolicyOptions
{
Secure = CookieSecurePolicy.Always,
});
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
//app.UseHttpsRedirection();
app.UseCors("AllowAll");
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
Is there something I’m missing in the configuration that could be causing the correlation cookies to be lost after authentication? Any advice or insights would be greatly appreciated.
Halvar is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.
0