41

I want to be able to authenticate against an Identity Server (STS) from outside and inside a docker machine.

I am having trouble with setting the correct authority that works both inside and outside the container. If I set the authority to the internal name mcoidentityserver:5000 then the API can authenticate but the client cannot get a token as the client lies outside of the docker network. If I set the authority to the external name localhost:5000 then the client can get a token but the API doesn't recognise the authority name (because localhost in this case is host machine).

What should I set the Authority to? Or perhaps I need to adjust the docker networking?

Diagram

The red arrow is the part that I'm having trouble with. Three docker containers in a network, a client and PostgreSQL Admin, their ports and a red arrow showing where I think the problem lies.

Detail

I am setting up a Windows 10 docker development environment that uses an ASP.NET Core API (on Linux), Identity Server 4 (ASP.NET Core on Linux) and a PostgreSQL database. PostgreSQL isn't a problem, included in the diagram for completeness. It's mapped to 9876 because I also have a PostgreSQL instance running on the host for now. mco is a shortened name of our company.

I have been following the Identity Server 4 instructions to get up and running.

Code

I'm not including the docker-compose.debug.yml because it has run commands pertinent only to running in Visual Studio.

docker-compose.yml

version: '2'

services:
mcodatabase:
    image: mcodatabase
    build:
    context: ./Data
    dockerfile: Dockerfile
    restart: always
    ports:
    - 9876:5432
    environment:
    POSTGRES_USER: mcodevuser
    POSTGRES_PASSWORD: password
    POSTGRES_DB: mcodev
    volumes:
    - postgresdata:/var/lib/postgresql/data
    networks:
    - mconetwork

mcoidentityserver:
    image: mcoidentityserver
    build:
    context: ./Mco.IdentityServer
    dockerfile: Dockerfile
    ports:
    - 5000:5000
    networks:
    - mconetwork

mcoapi:
    image: mcoapi
    build:
    context: ./Mco.Api
    dockerfile: Dockerfile
    ports:
    - 56107:80
    links:
    - mcodatabase
    depends_on:
    - "mcodatabase"
    - "mcoidentityserver"
    networks:
    - mconetwork

volumes:
postgresdata:

networks:
mconetwork:
    driver: bridge

docker-compose.override.yml

This is created by the Visual Studio plugin to inject extra values.

version: '2'

services:
mcoapi:
    environment:
    - ASPNETCORE_ENVIRONMENT=Development
    ports:
    - "80" 

mcoidentityserver:
    environment:
    - ASPNETCORE_ENVIRONMENT=Development
    ports:
    - "5000" 

API Dockerfile

FROM microsoft/aspnetcore:1.1
ARG source
WORKDIR /app
EXPOSE 80
COPY ${source:-obj/Docker/publish} .
ENTRYPOINT ["dotnet", "Mco.Api.dll"]

Identity Server Dockerfile

FROM microsoft/aspnetcore:1.1
ARG source
WORKDIR /app
COPY ${source:-obj/Docker/publish} .
EXPOSE 5000
ENV ASPNETCORE_URLS http://*:5000
ENTRYPOINT ["dotnet", "Mco.IdentityServer.dll"]

API Startup.cs

Where we tell the API to use the Identity Server and set the Authority.

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    loggerFactory.AddConsole(Configuration.GetSection("Logging"));
    loggerFactory.AddDebug();

    app.UseIdentityServerAuthentication(new IdentityServerAuthenticationOptions
    {
        // This can't work because we're running in docker and it doesn't understand what localhost:5000 is!
        Authority = "http://localhost:5000", 
        RequireHttpsMetadata = false,

        ApiName = "api1"
    });

    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Home/Error");
    }

    app.UseMvc(routes =>
    {
        routes.MapRoute(
            name: "default",
            template: "{controller=Home}/{action=Index}/{id?}");
    });
}

Identity Server Startup.cs

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddIdentityServer()
            .AddTemporarySigningCredential()
            .AddInMemoryApiResources(Config.GetApiResources())
            .AddInMemoryClients(Config.GetClients());
    }

    public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
    {
        loggerFactory.AddConsole();

        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        app.UseIdentityServer();

        app.Run(async (context) =>
        {
            await context.Response.WriteAsync("Hello World!");
        });
    }
}

Identity Server Config.cs

public class Config
{
    public static IEnumerable<ApiResource> GetApiResources()
    {
        return new List<ApiResource>
        {
            new ApiResource("api1", "My API")
        };
    }

    public static IEnumerable<Client> GetClients()
    {
        return new List<Client>
        {
            new Client
            {
                ClientId = "client",

                // no interactive user, use the clientid/secret for authentication
                AllowedGrantTypes = GrantTypes.ClientCredentials,

                // secret for authentication
                ClientSecrets =
                {
                    new Secret("secret".Sha256())
                },

                // scopes that client has access to
                AllowedScopes = { "api1" }
            }
        };
    }
}

Client

Running in a console app.

var discovery = DiscoveryClient.GetAsync("localhost:5000").Result;
var tokenClient = new TokenClient(discovery.TokenEndpoint, "client", "secret");
var tokenResponse = tokenClient.RequestClientCredentialsAsync("api1").Result;

if (tokenResponse.IsError)
{
    Console.WriteLine(tokenResponse.Error);
    return 1;
}

var client = new HttpClient();
client.SetBearerToken(tokenResponse.AccessToken);

var response = client.GetAsync("http://localhost:56107/test").Result;
if (!response.IsSuccessStatusCode)
{
    Console.WriteLine(response.StatusCode);
}
else
{
    var content = response.Content.ReadAsStringAsync().Result;
    Console.WriteLine(JArray.Parse(content));
}

Thanks in advance.

Dr Rob Lang
  • 6,659
  • 5
  • 40
  • 60

3 Answers3

24

Ensure IssuerUri is set to an explicit constant. We had similar issues with accessing Identity Server instance by the IP/hostname and resolved it this way:

services.AddIdentityServer(x =>
{
    x.IssuerUri = "my_auth";
})

P.S. Why don't you unify the authority URL to hostname:5000? Yes, it is possible for Client and API both call the same URL hostname:5000 if:

  • 5000 port is exposed (I see it's OK)
  • DNS is resolved inside the docker container.
  • You have access to hostname:5000 (check firewalls, network topology, etc.)

DNS is the most tricky part. If you have any trouble with it I recommend you try reaching Identity Server by its exposed IP instead of resolving hostname.

Ilya Chumakov
  • 23,161
  • 9
  • 86
  • 114
  • +1 Just wanted to write a quick thank you, your P.S. has given me some more things to investigate. I think setting the `IssuerUri` might well be the correct way to do it for our domain problem. I'll come back and update when I've completed my investigation. Thank you! – Dr Rob Lang May 12 '17 at 08:49
  • I am keeping this as the correct answer as it put me on the right track, I have added the complete code in my answer below. – Dr Rob Lang Jul 28 '17 at 10:14
  • If you set `IssuerUri` won't it be different from the `Authority` property that you define on `UseIdentityServerAuthentication()`, leading to the error `Bearer error="invalid_token", error_description="The issuer 'my_auth' is invalid"`? – Pedro Faustino Apr 16 '20 at 20:29
15

To make this work I needed to pass in two environment variables in the docker-compose.yml and setup CORS on the identity server instance so that the API was allowed to call it. Setting up CORS is outside the remit of this question; this question covers it well.

Docker-Compose changes

The identity server needs IDENTITY_ISSUER, which is name that the identity server will give itself. In this case I've used the IP of the docker host and port of the identity server.

  mcoidentityserver:
    image: mcoidentityserver
    build:
      context: ./Mco.IdentityServer
      dockerfile: Dockerfile
    environment:
      IDENTITY_ISSUER: "http://10.0.75.1:5000"
    ports:
       - 5000:5000
    networks:
     - mconetwork

The API needs to know where the authority is. We can use the docker network name for the authority because the call doesn't need to go outside the docker network, the API is only calling the identity server to check the token.

  mcoapi:
    image: mcoapi
    build:
      context: ./Mco.Api
      dockerfile: Dockerfile
    environment:
      IDENTITY_AUTHORITY: "http://mcoidentityserver:5000"
    ports:
       - 56107:80
    links:
     - mcodatabase
     - mcoidentityserver
    depends_on:
     - "mcodatabase"
     - "mcoidentityserver"
    networks:
     - mconetwork

Using these values in C#

Identity Server.cs

You set the Identity Issuer name in ConfigureServices:

    public void ConfigureServices(IServiceCollection services)
    {
        var sqlConnectionString = Configuration.GetConnectionString("DefaultConnection");

        services
            .AddSingleton(Configuration)
            .AddMcoCore(sqlConnectionString)
            .AddIdentityServer(x => x.IssuerUri = Configuration["IDENTITY_ISSUER"])
            .AddTemporarySigningCredential()
            .AddInMemoryApiResources(Config.GetApiResources())
            .AddInMemoryClients(Config.GetClients())
            .AddCorsPolicyService<InMemoryCorsPolicyService>()
            .AddAspNetIdentity<User>();
    }

API Startup.cs

We can now set the Authority to the environment variable.

app.UseIdentityServerAuthentication(new IdentityServerAuthenticationOptions
    {
        Authority = Configuration["IDENTITY_AUTHORITY"],
        RequireHttpsMetadata = false,
        ApiName = "api1"
    });

Drawbacks

As shown here, the docker-compose would not be fit for production as the hard coded identity issuer is a local IP. Instead you would need a proper DNS entry that would map to the docker instance with the identity server running in it. To do this I would create a docker-compose override file and build production with the overridden value.

Thanks to ilya-chumakov for his help.

Edit

Further to this, I have written up the entire process of building a Linux docker + ASP.NET Core 2 + OAuth with Identity Server on my blog.

Dr Rob Lang
  • 6,659
  • 5
  • 40
  • 60
  • I have asked a similar question here if you would like to take a look: https://stackoverflow.com/questions/53468074/integrating-identity-server-docker-and-mvc-core-web-client +1 for the reference to CORS. – w0051977 Nov 25 '18 at 13:43
  • 3
    "We can use the docker network name for the authority because the call doesn't need to go outside the docker network, the API is only calling the identity server to check the token." What about authorization code flow? My browser doesn't know what "docker network name" you setup. :( – Ömer Cinbat Jul 04 '19 at 13:07
  • @ÖmerCinbat please ask your own question, you're more likely to get a good answer in return. – Dr Rob Lang Jul 04 '19 at 17:08
  • 1
    I followed the quickstart guide on the docs page, and it's annoying that the `IssuerUri` is not included from the start - it's an essential piece of configuration. – helloserve Jul 24 '19 at 12:06
  • @ÖmerCinbat Have you found answer on your question? I'm facing with the same problem. – Astemir Almov Nov 24 '20 at 13:03
  • 1
    @AstemirAlmov it's been a long time now but as I remembered I ignored this issue and added docker network name to my hosts file in winodws os so the browser knew what address to call. – Ömer Cinbat Feb 03 '21 at 13:58
2

If you are running your docker containers in same network, you can do the followings:

  1. Add IssuerUri in your identity server.
services.AddIdentityServer(x =>
            {
                x.IssuerUri = "http://<your_identity_container_name>";
            })

This will set your identity server's URI. Therefore, your other web api services can use this URI to reach your identity server.

  1. Add Authority in your web api that has to use identity server.
services.AddAuthentication(options =>
          {
              options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
              options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
          }).AddJwtBearer(o =>
          {
              o.Authority = "http://<your_identity_container_name>";
              o.Audience = "api1"; // APi Resource Name
              o.RequireHttpsMetadata = false;
              o.IncludeErrorDetails = true;
          });