Dr Rob Lang
Dr Rob Lang

Reputation: 6903

How can I use IdentityServer4 from inside and outside a docker machine?

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.

Upvotes: 42

Views: 14455

Answers (3)

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

Upvotes: 2

Ilya Chumakov
Ilya Chumakov

Reputation: 25039

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.

Upvotes: 24

Dr Rob Lang
Dr Rob Lang

Reputation: 6903

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.

Upvotes: 15

Related Questions