---
title: "IdentityServer in Docker Containers: Networking (Part 2)"
description: "How to fix networking between IdentityServer and a client in Docker Compose: service names, internal ports, MetadataAddress and the issuer mismatch. Part 2."
url: https://nestenius.se/net/identityserver-in-docker-containers-part-2/
date: 2024-11-25
author: "Tore Nestenius"
category: ".NET"
tags: ["identityserver", "openid-connect"]
site: https://nestenius.se/
---

# IdentityServer in Docker Containers: Networking (Part 2)

This is **part 2** of a blog series on containerizing a Duende IdentityServer and a client application. In this post, we resolve communication challenges that arise when these applications run in separate Docker containers. You'll learn how to fix back-channel issues, handle localhost conflicts, and establish proper networking between the client and IdentityServer.

This blog has been broken up into four separate posts:

- [IdentityServer in Docker Containers: Adding Containers (Part 1)](https://nestenius.se/net/identityserver-in-docker-containers-part-1/) 
- **IdentityServer in Docker Containers: Networking (Part 2)**
- [IdentityServer in Docker Containers: HTTPS and SameSite (Part 3)](https://nestenius.se/net/identityserver-in-docker-containers-part-3/)
- [IdentityServer in Docker Containers: Handle Logout (Part 4)](https://nestenius.se/net/identityserver-in-docker-containers-part-4/)

If you want to view the final solution, the code for each step and the final code can be found in the [**IdentityServer-in-Docker folder on GitHub**](https://github.com/tndataab/PublicBlogContent/tree/main/IdentityServer-in-Docker). 

## Fixing the IdentityServer Back-Channel (Attempt #2)

In the previous post, we learned we can’t use localhost from within the containers, so what should we do instead?

Docker Compose automatically creates a default network for all services defined in the **docker-compose.yml** file. This network allows containers to reach each other using their service names as DNS names.

In our setup, the client container should communicate with the identity container using `http://identity:7000` rather than `http://localhost:7000`.

![How we want the Client app to communicate with IdentityServer over the Docker container network.](https://nestenius.se/_astro/docker-compose-container-networking-client-identity-service-.bNs-JTUZ_5q8Up.webp)

```csharp
}).AddOpenIdConnect(options =>
{
    options.Authority = "http://identity:7000";
    ...
};
```

Here, identity is the name of the **IdentityService** container as defined in our Docker Compose file.  

But when we run this, we’ll still encounter an error. Why?  

The issue lies in how Docker Compose handles container communication. Services within Docker Compose communicate using their **service** **names** and the **_internal_** container ports they expose, not the **external** ports.

This means that while external clients (such as a web browser or Postman) outside the Docker network should use port **7000** to reach the IdentityService, containers within Docker should use the **_internal_** port, which is **80**.

To resolve this, we need to adjust the port in the code to use port **80** instead, like this:

```csharp
}).AddOpenIdConnect(options =>
{
    options.Authority = "http://identity:7000";
    ...
};
```

This configuration change ensures that the client container can successfully communicate with the identity container using the correct internal port.

However, this still needs to be fixed!

We end up with the browser trying to reach `http://identity/connect/authorize`, but obviously, we can’t use this outside Docker. **Identity** is only a name that can be used within the Docker network.

![Problem! We end up with the browser trying to reach http://identity/connect/authorize](https://nestenius.se/_astro/browser-address-bar-identity-container-authorize-endpoint.BfmgZa2y_nIICI.webp)

We could resolve this by adding the Identity hostname to the operating system’s **host** file. However, I prefer a solution that doesn’t require modifying this file.

### The Issue with Setting the Authority in IdentityServer

Setting the **authority** in the client to `http://identity:80` might seem like the obvious solution, but it introduces a few complications!

In our current setup, the **Authority** configuration serves three distinct purposes:

1. **Retrieving Configuration Documents:** It is used by the OpenID Connect authentication middleware in the client container to retrieve the **/.well-known/openid-configuration** and **/.well-known/openid-configuration/jwks** documents from IdentityServer.
2. **OpenID Connect Flows:** The authority URL is crucial during various OpenID Connect flows, including authentication, token exchange, and sign-out.
3. **Browser Redirection:** It also tells the browser where to redirect the user when authentication is required so that the user can authenticate.

The problem arises because the client container needs to use `http://identity:80` to reach IdentityServer from within the Docker network. In contrast, the browser (which is outside the Docker network) needs to use `http://localhost:7000` to reach IdentityServer.

This mismatch between the internal and external URLs complicates the situation.

## Introducing the MetaDataAddress (Attempt #3)

To try to address this, we can introduce the **MetadataAddress** property in our configuration, as shown below:

```csharp
.AddOpenIdConnect(options =>
{
    options.Authority = "http://localhost:7000";
    options.MetadataAddress = "http://identity:80";
    ...
};
```

By setting these two properties, we try to achieve the following:

- **Authority**: This is set to the externally accessible URL of IdentityServer (`http://localhost:7000`).
- **MetadataAddress**: This points to where the OpenID Connect authentication handler can reach IdentityServer internally (`http://identity:80`). This allows the client container to correctly obtain the needed configuration documents from IdentityServer.

With this configuration, we maintain compatibility for both internal container-to-container communication and external browser redirects.

The application still does not work, and so we get a new exception in the browser:

```text
JsonException: IDX10805: Error deserializing json: '<!DOCTYPE html> <html lang="en">...
```

How do we solve this?

## Fixing the IdentityServer MetadataAddress (Attempt #4)

The **MetadataAddress** property in OpenID Connect should be set to the **absolute** **URL** of the metadata document, not just the base URL. This is an essential distinction because MetadataAddress specifies the exact endpoint from which the OpenID Connect middleware will retrieve the configuration.

The solution is to change the configuration to:

```csharp
.AddOpenIdConnect(options =>
{
    options.Authority = "http://localhost:7000";
    options.MetadataAddress = "http://identity:80/.well-known/openid-configuration";
    ...
};
```

Now, retrieving the metadata from IdentityServer should work. However, we are back to the problem where the browser tries to reach `http://identity/connect/authorize`.

### Observing the Back-Channel Communication

The client's OpenID Connect authentication handler occasionally makes HTTP requests to the authorization server (IdentityServer), which is known as the **back channel**. By default, this communication could be more visible from the outside. However, implementing a custom **backchannel handler** to log these interactions can make it more transparent.

![We can implement custom backchannel handler to log these interactions can make it more transparent.](https://nestenius.se/_astro/openidconnect-handler-backchannel-httpclient-to-identityserv.k4MxPIOE_1RW7JK.webp)

To implement this, we create a new delegating handler named **BackChannelListener**, implemented as follows:

```csharp
public class BackChannelListener : DelegatingHandler
{
    public BackChannelListener() : base(new HttpClientHandler())
    {
    }

    protected async override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
                                                                 CancellationToken token)
    {
        var sw = new Stopwatch();
        sw.Start();
        var result = await base.SendAsync(request, token);
        sw.Stop();
        Log.Logger.ForContext("SourceContext", "BackChannelListener")
                   .Information($"### BackChannel request to {request?.RequestUri?.AbsoluteUri} took {sw.ElapsedMilliseconds.ToString()} ms");
        return result;
    }
}
```

This implementation will measure the time it takes for each request to complete and log it using Serilog. This will give us insights into the back-channel communication between the client and IdentityServer.

Next, we configure the OpenID Connect handler to use this handler:

```csharp
.AddOpenIdConnect(options =>
{
    ...
    options.BackchannelHttpHandler = new BackChannelListener();
    options.BackchannelTimeout = TimeSpan.FromSeconds(30);

});
```

With this setup, we should be able to see in the logs each time the client makes a request to IdentityServer over the back-channel, like the examples below:

```text
### BackChannel request to http://identity/.well-known/openid-configuration took 362 ms
### BackChannel request to http://identity/.well-known/openid-configuration/jwks took 30 ms
```

These logs provide valuable visibility into internal HTTP communication, helping us better understand the performance and behavior of back-channel requests.

However, our application still can’t authenticate; let’s address that in the next attempt.

## Understanding the Discovery Document Issue (Attempt #5)

If we run the application now, we still can’t authenticate successfully because the browser tries to access  
`http://identity/connect/authorize`... instead of the expected URL.

To understand what’s happening, we can add a minor tweak to our **BackChannelListener** to log the back-channel responses from IdentityServer to the console. This will give us insight into what the client receives.

We add this code to the handler:

```csharp
// HACK: log the response body; never run this in production
Log.Logger.ForContext("SourceContext", "BackChannelListener")
          .Information("#####################################");

Log.Logger.ForContext("SourceContext", "BackChannelListener")
          .Information(responseContent);

Log.Logger.ForContext("SourceContext", "BackChannelListener")
          .Information("#####################################");
```

**⚠️Important⚠️**

The back-channel request and response contain sensitive information, such as tokens or configuration data. Therefore, you should only log this information during troubleshooting and never in a production environment.

With this in place, we can see that the client correctly retrieves the discovery document as expected when we attempt to authenticate. The log shows the following output:

```json
{
  "issuer":"http://identity",
  "jwks_uri":"http://identity/.well-known/openid-configuration/jwks",
  "authorization_endpoint":"http://identity/connect/authorize"
  ...
}
```

However, when we navigate to the discovery document directly from our browser at  
`http://localhost:7000/.well-known/openid-configuration`, we get a different result:

```json
{
  "issuer":"http://localhost:7000",
  "jwks_uri":"http://localhost:7000/.well-known/openid-configuration/jwks",
  "authorization_endpoint":"http://localhost:7000/connect/authorize"
  ...
}
```

The issuer URL changes depending on where the request is made:

- Inside Docker: `http://identity`
- Outside Docker: `http://localhost:7000`

This discrepancy occurs because the identity provider’s issuer value is dynamically generated based on the origin of the incoming request URL.

## Solving The Initial Authentication Problem (Attempt #6)

The main issue is that when we click the log-in button, the application receives information from IdentityServer indicating that the authorization endpoint is `http://identity/connect/authorize`. Naturally, it tries to redirect the browser to that URL for user authentication using the authorization code flow.

To address this in a development environment, we can add the following event handler to intercept the redirect and adjust the URL to the correct one:

```csharp
.AddOpenIdConnect(options =>
{
    ...
    options.Events.OnRedirectToIdentityProvider = context =>
    {
        context.ProtocolMessage.IssuerAddress =
            "http://localhost:7000/connect/authorize";
        return Task.CompletedTask;
    };
	
});
```

Depending on your deployment setup, you may need to implement a different strategy in production. The key takeaway here is to understand the root cause of the issue.

**Important!⚠️**

Let me know if you have a better approach to solving this problem!

With this change in place, the application should now correctly redirect to IdentityServer and display the login screen as expected.

![IdentityServer login page](https://nestenius.se/_astro/duende-identityserver-local-account-login-page.1-TiDi7t_1V2RCq.webp)

## **Great! Are we done?**

Not quite yet! When we try to log in with the default **bob/bob** user, the form doesn’t work. ☹

## What’s next

In the next post, we’ll address the login form issue and introduce HTTPS to secure communication between the host and the containers, enabling successful and secure authentications.

[**To the next post >>>**](https://nestenius.se/net/identityserver-in-docker-containers-part-3/)

## Related Training Workshops

- [Introduction to IdentityServer and OpenID-Connect](https://tn-data.se/courses/introduction-to-identityserver-and-openid-connect/)
- [IdentityServer in Production](https://tn-data.se/courses/identityserver-in-production/)
