asp.net-mvcasp.net-corecookiesidentityserver4

Error in configuring identity server. The cookie '.AspNetCore.Identity.Application' has set 'SameSite=None' and must also set 'Secure'


I am trying to learn IdentityServer4. I setup one project in identityserver and another project in mvc. the code run as expected in localhost but i got error in docker swarm environment. After sucessfull login usint correct username password it is again redirect to login page. The program cs of mvc is as follow:

using System.Security.Claims;
using DMNMiddleware.UserManagement.Services;
using IdentityModel.Client;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using Npgsql;

var builder = WebApplication.CreateBuilder(args);
builder.Configuration
    .SetBasePath(Directory.GetCurrentDirectory())
    .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
    .AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", optional: true)
    .AddEnvironmentVariables();

string authUrl = builder.Configuration.GetValue<string>("AuthServerUrl") ?? string.Empty;
string interceptUrl = builder.Configuration.GetValue<string>("AuthInterceptUrl") ?? string.Empty;
builder.Services.AddControllersWithViews();

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");

builder.Services.AddAuthentication(options =>
{
    options.DefaultScheme = "Cookies";
    options.DefaultChallengeScheme = "oidc";

}).AddCookie("Cookies", c =>
{
    c.CookieManager = new ChunkingCookieManager();
    c.Cookie.HttpOnly = true;
    c.Cookie.SameSite = SameSiteMode.None;
    c.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
    c.ExpireTimeSpan = TimeSpan.FromMinutes(30);
})
.AddOpenIdConnect("oidc", options =>
{

    options.Authority = authUrl;
    options.MetadataAddress = $"{authUrl}/.well-known/openid-configuration";
    options.Events.OnRedirectToIdentityProvider = context =>
    {
        // Intercept the redirection so the browser navigates to the right URL in your host
        context.ProtocolMessage.IssuerAddress = $"{interceptUrl}/connect/authorize";
        return Task.CompletedTask;
    };
    options.RequireHttpsMetadata = false;
    options.GetClaimsFromUserInfoEndpoint = true;
    options.ClientId = "usermanagement";
    options.ClientSecret = "secret";
    options.ResponseType = "code";
    options.Scope.Add("usermanagement");
    options.Scope.Add("openid");
    options.Scope.Add("profile");
    options.SaveTokens = true;
    options.TokenValidationParameters = new TokenValidationParameters
    {
        NameClaimType = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name",  // Explicitly set the correct name claim type
        RoleClaimType = "http://schemas.microsoft.com/ws/2008/06/identity/claims/role"
    };
    options.ClaimActions.MapJsonKey(ClaimTypes.Name, "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name");
    options.ClaimActions.MapJsonKey(ClaimTypes.Role, "http://schemas.microsoft.com/ws/2008/06/identity/claims/role");
    options.NonceCookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
    options.CorrelationCookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
});


builder.Services.AddDataProtection()
    .PersistKeysToFileSystem(new DirectoryInfo(@"/root/.aspnet/DataProtection-Keys"))
    .SetApplicationName("SharedApp");


builder.Services.AddHttpContextAccessor();

builder.Services.AddScoped<NpgsqlConnection>(sp =>
{
    var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
    return new NpgsqlConnection(connectionString);
});

builder.Services.AddScoped<IUserService, UsersService>();
builder.Services.AddScoped<IEndPointService, EndPointService>();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Home/Error");
    app.UseHsts();
}
app.UseCookiePolicy();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

app.Run();

and program.cs of the identity server is as follow:

using DMNMiddleware.AuthServer;
using DMNMiddleware.AuthServer.Data;
using DMNMiddleware.AuthServer.DomainModels;
using DMNMiddleware.AuthServer.Profiles;
using DMNMiddleware.AuthServer.Services;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using StackExchange.Redis;

var builder = WebApplication.CreateBuilder(args);
builder.Configuration
    .SetBasePath(Directory.GetCurrentDirectory()) 
    .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) 
    .AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", optional: true) 
    .AddEnvironmentVariables();
builder.Services.AddControllersWithViews();
//database connection
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");

builder.Services.AddDbContext<AuthServerDbContext>(options => 
options.UseNpgsql(connectionString));

//Add Identity
builder.Services.AddIdentity<ApplicationUser, IdentityRole>()
                .AddEntityFrameworkStores<AuthServerDbContext>()
                .AddDefaultTokenProviders();

// Configure IdentityServer4
builder.Services.AddIdentityServer(options =>
                {
                    options.Authentication.CookieAuthenticationScheme = IdentityConstants.ApplicationScheme;
                })
                .AddInMemoryClients(Config.Clients)
                .AddInMemoryIdentityResources(Config.IdentityResources)
                .AddInMemoryApiScopes(Config.ApiScopes)
                .AddAspNetIdentity<ApplicationUser>()
                .AddProfileService<IdentityProfileService>()
                .AddDeveloperSigningCredential();

builder.Services.ConfigureApplicationCookie(options =>
{
    options.Cookie.SameSite = SameSiteMode.None;
    options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
});

builder.Services.AddHttpContextAccessor();

builder.Services.AddScoped<IDbInitializer, DbInitializer>();
builder.Services.AddScoped<IAccountService, AccountService>();
builder.Services.AddScoped<IUsersService, UsersService>();
builder.Services.AddScoped<IEndPointService, EndPointService>();

builder.Services.AddDataProtection()
    .PersistKeysToFileSystem(new DirectoryInfo(@"/root/.aspnet/DataProtection-Keys"))
    .SetApplicationName("SharedApp");


builder.Services.AddAntiforgery();
    
var app = builder.Build();
app.UseStaticFiles();
app.UseRouting();

app.UseIdentityServer();
app.UseAuthorization();
app.Use(async (context, next) =>
{
    context.Response.Headers.Append("Content-Security-Policy", "default-src 'self'; style-src 'self' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com;");
    await next();
});
using(var scope = app.Services.CreateScope())
{
    var dbInitializer = scope.ServiceProvider.GetRequiredService<IDbInitializer>();
    dbInitializer.Initialize();
}

app.UseEndpoints(endpoints => {
    _ = endpoints.MapDefaultControllerRoute();
});

app.Run();

my docker stack file is as follow

version: '3.8'

services:
  authserver:
    image: 192.168.48.107:5000/dmn/authserver:1.0.0
    ports:
      - "7000:80"
    networks:
      - my_network
    deploy:
      replicas: 1
      restart_policy:
        condition: on-failure
    volumes:
      - key-storage:/root/.aspnet/DataProtection-Keys




  usermanagement:
    image: 192.168.48.107:5000/dmn/usermanagement:1.0.0
    ports:
      - "9000:80"
    networks:
      - my_network
    deploy:
      replicas: 1
      restart_policy:
        condition: on-failure
    volumes:
      - key-storage:/root/.aspnet/DataProtection-Keys

networks:
  my_network:
    driver: overlay

volumes:
  key-storage:
  redis-data:

I am currently playing with cookies settings in both program.cs but not working as expected. My config.cs file is as follow:

using System.Security.Claims;
using IdentityServer4;
using IdentityServer4.Models;

namespace DMNMiddleware.AuthServer;

public class Config
{

    public static IEnumerable<Client> Clients => new Client[]
    {
        new Client
        {
            ClientId = "api_client",
            AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
            ClientSecrets = 
            {
                new Secret("client_secret".Sha256())
            },
            AllowedScopes = {"api_scope"}
        },
        new Client
        {
            ClientId = "usermanagement",
            ClientSecrets = { new Secret("secret".Sha256()) },
            AllowedGrantTypes = GrantTypes.Code,
            RedirectUris = {
                "http://localhost:9000/signin-oidc", 
                "http://192.168.48.107:9000/signin-oidc",
                "http://192.168.48.108:9000/signin-oidc",
                "http://192.168.48.109:9000/signin-oidc"
             },
            PostLogoutRedirectUris = { 
                "http://localhost:9000/signout-callback-oidc",
                "http://192.168.48.107:9000/signout-callback-oidc", 
                "http://192.168.48.108:9000/signout-callback-oidc",
                "http://192.168.48.109:9000/signout-callback-oidc" 
            },
            AllowedScopes = new List<string>
            {
                IdentityServerConstants.StandardScopes.OpenId,
                IdentityServerConstants.StandardScopes.Profile,
                IdentityServerConstants.StandardScopes.Email,
                "usermanagement"
            },
            RequireConsent = false
        },
    };

    public static IEnumerable<ApiScope> ApiScopes => new ApiScope[]
    {
        new ApiScope("api_scope","api_scope"),
        new ApiScope("usermanagement", "usermanagement")
    };

    public static IEnumerable<ApiResource> ApiResources => new ApiResource[]
    {

    };

    public static IEnumerable<IdentityResource> IdentityResources => 
    new IdentityResource[]
    {
      new IdentityResources.OpenId(),
      new IdentityResources.Profile(),
      new IdentityResources.Address(),
      new IdentityResources.Email(),
      new IdentityResource(
        "roles",
        "",
        new List<string>() {"role"}
      ),


    };
}



Solution

  • You must use HTTPS between the browser and the services, that includes the RedirectUris , but it is OK to use HTTP between the containers.

    SameSite=none cookies requires HTTPS to work.

    I did a sample project using Docker Compose and IdentityServer and you can find my code here https://github.com/tndataab/PublicBlogContent/tree/main/IdentityServer-in-Docker (Look in the Final folder).

    The code is used in an upcoming blog post.

    Then, I think you need to use Lax here, because the browser will reject Strict cookies here:

    c.Cookie.SameSite = SameSiteMode.None;
    

    It will probably be blocked by the browser. If you want to learn more about how to debug cookie problems in ASP.NET Core, then see my blog post at: https://nestenius.se/2023/10/09/debugging-cookie-problems/

    One reason why the cookie is blocked is that the request that sets it comes from another site/domain.