I am trying to dockerize my pet project on a microservice architecture. I have several microservices, a client and duende identity server. Locally, on my machine, the client communicates directly with the duende server. When I click Login it throws me to the identity server host where I register or authenticate and I am returned back to the client host with an authorization cookie. When I dockerize my project and specify localhost identity server for connection I get the error "Unable to obtain configuration from: 'https://localhost:44384/.well-known/openid-configuration'. Connection refused".
If I enter this link just in the browser manually: https://localhost:44384/.well-known/openid-configuration, I get the required json response. If I specify the internal address of the docker https://app.service.identity I get an error that the page is not found, because it throws me to this link, which exists only inside the docker. How can I fix this?
docker-compose.yml:
x-app-default-env: &app-default-env
Logging__LogLevel__Default: Information
Logging__LogLevel__Microsoft.AspNetCore: Warning
AllowedHosts: "*"
services:
app:
restart: unless-stopped
image: mango.web
build:
context: ./Mango.Web
volumes:
- ./Mango.Web/wwwroot:/app/wwwroot
environment:
<<: *app-default-env
ServiceUrls__IdentityAPI: https://localhost:44384
ServiceUrls__ProductAPI: http://app.service.product
ServiceUrls__CouponAPI: http://app.service.coupon
ServiceUrls__ShoppingCartAPI: http://app.service.shoppingcart
ServiceUrls__AzureBlobAPI:
ConnectionStrings__DefaultConnection: "Host=db;Port=5432;Username=postgres;Password=postgres;Database=MangoWEBAPI"
app.service.order:
restart: unless-stopped
image: mango.services.order
build:
context: .
dockerfile: ./Mango.Services.OrderAPI/Dockerfile
environment:
<<: *app-default-env
ConnectionStrings__DefaultConnection: "Host=db;Port=5432;Username=postgres;Password=postgres;Database=MangoOrderAPI"
ConnectionStrings__AzureServiceBus: ${AZURE_SERVICE_BUS_CONNECTION_STRING}
CheckoutMessageTopic: "checkoutmessagetopic"
CheckoutSubscription: "mangoOrdersSubscription"
OrderPaymentProcessTopic: "orderpaymentprocesstopic"
OrderPaymentProcessSubscription: "mangoPayment"
OrderUpdatePaymentResultTopic: "orderupdatepaymentresulttopic"
OrderUpdatePaymentResultSubscription": "mangoOrdersSubscription"
CheckoutMessageQueue: "checkoutqueue"
app.service.azureblobservice:
restart: unless-stopped
image: mango.services.azureblobservice
build: Mango.Services.AzureBlobService
environment:
<<: *app-default-env
ServiceUrls__IdentityServer: http://app.service.identity
ConnectionStrings__BlobStorage: ${CONNECTION_STRINGS_BLOB_STORAGE}
BlobContainerName: ${BLOB_CONTAINER_NAME}
app.service.product:
restart: unless-stopped
image: mango.services.productapi
build: Mango.Services.ProductAPI
environment:
<<: *app-default-env
ConnectionStrings__DefaultConnection: "Host=db;Port=5432;Username=postgres;Password=postgres;Database=MangoProductAPI"
app.service.payment:
restart: unless-stopped
image: mango.services.payment
build:
context: .
dockerfile: ./Mango.Services.PaymentAPI/Dockerfile
environment:
<<: *app-default-env
ServiceUrls__IdentityServer: http://app.service.identity
ConnectionStrings__AzureServiceBus: ${AZURE_SERVICE_BUS_CONNECTION_STRING}
OrderPaymentProcessTopic: "orderpaymentprocesstopic"
OrderPaymentProcessSubscription: "mangoPayment"
OrderUpdatePaymentResultTopic: "orderupdatepaymentresulttopic"
OrderUpdatePaymentResultSubscription: "mangoOrdersSubscription"
ConnectionStrings__DefaultConnection: "Host=db;Port=5432;Username=postgres;Password=postgres;Database=MangoPaymentAPI"
app.service.shoppingcart:
restart: unless-stopped
image: mango.services.shoppingcart
build:
context: .
dockerfile: ./Mango.Services.ShoppingCartAPI/Dockerfile
environment:
<<: *app-default-env
ServiceUrls__IdentityServer: http://app.service.identity
ServiceUrls__CouponAPI: http://app.service.coupon
ConnectionStrings__DefaultConnection: "Host=db;Port=5432;Username=postgres;Password=postgres;Database=MangoShoppingCartAPI"
app.service.identity:
restart: unless-stopped
image: mango.services.identity
build: Mango.Services.Identity
volumes:
- ./Mango.Services.Identity/wwwroot:/app/wwwroot
environment:
<<: *app-default-env
ConnectionStrings__DefaultConnection: "Host=db;Port=5432;Username=postgres;Password=postgres;Database=MangoIdentityAPI"
app.service.email:
restart: unless-stopped
image: mango.services.email
build:
context: .
dockerfile: ./Mango.Services.Email/Dockerfile
environment:
<<: *app-default-env
ConnectionStrings__AzureServiceBus: ${AZURE_SERVICE_BUS_CONNECTION_STRING}
OrderUpdatePaymentResultTopic: "orderupdatepaymentresulttopic"
EmailSubscription: "emailSubscription"
ConnectionStrings__DefaultConnection: "Host=db;Port=5432;Username=postgres;Password=postgres;Database=MangoEmailAPI"
app.service.coupon:
restart: unless-stopped
image: mango.services.coupon
build: Mango.Services.CouponAPI
environment:
<<: *app-default-env
ServiceUrls__IdentityServer:
ConnectionStrings__DefaultConnection: "Host=db;Port=5432;Username=postgres;Password=postgres;Database=MangoCouponAPI"
docker-compose.override.yml:
services:
app:
ports:
- 8080:80
- 44315:443
environment:
- ASPNETCORE_ENVIRONMENT=Development
- ASPNETCORE_URLS=https://+:443;http://+:80
- ASPNETCORE_Kestrel__Certificates__Default__Password=password
- ASPNETCORE_Kestrel__Certificates__Default__Path=/https/aspnetapp.pfx
volumes:
- ~/.aspnet/https:/https:ro
app.service.identity:
ports:
- 44384:443
- 8081:80
environment:
- ASPNETCORE_ENVIRONMENT=Development
- ASPNETCORE_URLS=https://+:443;http://+:80
- ASPNETCORE_Kestrel__Certificates__Default__Password=password
- ASPNETCORE_Kestrel__Certificates__Default__Path=/https/aspnetapp.pfx
volumes:
- ~/.aspnet/https:/https:ro
db:
restart: unless-stopped
ports:
- 5433:5432
image: postgres:16
user: root
volumes:
- ../docker/db/data:/var/lib/pgsql/data
environment:
POSTGRES_PASSWORD: postgres
POSTGRES_USER: postgres
Client Program.cs Authorization:
builder.Services.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
}).AddCookie("Cookies", c => c.ExpireTimeSpan=TimeSpan.FromMinutes(10))
.AddOpenIdConnect("oidc", options =>
{
options.Authority = builder.Configuration["ServiceUrls:IdentityAPI"];
options.GetClaimsFromUserInfoEndpoint = true;
options.ClientId = "mango";
options.ClientSecret = "secret";
options.ResponseType = "code";
options.ClaimActions.MapJsonKey("role", "role", "role");
options.ClaimActions.MapJsonKey("sub", "sub", "sub");
options.TokenValidationParameters.NameClaimType = "name";
options.TokenValidationParameters.RoleClaimType = "role";
options.Scope.Add("mango");
options.SaveTokens = true;
options.RequireHttpsMetadata = false;
options.BackchannelHttpHandler = new HttpClientHandler
{
ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => true
};
});
To diagnose and debug the communication between the client and IdentityServer, then you can use some code like:
.AddJwtBearer(opt =>
{
opt.BackchannelHttpHandler = new JwtBearerBackChannelListener();
opt.BackchannelTimeout = TimeSpan.FromSeconds(60); //default 60s
...
}
.AddOpenIdConnect(opt =>
{
opt.BackchannelHttpHandler = new JwtBearerBackChannelListener();
opt.BackchannelTimeout = TimeSpan.FromSeconds(60); //default 60s
...
}
Then, a sample back channel listener can look like this:
namespace Infrastructure
{
/// <summary>
/// Backchannel listener, that will log requests made to our IdentityServer
/// From the IdentityServer in production training class https://www.tn-data.se
/// </summary>
public class BackChannelListener : DelegatingHandler
{
public BackChannelListener() : base(new HttpClientHandler())
{
Console.WriteLine();
}
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
Console.WriteLine("BackChannelListener");
Console.WriteLine("@@@@ SendASync: " + request.RequestUri);
var sw = new Stopwatch();
sw.Start();
var result = base.SendAsync(request, cancellationToken);
result.ContinueWith(t =>
{
sw.Stop();
Console.WriteLine("@@@@ success: " + result.IsFaulted);
Console.WriteLine("@@@@ loadtime: " + sw.ElapsedMilliseconds.ToString());
Console.WriteLine("@@@@ url: " + request.RequestUri);
Serilog.Log.Logger.ForContext("SourceContext", "BackChannelListener")
.ForContext("url", request.RequestUri)
.ForContext("loadtime", sw.ElapsedMilliseconds.ToString() + " ms")
.ForContext("success", result.IsCompletedSuccessfully)
.Information("Loading IdentityServer configuration");
});
return result;
}
}
}
Hope this helps.
In AddOpenIDConnect, and if it redirects to the wrong URL, then you can override this by addding this event handler
.AddOpenIdConnect(options =>
{
options.Events.OnRedirectToIdentityProvider = context =>
{
context.ProtocolMessage.IssuerAddress = "https://localhost:7001/connect/authorize";
return Task.CompletedTask;
};
});
To get logout to work, you first have to modify these rather "hidden" settings in the UI:
public static class LogoutOptions
{
public static readonly bool ShowLogoutPrompt = false;
public static readonly bool AutomaticRedirectAfterSignOut = true;
}
If that does not work, you need to ensure in the logs that the ID-token sent to IdentityServer during logut is actually accepted. If you get this error "JWT token validation error: IDX10205: Issuer validation failed" then you need to tweak the issuer value.
I was curious myself how to dockerize IdentityServer properly, so I am working on a blogpost, but that one will be published in 2-3 week time. In the meantime, you can peek at the source code here: https://github.com/tndataab/PublicBlogContent/tree/main/IdentityServer-in-Docker
There will be a final source folder with everything working later today or tomorrow.