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"}
),
};
}
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.