This is a description of how I am solving the issue of IdenityServer4 not working when
I have a micro-service solution, loosely based on the Microsoft Micro-Service guidance. It is using ASP.NET Core 1.1. When I attempted to us Nginx to expose all the services on ports 80 and 443 using Host Header mapping, the Authentication failed due to the difference in the internal and external urls for the AuthorityUri and the Metadata UrI.
Some of these issues appear to be corrected in V2, but we are not ready to convert the solution at this time.
The Docker Compose file service entry for the Nginx service needs to depend on all the services that Nginx will be mapping to. If this is not done, then the services will not be started before Nginx and therefore there names will not be resolvable by the Docker DNS. This results in an exception in Nginx and it stops.
For example:
~
nginx:
image: nginx
ports:
- "80:80"
depends_on:
- webmvc
- webstatus
- my-api
- identity
~
If the AuthoriyUri is set to the internal uri, such as http://identity, then IdentityServer does HTTP redirects to the internal uri, rather than the external uri. The browser cannot resolve this address and the redirection fails.
If the AuthorityUri is set to the eternal uri, then the internal services cannot resolve the DiscoveryUri as it uses the external http://identity.mydomain.com/.well-known/openid-configuration value.
This can be fixed with
The problem is that IdentityServer will use the request's hostname to set the IssuerUri, which will be the internal uri, not the eternal uri as desired. This is solve by explicitly setting the IssuerUri
~ c#
// Adds IdentityServer
services.AddIdentityServer(options =>
{
options.IssuerUri = "http://identity.mydomain.com";
})
.AddDeveloperSigningCredential()
~
Add a middleware to change the Location Header for any redirects to the IdentityServer's internal uri. This should come before any authentication middleware.
~~~ c#
app.Use(async (httpcontext, next) =>
{
await next();
if (httpcontext.Response.StatusCode == StatusCodes.Status302Found)
{
string location = httpcontext.Response.Headers[Microsoft.Net.Http.Headers.HeaderNames.Location];
httpcontext.Response.Headers[Microsoft.Net.Http.Headers.HeaderNames.Location] =
location.Replace("://identity/", "://identity.mydomain.com/");
}
});
app.UseCookieAuthentication(new CookieAuthenticationOptions
~ c#
In addition, you need to set the 'MetadataAddress' property in the 'OpenIdConnectOptions' to allow the server get the Discovery Document for the OpenID Configuration. For example:
~
identityUrl = Configuration.GetValue
app.UseOpenIdConnectAuthentication(new OpenIdConnectOptions
{
AuthenticationScheme = OpenIdConnectDefaults.AuthenticationScheme,
SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme,
PostLogoutRedirectUri = "/",
Authority = identityUrl,
MetadataAddress = "http://identity/.well-known/openid-configuration",
RequireHttpsMetadata = false,
ClientId = clientId,
ClientSecret = clientSecret,
ResponseType = "code id_token",
Scope = {
OpenIdConnectScope.OpenIdProfile,
"offline_access",
"iot.gateway.manage"
},
GetClaimsFromUserInfoEndpoint = true,
SaveTokens = true,
});
~~~
Similarly, we need to set the MetadatAddress in the when initialzing the IdentityServer Authentication middleware. This is a little trick and requires digging into the JwtBearerOptions as shown below
~~~ c#
var identityUrl = Configuration.GetValue
IdentityServerAuthenticationOptions idsrvOptions = new IdentityServerAuthenticationOptions
{
RequireHttpsMetadata = false, // shouldn't be allowed in production
Authority = identityUrl,
ApiName = "myapi",
ApiSecret = "secret",
AutomaticAuthenticate = true,
AutomaticChallenge = true,
//EnableCaching = true,
//CacheDuration = TimeSpan.FromMinutes(10) // that's the default
};
var combinedOptions = CombinedAuthenticationOptions.FromIdentityServerAuthenticationOptions(idsrvOptions);
combinedOptions.JwtBearerOptions.MetadataAddress = "http://identity/.well-known/openid-configuration";
app.UseIdentityServerAuthentication(combinedOptions);
app.UseWebSockets();
app.UseMvc();
if (env.IsDevelopment())
{
// this will do the initial DB population
InitializeDatabase(app);
}
}
~~~
This appears to 'fix' my issues. The MVC app can log in and access the protected API.
I still have to configure the Nginx proxy for SSL termination, which I am sure this will result in a few other issues. I'll update this issue as I proceed.
I never used nginx myself. So no idea.
Neither had I, however, I am setting up a project with an MVC, WebAPI, IdentityServer in Docker Linux containers.
I'm using Nginx as the reverse proxy with SSL termination and the difference in the external URLs and Schemes is tricky to handle, especially with .NET Core 1.1. I thought that what I found would be useful.
This will probably be a fairly common scenario given the Xplat nature of .NET Core and the micro-services hype, and is probably something that should be documented.
I've just about got the SSL stuff working and will update my original comment to reflect the additional changes.
Hi Matthew, very interesting I'm running a similar project on Ubuntu 16.04 LTS running IDS4 with NGINX as reverse to Kestrel. I made some progress actually Identity Server is running on https://mydomain.com and my MvcClient and API (taken from Quickstart examples) are working correctly on my DEV Environment. As soon as I publish them to the same server the Secure Content is not longer found, I receive https 404 not found error. My version of dotnet is 2.x. I'll try your configuration I'm wondering about what you are saying concerning the internal / external URLs. Keep you posted.
Ok Matt I mad it works with the following config tested after publish using MVC Implicit and Hybrid clients:
events {
worker_connections 768;
# multi_accept on;
}
http {
##
# Basic Settings
##
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
client_body_timeout 10; client_header_timeout 10; send_timeout 10;
upstream idsrv {
server 127.0.0.1:44320;
}
upstream mvclient {
server 127.0.0.1:44321;
}
server_tokens off;
# server_names_hash_bucket_size 64;
# server_name_in_redirect off;
include /etc/nginx/mime.types;
default_type application/octet-stream;
include /etc/nginx/proxy.conf;
##
# SSL Settings
##
#server {
#listen 80 default_server;
#listen [::]:80 default_server;
#return 200 https://$host$request_uri;
#}
server{
listen 80 default_server;
listen [::]:80 default_server;
server_name myswisscompany.ch;
#listen *:80;
return 301 https://$host$request_uri;
}
server{
listen *:443 ssl http2;
listen [::]:443 ssl http2;
server_name myswisscomany.ch;
#listen 80;
#listen 443;
ssl_certificate /etc/ssl/certs/fullchain.pem;
ssl_certificate_key /etc/ssl/private/privkey.pem;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # Dropping SSLv3, ref: POODLE
ssl_prefer_server_ciphers on;
ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH";
ssl_ecdh_curve secp384r1;
ssl_session_cache shared:SSL:50m;
ssl_session_timeout 1d;
ssl_session_tickets off;
ssl_dhparam /etc/ssl/certs/dhparam.pem;
ssl_stapling on; #ensure your cert is capable
ssl_stapling_verify on; #ensure your cert is capable
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;
#the next four lines of the configuration of nginx.conf is what made MVC Hybrid work correctly after publish:
proxy_buffer_size 128k;
proxy_buffers 4 256k;
proxy_busy_buffers_size 256k;
large_client_header_buffers 4 16k;
#Redirects all traffic
location /{
#MY IDS SERVER REPLIES ON THE ROOT DIRECTORY-> https://mycompany.com
#rewrite ^/ids(.*) /$1 break;
proxy_pass https://idsrv;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection keep-alive;
#proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;# must be set or app will not work
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;#must be set or wrong url
proxy_set_header X-Forwarded-Proto $scheme;#must be set or wrong url
#proxy_redirect off;
proxy_cache_bypass $http_upgrade;
proxy_set_header X-URL-SCHEME https;
}
location /mvc{
#rewrite ^/mvc(.*) /$1 break;
proxy_pass https://mvclient;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection keep-alive;
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-URL-SCHEME https;
}
}
Hybrid MW - Startup.cs:
//n my case I have both IDS and MVC client Hosted in the same web server
public void Configure(IApplicationBuilder app)
{
app.Map("/mvc", mapping =>{
mapping.UseForwardedHeaders(new ForwardedHeadersOptions
{
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
});
mapping.Use(async (context, next) =>
{
context.Request.Scheme = "https";
await next.Invoke();
});
mapping.UseStaticFiles();
mapping.UseAuthentication();
mapping.UseMvc(routes => {
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
});
*My Program.cs*
//in .net 2.x there are some differences
namespace MvcHybrid
{
public class Program
{
public static void Main(string[] args)
{
BuildWebHost(args).Run();
}
public static IWebHost BuildWebHost(string[] args)
{
return WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.UseKestrel(options =>
{
options.Listen(IPAddress.Loopback, 44321, listenoptions =>
{
listenoptions.UseHttps("mysslcert.pfx","myaccesspassword");
});
})
.Build();
}
}
I hope this could help.
Regards. j
All set on this issue -- can we close?
Yes, there are number of ways to work around this for v1 and v2. You just need to understand which url is being requested and which is needed for each service. The issue is whether the docker internal or public facing url is required. A little middleware can solve this.
Matthew
Sent from Mail for Windows 10
From: Brock Allen
Sent: Saturday, November 11, 2017 3:35 PM
To: IdentityServer/IdentityServer4
Cc: Matthew Dennis; Author
Subject: Re: [IdentityServer/IdentityServer4] Using IdentityServer4 V1 behinda Nginx Reverse Proxy in Docker (#1623)
All set on this issue -- can we close?
—
You are receiving this because you authored the thread.
Reply to this email directly, view it on GitHub, or mute the thread.
Thanks.
if (!env.IsDevelopment())
{
app.Use((context, next) =>
{
context.Request.Scheme = "https";
return next();
});
}
Following did the trick for me:
Service config
services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders =
ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedHost;
});
middleware config
app.UseForwardedHeaders();
And from the proxy, following headers were sent
X-Forwarded-Host
X-Forwarded-Proto
This helped https://docs.microsoft.com/en-us/aspnet/core/host-and-deploy/proxy-load-balancer?view=aspnetcore-2.2
This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.