IdentityServer in Docker Containers - Part 1

IdentityServer in Docker Containers – Part 1

Getting Duende IdentityServer and a client application up and running in separate containers can be challenging. This blog post will provide a step-by-step guide for a smooth setup and show you how to resolve common challenges along the way. We will also learn about security, cookies, ports, containers, and certificates.

This is a big topic, so this blog is divided into four posts. You can get to where you need to be below, but for handy context, it may be helpful to read this post first.

⦁ IdentityServer in Docker Containers: Adding Containers (Part 1)
IdentityServer in Docker Containers: Networking (Part 2)
⦁ IdentityServer in Docker Containers: HTTPS and SameSite (Part 3) (Coming soon)
⦁ IdentityServer in Docker Containers: Handle Logout (Part 4) (Coming soon)

If you want to view the final solution, the code for each step and the final code can be found on GitHub here.

Background – containerizing a Duende IdentityServer

I spend quite a bit of my time helping developers on Stack Overflow, and I’ve noticed that many struggle with containerizing a Duende IdentityServer solution. There are numerous pitfalls, gotchas, and misconceptions that we need to understand and address.

The series focuses on deploying Duende IdentityServer locally using Docker Compose, emphasizing the fundamentals rather than a production-ready implementation.

The two Docker Containers (Client Application and IdentityService) we will use in this blog post.

This series takes a step-by-step approach to tackle the challenge, highlighting common mistakes and pitfalls along the way. Rather than simply presenting the final solution, it encourages a shared learning experience, and troubleshooting issues together to build a strong understanding of the fundamentals for containerizing IdentityServer with Docker Compose.

Important – Not for Production

The focus is on getting the application to work locally in a containerized environment using Docker Compose. Making this setup production-ready involves additional considerations beyond the scope of this post.

The Setup – IdentityServer and the Client Application

We have two ASP.NET Core 8 applications: one client application and one IdentityServer application. Both applications have been tweaked to make them more educational.

The relationship between the IdentityServer, Client Application and the Browser in this setup.

We name the IdentityServer project IdentityService to avoid potential naming collisions in our code.

The complete source code for this setup and each attempt to create it can be found in my public GitHub repository. The source code in the \Start project folder contains the start code, which you can run from within Visual Studio and authenticate using the credentials bob/bob.

The Goal – containerized IdentityServer solution

This setup aims to package two applications into separate containers and run them locally using Docker Desktop with Docker Compose. Authentication against IdentityServer should still work seamlessly after containerization.

How the browser communicates with the Docker Container host.

Containerize IdentityServer and The Client (Attempt #1)

In our first attempt, we’ll create the following setup using HTTP. Starting with HTTP is beneficial for its simplicity and highlighting some gotchas we’ll explore later in this blog post series. We’ll introduce and use HTTPS later.

We add two Dockerfiles (Dockerfile_Client and Dockerfile_Identity) to containerize the client and IdentityServer projects. The code for them can be found on GitHub.

Next, we add a Docker compose file to launch the two containers and expose ports 5000 and 7000 to the host machine.

The port mapping between the host and the container ports.
				
					services:
  client:
    build:
      context: .
      dockerfile: Dockerfile_Client
    ports:
      - "5000:80"

  identity:
    build:
      context: .
      dockerfile: Dockerfile_Identity
    ports:
      - "7000:80"
				
			

In the client application (program class), we modify the OpenID Connect authority as follows:

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

				
			

To build and deploy the containers, we use the following Docker Compose command:

				
					docker-compose up --build
				
			

A simple batch file (RunCompose.bat) has been added that runs the command for us.
When we run the containers, we can observe that both are up and running and can be accessed using http://localhost:5000/ and http://localhost:7000/.

However, when we click on the login button, we get the following error:

				
					IOException: IDX20804: Unable to retrieve document from: 'http://localhost:7000/.well-known/openid-configuration'.
				
			

This error occurs because the client application tries to download the IdentityServer’s discovery document and public signing keys from the specified URL but fails to do so. The discovery document is crucial as it contains endpoints and configuration data that the client needs to communicate with IdentityServer securely.

Client container trying to connect with the IdentityServer container.

The URL works fine when I go to http://localhost:7000/.well-known/openid-configuration in my browser!

Accessing the OpenID Connect Discovery document in the browser. Located at the well-known endpoint.

So, why does this not work when it works from the browser?

The problem with Localhost and Containers

In our setup, the client attempts to make a cross-container HTTP request like this:

Client container successfully connecting with the Duende IdentityServer.

However, when requesting http://localhost:7000/, it is crucial to understand that this URL is resolved within the client container itself, not to the IdentityService that we intended to reach.

The image below shows what happens in our current setup. Obviously, nothing is listening on port 7000 inside the client container.

Showing why trying make a HTTP request to another container using localhost fails.

The issue arises from having multiple isolated localhost instances in the setup, each tied to a specific container:

⦁ Client container has its own localhost.
⦁ Identity container has its own localhost.
⦁ Host machine has yet another localhost.

As a result, when the client container tries to access http://localhost:7000/, the request is confined to the client container itself and never reaches the IdentityServer container.

The image below illustrates how these three separate localhost environments exist in our setup:

The three separate localhosts in our application. The client, Duende IdentityServer and the host machine.

The key here is Docker’s network isolation, which prevents containers from communicating with one another using localhost.

What’s next

We’ll resolve this further in my next post, were we  will continue refining the communication between the two containers as we work toward building a fully functional, containerized IdentityServer solution.

To the next post >>>

 

About the author

Hi, I’m Tore! I have been fascinated by computers since I unpacked my first Commodore VIC-20. Today, I enjoy providing freelance development and developer training services, focusing on ASP.NET Core, IdentityServer, OpenID Connect, Architecture, and Web Security. You can connect with me on LinkedIn and Twitter, and you can find out more about me and my services here, as well as my courses for developers, including my course, Introduction to IdentityServer and OpenID-Connect.

Tore’s Newsletter

Be the First to Know! Get notified about my latest blog posts, upcoming presentations, webinars, and more — subscribe today!

Cartoon of Tore Nestenius
Share This Story

Related Posts

About Tore Nestenius - Freelance software development instructor and consultant.

About me

My name is Tore Nestenius and I’m a trainer and senior software developer focusing on Architecture, Security and Identity, .NET, C#, Backend, and Cloud, among other things.

Do You Want Tore To Be Your Mentor?

Categories