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?
La flèche rouge est la partie avec laquelle j'ai des problèmes.
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.
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.
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:
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
.
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.
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
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"
});
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.
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.