web-dev-qa-db-fra.com

Comment puis-je utiliser IdentityServer4 de l'intérieur et de l'extérieur d'une machine Docker?

Je veux pouvoir m'authentifier auprès d'un Identity Server (STS) de l'extérieur et de l'intérieur d'un docker.

J'ai du mal à définir l'autorité correcte qui fonctionne à l'intérieur et à l'extérieur du conteneur. Si je mets l'autorité sur le nom interne mcoidentityserver:5000 alors l'API peut s'authentifier mais le client ne peut pas obtenir de jeton car le client se trouve en dehors du réseau docker. Si je mets l'autorité sur le nom externe localhost:5000 alors le client peut obtenir un jeton mais l'API ne reconnaît pas le nom de l'autorité (car localhost dans ce cas est la machine hôte).

À quoi dois-je définir l'autorité? Ou peut-être ai-je besoin d'ajuster le réseau Docker?

Diagramme

La flèche rouge est la partie avec laquelle j'ai des problèmes. Three docker containers in a network, a client and PostgreSQL Admin, their ports and a red arrow showing where I think the problem lies.

Détail

Je mets en place un environnement de développement Docker Windows 10 qui utilise une API ASP.NET Core (sous Linux), Identity Server 4 (ASP.NET Core sous Linux) et une base de données PostgreSQL. PostgreSQL n'est pas un problème, inclus dans le diagramme pour être complet. Il est mappé sur 9876 car j'ai également une instance PostgreSQL en cours d'exécution sur l'hôte pour l'instant. mco est un nom abrégé de notre société.

J'ai suivi les instructions d'Identity Server 4 pour être opérationnel.

Code

Je n'inclus pas le docker-compose.debug.yml car il a exécuté des commandes pertinentes uniquement pour l'exécution dans 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

Ceci est créé par le plugin Visual Studio pour injecter des valeurs supplémentaires.

version: '2'

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

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

Dockerfile API

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

Dockerfile Identity Server

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

Où nous disons à l'API d'utiliser le serveur d'identité et de définir l'autorité.

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

Startup.cs d'Identity Server

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

Config.cs Identity Server

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

Exécution dans une application console.

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

Merci d'avance.

26
Dr Rob Lang

Assurez-vous que IssuerUri est défini sur une constante explicite. Nous avons eu des problèmes similaires avec l'accès à l'instance Identity Server par l'IP/nom d'hôte et nous l'avons résolu de cette façon:

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

P.S. Pourquoi ne pas unifier l'URL de l'autorité à hostname:5000? Oui, il est possible pour Client et API d'appeler la même URL hostname:5000 si:

  • Le port 5000 est exposé (je vois que c'est OK)
  • DNS est résolu à l'intérieur du conteneur Docker.
  • Vous avez accès à hostname:5000 (vérifier les pare-feu, la topologie du réseau, etc.)

DNS est la partie la plus délicate. Si vous rencontrez des problèmes, je vous recommande d'essayer d'atteindre Identity Server par son IP exposée au lieu de résoudre hostname.

15
Ilya Chumakov

Pour que cela fonctionne, je devais passer deux variables d'environnement dans le docker-compose.yml et configuration CORS sur l'instance du serveur d'identité afin que l'API soit autorisée à l'appeler. La mise en place de CORS sort du cadre de cette question; cette question le couvre bien.

Modifications de Docker-Compose

Le serveur d'identité a besoin de IDENTITY_ISSUER, qui est le nom que le serveur d'identité se donnera. Dans ce cas, j'ai utilisé le IP du Docker Host et le port du serveur d'identité.

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

L'API doit savoir où se trouve l'autorité. Nous pouvons utiliser le nom du réseau de docker pour l'autorité, car l'appel n'a pas besoin de sortir du réseau de docker, l'API appelle uniquement le serveur d'identité pour vérifier le jeton.

  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

Utilisation de ces valeurs en C #

Identity Server.cs

Vous définissez le nom de l'émetteur d'identité dans 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

Nous pouvons maintenant définir l'Autorité sur la variable d'environnement.

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

Désavantages

Comme indiqué ici, le docker-compose ne serait pas apte à la production car l'émetteur d'identité codé en dur est une adresse IP locale. Au lieu de cela, vous auriez besoin d'une entrée DNS appropriée qui serait mappée à l'instance de docker avec le serveur d'identité en cours d'exécution. Pour ce faire, je créerais un fichier de substitution docker-compose et générerais la production avec la valeur substituée.

Merci à ilya-chumakov pour son aide.

Éditer

En plus de cela, j'ai écrit tout le processus de construction d'un Linux docker + ASP.NET Core 2 + OAuth avec Identity Server sur mon blog.

8
Dr Rob Lang