.netasp.net-mvcdockermicroservicesduende-identity-server

Dockerize Duende Identity Server: "Unable to obtain configuration from: 'https://localhost:44384/.well-known/openid-configuration'.Connection refused"


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
        };
    });


Solution

  • 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.