Before getting to the question - which is how do we solve the infinite authentication loop - some information regarding architecture.
We are using .net core 2.1.
We have 2 services. The first one is the one that's facing the public traffic, does the TLS termination and figures out if the request should be passed on or not. (Perhaps to other servers) When this server figures out that the request is made to a certain path, it uses RunProxy method to map the request to the 'other' service using http. That code looks like below:
app.MapWhen(<MatchRequestCondition>, proxyTime => proxyTime.RunProxy(
new ProxyOptions
{
Scheme = "http",
Host = "localhost",
Port = "1122"
}
));
As an example, if you visit https://localhost:1234/abc - this would be mapped to http://localhost:1122 - which is the port where the second application lives.
Now, this secondary service uses OpenIdConnect - the configuration of it looks like below.
// Configure Services method
services.AddMvc(mvcOptions => {
AuthorizationPolicy policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
mvcOptions.Filters.Add(new AuthorizeFilter(policy));
});
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(auth =>
{
auth.ClientId = "<client_id>";
auth.ClientSecret = "<client_secret>";
auth.Authority = "<authority>";
});
// Configure method
app.UseAuthentication();
Here's where it gets interesting:
If I visit the second node (the one that's meant to receive traffic from the first one only) directly - like http://localhost:1122 - I'm redirected to sign-in and everything works correctly.
But if I visit the first node (which is the one that the real traffic should be coming from) - it goes into a crazy authentication loop.
Any ideas to what might be the root cause? How is this different than having a load balancer in front of the regular service? Or perhaps it's because I'm using the cookie middleware in the secondary service?
Related
I am hosting identity server behind a reverse proxy on a subpath of the root url (example.com/subpath).
For external api's behind the same proxy (example.com/apisubpath) token validation is working correctly.
Now I added an api on the same service that hosts IdentityServer as documented in
Adding more API endpoints.
Requesting a token with the scope IdentityServerApi is working fine and when testing it in my local dev environment without the reverse proxy I call the api successfully.
Behind the reverse proxy I get an error: Microsoft.IdentityModel.Tokens.SecurityTokenInvalidIssuerException: IDX10205: Issuer validation failed. Issuer: 'https://example.com/subpath'. Did not match: validationParameters.ValidIssuer: 'https://example.com' or validationParameters.ValidIssuers: 'null'.
So it gets the right domain from the requests but it does'nt include the sub path when 'registering' the validationParameters.ValidIssuer it seems.
Is there a way to set the validationParameters.ValidIssuer manually or am I doing something else wrong?
Thanks for your help
There're a few way to get this thing work, with Identity Server 4
If no explicit Issuer was set on Identity Server, it's would inspect the Host header that came from client request. Otherwise just explicitly set Issuer on Identity Server 4:
// This one was on the Identity server
services.AddIdentityServer(opts => opts.IssuerUri = "The explicit Url came here!")
Don't wanna explicit set Issuer on server but rather let Identity Server figure out Issuer on its own ?
// This one was on the other services, If not set to the same instance of Identity Server
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false, // If set to true, It's gonna take care of the issuer
ValidIssuer = "If we just have a single Issuer, set it here",
ValidIssuers = new []{"Multiple", "Issuer", "Came", "Here"}
};
});
Your current situation should be when taking token out of the server, the request to connect/token have Host header of https://example.com/subpath, while it should be https://example.com, so... just choose some of the options above that suit the most
Firstly, there is no such specific check/relation between deployment strategy, it's all about how you are validating the token. As you haven't posted the ID4 validation
middleware configuration I will just explain what exactly happens while validating the token.
Token Creation:
I think there is a lot of confusion regarding ID4 Issuer validation, So let's deep dive into it. By default ID4 includes the hosting path as an issuer, lets say if you have an ID4 app deployed in IIS and its virtual path is https://localhost/IdentityServer then this path would be your issuer in the token. Here you can verify your token. So as it's taking issuer as your hosted application path, which means issuer would be different for each IdS4 instance which is deployed in different environments (it's helpful when you don't want to share your token with multiple environments (Staging and Production) or instances). But you can anyway set this issuer URL in the ID4 app by setting the below option.
services.AddIdentityServer(options => {
options.IssuerUri = "https://MyDNS/Common"
});
By doing this you should be able to validate your token across the environment or IdS4 instances. If you don't want a static issuer which may lead to a security loophole where the user can your pre-prod environments token to call prod API then you may be interested in the next section which is middleware configuration in consumer API.
Middleware configuration using .AddJwtBearer:
There is another middleware developed by the IdS4 team themselve (.AddIdentityServerAuthentication()) but they are no longer maintaining the code and as per the latest documents they are suggesting .AddJwtBearer().
In the previous section, we covered the token provider's configuration (In our case IdS4) to set static issuer but in case you don't want to do that as it opens up security concerns then consumer configuration would be your next step to follow. Below code snippet is the basic minimum configuration required to validate the token.
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = "https://MyID4Domain/"
options.RequireHttpsMetadata = false; // Non-PROD
options.TokenValidationParameters.ValidateAudience = false;
options.Events = new JwtBearerEvents
{
OnAuthenticationFailed = context =>
{
Log.Logger.Error($"Authentication failed. {context.Exception.InnerException}");
return Task.CompletedTask;
}
};
});
If you have your WebApi protected using above code then .AddJwtBearer() middleware by default expects options.Authority URL as an issuer in our case it's https://MyID4Domain/, but you can override that check with a valid issuer which is expected in your token.
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters.ValidIssuer = "SomeOtherIssuer"; // If you have different issuer than your Authority url.
**OR**
options.TokenValidationParameters.ValidIssuers = ["Issuer1", "issuer2", "issuer3"]; // In case deployed on multiple instances and wanted to validate token which is generated from any of these instance.
// Kind of multi-tenant support. I know there is a different mechanism that is coming along with ID5.
**OR**
options.TokenValidationParameters.ValidateIssuer = false; // If you don't want to validate issuer at all.
}
Happy coding!
I have an application (.NET 5.0 ASP Net Core) application that I am trying to deploy to an AWS Amazon Linux 2 server. It appears that all aspects of deployment are fine except for authorization with AWS Congnito and Microsoft.AspNetCore.Authentication.OpenIdConnect. Everything works fine in dev/local and the problems only exhibit themselves when in prod deployment.
The issue exhibits itself as an "An error was encountered with the requested page." at https://auth.<mydomain>.com/error?error=redirect_mismatch&client_id=<myclientid> in the Hosted UI when trying to login. I have confirmed and reconfirmed that the Callback URL(s) are set correctly: https://sub.domain.com/signin-oidc, https://localhost:5001/signin-oidc.
My app is running on http://localhost:5000 behind an apache reverse proxy. I suspect that the non-HTTPS portion of the path between Apache and Kestrel is the issue.
What I have noticed is that Microsoft.AspNetCore.Authentication.OpenIdConnect is lacking https in the redirect_uri value that it creates as part of the /oauth2/authorize endpoint it calls.
This is what I see in Dev (no issues):
This is what I see when deployed, note that the redirect_uri is http:
In the App client settings, I can't set the signin-oidc endpoint to use the HTTP.
My ConfigureServices:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(options =>
{
options.ResponseType = "code";
options.ResponseType = Configuration["Authentication:Cognito:ResponseType"];
options.MetadataAddress = Configuration["Authentication:Cognito:MetadataAddress"];
options.ClientId = Configuration["Authentication:Cognito:ClientId"];
options.TokenValidationParameters = new TokenValidationParameters
{
RoleClaimType = "cognito:groups"
};
options.Events = new OpenIdConnectEvents
{
OnTicketReceived = e =>
{
e.ReturnUri = string.Format("/Home/CheckProfile?url={0}", HttpUtility.UrlEncode(e.ReturnUri));
return Task.CompletedTask;
}
};
});
}
So, why is Microsoft.AspNetCore.Authentication.OpenIdConnect using HTTP when it generates the redirect_uri value of the /oauth2/authorize endpoint. Is that somethign that I need to adjust somewhere? And, does that appear to be the core issue that results in my overall https://auth.<mydomain>.com/error?error=redirect_mismatch&client_id=<myclientid> issue?
The core issue here was the reverse proxy; Kestrel running behind Apache. While I had used this setup (with certbot) regularly over the past few years, I had not previously used it with a OIDC auth scheme. The issue was the https termination at apache and the http transmission between Apache and Kestrel. An OIDC auth scheme (in my case supported by AWS Cognito) needs end-to-end https.
The "lacking https in the redirect_uri value that it creates as part of the /oauth2/authorize endpoint" was just the first of many issues I uncovered. I came up with a solution for that issue:
.AddOpenIdConnect(options =>
{
...
options.Events = new OpenIdConnectEvents
{
...
OnRedirectToIdentityProvider = async n =>
{
n.ProtocolMessage.RedirectUri = n.ProtocolMessage.RedirectUri.Replace("http://", "https://");
await Task.FromResult(0);
}
};
});
But this only solved the narrow issue of changing the redirect_uri proto; other cookie SameSite=None/Secure/http issues then appeared.
At this point, I have had success directly exposing Kestrel on 80 and 443. I realize that it's debatable whether this is a prudent idea, but it's working for me at the moment and today (Summer 2021 on .NET 5.0) it seems like Kestrel is maturing to the point where it is not one of those "only do this in development!" tools.
I found both of these articles very helpful:
https://swimburger.net/blog/dotnet/how-to-run-aspnet-core-as-a-service-on-linux
https://thecodeblogger.com/2021/05/07/certificates-and-limits-for-asp-net-core-kestrel-web-server/
Better answer. While the "Kestrel exposed to the world" answer worked, I ended up figuring out how to make the reverse proxy work with Cognito.
In the reverse proxy I ended up setting "'https' env=HTTPS" as shown here:
<VirtualHost *:*>
RequestHeader set "X-Forwarded-Proto" 'https' env=HTTPS
</VirtualHost>
I also rearanged my Prod Configure(...) as follows:
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseForwardedHeaders();
}
else
{
app.UseExceptionHandler("/Home/Error");
app.UseForwardedHeaders();
app.Use((ctx, next) =>
{
ctx.Request.Host = new HostString("sub.domain.com");
ctx.Request.Scheme = "https";
return next();
});
app.UseHsts();
}
So, we have a SPA that is calling a series a microservices that sits behind an API Gateway. The authentication scheme is OpenId flow against Azure AD. So, it goes as follows: SPA (Public client) gets the access code, then the API gateway (Confidential Client) call the token service in order to get the access token; this is what it gets forwarded to the microservices themselves. So, in the end, our microservices will receive just the non-JWT Access Token. It's important to note that this token is NOT a JWT. In order to validate we're forced to use the /openId/UserInfo endpoint in the azure tenant to check the user with the access token.
We've tried used the AddOpenIdConnect extension in startup, as described [here] https://learn.microsoft.com/en-us/dotnet/architecture/microservices/secure-net-microservices-web-applications/
{
//…
// Configure the pipeline to use authentication
app.UseAuthentication();
//…
app.UseMvc();
}
public void ConfigureServices(IServiceCollection services)
{
var identityUrl = Configuration.GetValue<string>("IdentityUrl");
var callBackUrl = Configuration.GetValue<string>("CallBackUrl");
// Add Authentication services
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(options =>
{
options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.Authority = identityUrl;
options.SignedOutRedirectUri = callBackUrl;
options.ClientSecret = "secret";
options.SaveTokens = true;
options.GetClaimsFromUserInfoEndpoint = true;
options.RequireHttpsMetadata = false;
options.Scope.Add("openid");
options.Scope.Add("profile");
options.Scope.Add("orders");
options.Scope.Add("basket");
options.Scope.Add("marketing");
options.Scope.Add("locations");
options.Scope.Add("webshoppingagg");
options.Scope.Add("orders.signalrhub");
});
}
But this assumes the whole OpenId flow. Regardless of putting the Bearer token in the request, application redirects to the login page.
So the question is, is there any out-of-the-box configuration for this? Or we should rely in some custom handler? In such case, how the user can be properly authenticated within the context? We would need to access the HttpContext.User.Claims in the controllers themselves.
Any hint would be greatly appreciated!
Could I ask a couple of questions - partly for my own understanding:
What causes the token to not be a JWT - I don't think the link you posted explains this?
Are you using some form of token exchange from the original one issued to the SPA?
I have an equivalent solution that works, but I am using a free developer account, so maybe my setup is different:
Token validation code
Write up on messages and config
If I understand how to reproduce your setup - and a rough understanding of the config differences - I may be able to help.
Using the user info endpoint to validate tokens doesn't feel right. It would be good to get token validation working in the standard way.
I have an angular application that communicates with a .Net Core REST API. I also have an authorization server that is installed on the same server as the API. The API validates the authorization token it receives from angular against the authorization server.
My plan was to set everything up behind a reverse proxy so that internal communication happens over http between the API and the authorization server in order to simplify the management of the certificates since there will be multiple instances of the services. External access would still happen over https.
In order to to do this I need to set the RequireHttpsMetadata property in JwtBearerOptions to False, but the documentation says that this should always be True when used in production. But considering that in my case the communication happens internally would it be okay if it's false?
public void ConfigureServices(IServiceCollection services)
{
services.AddCors();
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
{
options.Authority = "http://localhost:8080/xxxxxx";
options.RequireHttpsMetadata = false;
});
}
I was looking into the same issue and found a github issue discussing this same scenario.
The comments and code link there indicate that it validates the https connection for downloading metadata documents. So your reverse proxy should not impact this process and it should be left as true for security reasons.
I am developing an ASP.Net Core 2.0 application in Azure Service Fabric. This application uses Azure Ad Authentication. This authentication requires a registered reply URL in the Azure portal. Service fabric however assigns a port to my application based on the available ports. How can I implement this way of authentication with dynamically assigned ports without registering over one hundred different reply URLs? The ports are currently resolved through the use of the Service Fabric reverse proxy.
In my startup I have entered my Azure Ad config in ConfigureServices with:"
services.AddAzureAd(options =>
{
Configuration.Bind("AzureAd", options);
})
My appsettings contains:
{
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"Domain": "mydomain.com",
"TenantId": "AD tenant id (GUID)",
"ClientId": "registered app id (GUID)",
"CallbackPath": "/signin-oidc"
},
...
}
For what it's worth, here is my suggestion.
Use SF reverse proxy url as ReplyUrl
You don't need to register every possible private address along with the port in ReplyUrls section for a given app registration. As you communicate with the service via SF Reverse Proxy, put its address as a reply url.
Adjust the code to build a correct redirect_uri
There is a change you'll need to apply to make sure that redirect_uri would be pointing at your reverse proxy rather than local ip. The exact code depends on many factors, include asp.net core build installed on your machine, but here is the prototype that I've got working on .net core 2.1.4 -
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultSignInScheme = OpenIdConnectDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(options =>
{
options.ClientId = "xxx-xxxxx-xxxx";
options.Authority = "xxx-xxxxx-xxxx";
options.CallbackPath = "/signin-oidc";
options.Events = new OpenIdConnectEvents
{
OnRedirectToIdentityProvider = ctx =>
{
ctx.ProtocolMessage.RedirectUri = $"https://reverse_proxy_url/app_name/service_name{options.CallbackPath}";
return Task.CompletedTask;
}
};
});