Dynamic ports with Azure Ad Authentication - authentication

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

Related

Microsoft.AspNetCore.Authentication.OpenIdConnect /oauth2/authorize endpoint form - redirect_uri error

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

How to combine multiple authentication schemes for different types of clients (user/pass and client/secret) in a Blazor WASM project?

I have a Blazor WASM project with a Blazor Client and ASP.NET core server. I can authenticate with user/password using the following code:
services
.AddDefaultIdentity<ApplicationUser>(
options => options.SignIn.RequireConfirmedAccount = true)
.AddRoles<IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>();
services
.AddIdentityServer()
.AddApiAuthorization<ApplicationUser, ApplicationDbContext>();
services
.AddAuthentication()
.AddIdentityServerJwt();
services.AddTransient<IProfileService, ProfileService>();
services.AddAuthorization(options =>
{
options.AddPolicy("ApiScope", policy =>
{
policy.RequireAuthenticatedUser();
policy.RequireClaim("scope", "api1");
});
});
When I add the following code, I can successfully authenticate with clientcredentials from a console client. But then the Blazor client user/password authentication stops working.
...
services
.AddIdentityServer()
.AddApiAuthorization<ApplicationUser, ApplicationDbContext>()
+.AddInMemoryApiScopes(Config.ApiScopes)
+.AddClientStore<ClientStore>()
+.AddDeveloperSigningCredential();
services
.AddAuthentication()
.AddIdentityServerJwt();
+services
+ .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
+ .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
+ {
+ options.Authority = "https://localhost:44311";
+ options.TokenValidationParameters = new TokenValidationParameters
+ {
+ ValidateAudience = false,
+ };
+ });
...
In the browser while trying to authenticate in the Blazor client, the console prints:
info: Microsoft.AspNetCore.Authorization.DefaultAuthorizationService[2]
Authorization failed. These requirements were not met:
DenyAnonymousAuthorizationRequirement: Requires an authenticated user.
I have tried a lot, but I'm not able to make both work together. It seems that somehow this configuration requires authentication for everything, even the pages/controllers that are marked AllowAnonymous. So, when I try to authenticate, it gives me an error telling me the user must be authenticated: DenyAnonymousAuthorizationRequirement. The policy, "ApiScope" is only intended for the clientcredentials client, not for the Blazor client. If removed, the RequireAuthenticatedUser call doesn't make a difference, same error.
Any help is appreciated.

Share authentication cookie across subdomains in ASP.NET Core - cannot login

I am trying to implement sharing authentication cookie across two subdomains in ASP.NET Core web apps. I have changed Startup.cs and added following code in ConfigureServices:
services.AddDataProtection()
.PersistKeysToAzureBlobStorage(new Uri("{{my_url_to_blob_storage}}"))
.SetApplicationName("myapp");
services.AddAuthentication(sharedOptions =>
{
sharedOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
{
options.Cookie.Name = "myapp";
options.Cookie.Domain = ".azurewebsites.net";
});
I have published the web app to Azure, but when trying HttpContext.SignInAsync(), nothing happens - HttpContext.User is not set.
Thanks for any help
Solved! When using custom domain, everything runs as expected

Is it okay to use JwtBearerOptions.RequireHttpsMetadata=false in production when behind a Reverse Proxy?

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.

Infinite auth loop when using RunProxy and OpenIdConnect

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?