Certificate was not authenticated, request succeeds anyway with asp.net 5 on Azure App Service - azure

I'm trying to enable client certificate authentication for my server api per here:
https://learn.microsoft.com/en-us/aspnet/core/security/authentication/certauth?view=aspnetcore-5.0
The problem I'm seeing is that the certificate is sent by the client (as required by the Azure App Service settings), but even though I deliberately call context.Fail, the request is always processed and returns 200. I guess I'm probably missing something sort of fundamental - I'm totally new to, well, pretty much all of this server-side .NET. Thanks for looking!
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme).
AddCertificate(options =>
{
options.AllowedCertificateTypes = CertificateTypes.All;
options.Events = new CertificateAuthenticationEvents
{
OnCertificateValidated = context => {
context.Fail("FAIL!!!");
_logger.LogWarning("OnCertificateValidated!!!");
return Task.CompletedTask; },
OnAuthenticationFailed = context => {
context.Fail("BAD cert. BAD!");
_logger.LogWarning("OnAuthenticationFailed!!!");
return Task.CompletedTask; }
};
}).
AddCertificateCache();
services.AddLogging(loggingBuilder =>
{
loggingBuilder.AddConsole();
loggingBuilder.AddDebug();
loggingBuilder.AddAzureWebAppDiagnostics();
});
services.AddControllers();
}
And Configure
private static ILogger<Startup> _logger;
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILogger<Startup> logger)
{
_logger = logger;
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseCertificateForwarding();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
In my Azure App Service log stream I see
2021-04-21 05:42:07.759 +00:00 [Warning] MyApi.Startup: OnCertificateValidated!!!!!
2021-04-21 05:42:07.759 +00:00 [Information] Microsoft.AspNetCore.Authentication.Certificate.CertificateAuthenticationHandler: Certificate was not authenticated. Failure message: FAIL!!!
and if I configure App Service to allow no certificate, I get a different log, but the request still
2021-04-21 05:27:12.119 +00:00 [Debug] Microsoft.AspNetCore.Authentication.Certificate.CertificateAuthenticationHandler: No client certificate found.
2021-04-21 05:27:12.120 +00:00 [Debug] Microsoft.AspNetCore.Authentication.Certificate.CertificateAuthenticationHandler: AuthenticationScheme: Certificate was not authenticated.
But, in all cases, the request succeeds, while the above linked documentation seemed to indicate I should see a 403 (Forbidden) result -- which I did when I sent no certificate and Azure configuration was set to require a certificate. That's the only time I can get it to fail.
I see that I can perhaps use a method as described here -- retrieve the request header and parse and validate it entirely myself. But isn't the above supposed to work?
https://learn.microsoft.com/en-us/azure/app-service/app-service-web-configure-tls-mutual-auth

I ran into this as well. My issue was that I had not declared any controller routes as requiring Authentication.
Two possible fixes depending on your use case:
Turn on auth for all routes
// Require auth by default for all routes
services.AddAuthorization(options =>
{
options.FallbackPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
});
Turn on auth for specific controllers/actions with an attribute per the docs
[Authorize]
public class AccountController : Controller
{
public ActionResult Login()
{
}
public ActionResult Logout()
{
}
}

Related

Get Token request returned http error: 400 and server response

I have a asp.net core 3.1 web api which adds messages to Azure Queue. In this case I am using an account to login into the VS2019 and debug the code in my local development environment. The same account is also added to the access policy for the storage account with the role : Storage Queue Data Contributor
Here I am trying to remove the dependency of using connectionstring and queue name to connect to the Azure Queue service from the asp.net core web api. All works fine in the case where I am providing connectionstring and queue name. But when I am trying to go with the route of Managed Service Identity in context to my local development environment it is throwing error.
Here goes the code for the asp.net core web api:
TestAPIController.cs:
[HttpPost]
public async Task Post([FromBody]WeatherForecast data)
{
var message = JsonSerializer.Serialize(data);
await _queueClient.SendMessageAsync(message, null, TimeSpan.FromSeconds(-1));
}
Startup.cs:
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
//services.AddHostedService<WeatherDataService>();
services.AddAzureClients(builder =>
{
builder.AddClient<QueueClient, QueueClientOptions>((options, _, _) =>
{
options.MessageEncoding = QueueMessageEncoding.Base64;
var credential = new DefaultAzureCredential();
var queueUri = new Uri("<AzureQueueURL>");
return new QueueClient(queueUri, credential, options);
});
});
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo{Title = "queue_storage", Version = "v1"});
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseSwagger();
app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "queue_storage v1"));
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
On executing the above code I found the below error:
Azure.Identity.AuthenticationFailedException: Azure CLI authentication failed due to an unknown error. ERROR: The command failed with an unexpected error. Here is the traceback:
ERROR: Get Token request returned http error: 400 and server response: {"error":"invalid_grant","error_description":"AADSTS700082: The refresh token has expired due to inactivity.áThe token was issued on 2021-04-23T15:29:05.0816332Z and was inactive for 90.00:00:00.\r\nTrace ID: cbd16614-192a-409b-82a8-348597e81900\r\nCorrelation ID: 85b72955-22a3-4b1c-b05c-d7054ce6a6c6\r\nTimestamp: 2022-05-08 11:22:40Z","error_codes":[700082],"timestamp":"2022-05-08 11:22:40Z","trace_id":"cbd16614-192a-409b-82a8-348597e81900","correlation_id":"85b72955-22a3-4b1c-b05c-d7054ce6a6c6","error_uri":"https://login.microsoftonline.com/error?code=700082"}
I referred to the this article :https://www.rahulpnath.com/blog/getting-started-with-azure-queue-storage/ for my POC.
Can anyone provide their guidance to fix this issue

Error : Internal server error in Azure AD authentication Web App deployed as docker container in Azure web App service

I have aspnet core 2.1 web application with azure ad authentication. When I run my application locally in debugging mode everything works as expected. Whereas when I deploy my web application in azure web app service (Linux, Docker) after login it throws internal server error 500 'xxxx.azurewebsites.net can't currently handle this request.'
Below is my startup.cs and appsettings.json for reference
{ "Authentication": { "ClientId": "my client Id",
"Authority": "https://login.microsoftonline.com/My tenant Id/",
"PostLogoutRedirectUri": "http://localhost:44372",
"CallbackPath": "/signin-oidc", "ClientSecret": "", "ResponseType": "code id_token" } }
using System;
using System.Threading.Tasks;
using Core2AadAuth.Filters;
using Core2AadAuth.Options;
using Core2AadAuth.Services;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Clients.ActiveDirectory;
namespace Core2AadAuth
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
private IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc(opts =>
{
opts.Filters.Add(typeof(AdalTokenAcquisitionExceptionFilter));
}).SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
//TODO: Set up Data Protection key persistence correctly for your env: https://learn.microsoft.com/en-us/aspnet/core/security/data-protection/configuration/overview?tabs=aspnetcore2x
//I go with defaults, which works fine in my case
//But if you run on Azure App Service and use deployment slots, keys get swapped with the app
//So you'll need to setup storage for keys outside the app, Key Vault and Blob Storage are some options
services.AddDataProtection();
//Add a strongly-typed options class to DI
services.Configure<AuthOptions>(Configuration.GetSection("Authentication"));
services.AddScoped<ITokenCacheFactory, TokenCacheFactory>();
services.AddAuthentication(auth =>
{
auth.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
auth.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(opts =>
{
Configuration.GetSection("Authentication").Bind(opts);
opts.Events = new OpenIdConnectEvents
{
OnRedirectToIdentityProvider = async n =>
{
n.ProtocolMessage.RedirectUri = "https://xxxxx.azurewebsites.net/signin-oidc";
await Task.CompletedTask;
},
OnAuthorizationCodeReceived = async ctx =>
{
HttpRequest request = ctx.HttpContext.Request;
//We need to also specify the redirect URL used
string currentUri = UriHelper.BuildAbsolute(request.Scheme, request.Host, request.PathBase, request.Path);
//Credentials for app itself
var credential = new ClientCredential(ctx.Options.ClientId, ctx.Options.ClientSecret);
//Construct token cache
ITokenCacheFactory cacheFactory = ctx.HttpContext.RequestServices.GetRequiredService<ITokenCacheFactory>();
TokenCache cache = cacheFactory.CreateForUser(ctx.Principal);
var authContext = new AuthenticationContext(ctx.Options.Authority, cache);
//Get token for Microsoft Graph API using the authorization code
string resource = "https://graph.microsoft.com";
AuthenticationResult result = await authContext.AcquireTokenByAuthorizationCodeAsync(
ctx.ProtocolMessage.Code, new Uri(currentUri), credential, resource);
//Tell the OIDC middleware we got the tokens, it doesn't need to do anything
ctx.HandleCodeRedemption(result.AccessToken, result.IdToken);
}
};
});
services.Configure<HstsOptions>(o =>
{
o.IncludeSubDomains = false;
o.Preload = false;
o.MaxAge = TimeSpan.FromDays(365);
});
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
//Outside dev, require HTTPS and use HSTS
app.UseHttpsRedirection();
app.UseHsts();
}
app.UseStaticFiles();
app.UseAuthentication();
app.UseMvcWithDefaultRoute();
}
}
}
2021-05-02T09:33:20.315827397Z [40m[32minfo[39m[22m[49m:
Microsoft.AspNetCore.Hosting.Internal.WebHost1
2021-05-02T09:33:20.315907498Z Request starting HTTP/1.1 GET
http://mwp4.azurewebsites.net/ 2021-05-02T09:33:20.608016175Z
[40m[32minfo[39m[22m[49m:
Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[3]
2021-05-02T09:33:20.608738579Z Route matched with {action =
"Index", controller = "Home"}. Executing controller action with
signature Microsoft.AspNetCore.Mvc.IActionResult Index() on controller
Core2AadAuth.Controllers.HomeController (Core2AadAuth).
2021-05-02T09:33:20.745331263Z [40m[32minfo[39m[22m[49m:
Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker1
2021-05-02T09:33:20.745368864Z Executing action method
Core2AadAuth.Controllers.HomeController.Index (Core2AadAuth) -
Validation state: Valid 2021-05-02T09:33:20.755374821Z
[40m[32minfo[39m[22m[49m:
Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[2]
2021-05-02T09:33:20.755394921Z Executed action method
Core2AadAuth.Controllers.HomeController.Index (Core2AadAuth), returned
result Microsoft.AspNetCore.Mvc.ViewResult in 0.8199ms.
2021-05-02T09:33:20.831668759Z [40m[32minfo[39m[22m[49m:
Microsoft.AspNetCore.Mvc.ViewFeatures.ViewResultExecutor1
2021-05-02T09:33:20.831702459Z Executing ViewResult, running
view Index.
2021-05-02T09:33:21.814197839Z [40m[32minfo[39m[22m[49m:
Microsoft.AspNetCore.Mvc.ViewFeatures.ViewResultExecutor[4]
2021-05-02T09:33:21.814222039Z Executed ViewResult - view Index
executed in 1012.1419ms. 2021-05-02T09:33:21.814531541Z
[40m[32minfo[39m[22m[49m:
Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[2]
2021-05-02T09:33:21.814560341Z Executed action
Core2AadAuth.Controllers.HomeController.Index (Core2AadAuth) in
1186.6101ms 2021-05-02T09:33:21.834760456Z [40m[32minfo[39m[22m[49m:
Microsoft.AspNetCore.Hosting.Internal.WebHost[2]
2021-05-02T09:33:21.834786356Z Request finished in 1554.1302ms
200 text/html; charset=utf-8 2021-05-02T09:33:24.456892780Z
[40m[32minfo[39m[22m[49m:
Microsoft.AspNetCore.Hosting.Internal.WebHost1
2021-05-02T09:33:24.456933081Z Request starting HTTP/1.1 GET
http://mwp4.azurewebsites.net/Account/SignIn
2021-05-02T09:33:24.459599695Z [40m[32minfo[39m[22m[49m:
Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[3]
2021-05-02T09:33:24.459617195Z Route matched with {action =
"SignIn", controller = "Account"}. Executing controller action with
signature Microsoft.AspNetCore.Mvc.IActionResult SignIn() on
controller Core2AadAuth.Controllers.AccountController (Core2AadAuth).
2021-05-02T09:33:24.459623195Z [40m[32minfo[39m[22m[49m:
Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker1
2021-05-02T09:33:24.459628095Z Executing action method
Core2AadAuth.Controllers.AccountController.SignIn (Core2AadAuth) -
Validation state: Valid 2021-05-02T09:33:24.459632395Z
[40m[32minfo[39m[22m[49m:
Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[2]
2021-05-02T09:33:24.459636995Z Executed action method
Core2AadAuth.Controllers.AccountController.SignIn (Core2AadAuth),
returned result Microsoft.AspNetCore.Mvc.ChallengeResult in 0.634ms.
2021-05-02T09:33:24.476765988Z [40m[32minfo[39m[22m[49m:
Microsoft.AspNetCore.Mvc.ChallengeResult1
2021-05-02T09:33:24.476797989Z Executing ChallengeResult with
authentication schemes (). 2021-05-02T09:33:25.925815616Z
[40m[32minfo[39m[22m[49m:
Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler[12]
2021-05-02T09:33:25.925878418Z AuthenticationScheme:
OpenIdConnect was challenged. 2021-05-02T09:33:25.947421249Z
[40m[32minfo[39m[22m[49m:
Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[2]
2021-05-02T09:33:25.947478451Z Executed action
Core2AadAuth.Controllers.AccountController.SignIn (Core2AadAuth) in
1488.4947ms 2021-05-02T09:33:25.965321140Z [40m[32minfo[39m[22m[49m:
Microsoft.AspNetCore.Hosting.Internal.WebHost[2]
2021-05-02T09:33:25.965363141Z Request finished in 1500.8083ms
302
2021-05-02T09:33:29.811005026Z [40m[32minfo[39m[22m[49m:
Microsoft.AspNetCore.Hosting.Internal.WebHost1
2021-05-02T09:33:29.811039125Z Request starting HTTP/1.1 POST
http://mwp4.azurewebsites.net/signin-oidc
application/x-www-form-urlencoded 2784 2021-05-02T09:33:29.905246546Z
[40m[1m[33mwarn[39m[22m[49m:
Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler[15]
2021-05-02T09:33:29.905324345Z
'.AspNetCore.Correlation.OpenIdConnect.NKdEqhMMORt61fYDDm70J3AyzqpUmgMxsYxaQfuygWI'
cookie not found. 2021-05-02T09:33:29.906978817Z
[40m[32minfo[39m[22m[49m:
Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler[4]
2021-05-02T09:33:29.906995617Z Error from RemoteAuthentication:
Correlation failed.. 2021-05-02T09:33:29.976640649Z
[41m[30mfail[39m[22m[49m: Microsoft.AspNetCore.Server.Kestrel[13]
2021-05-02T09:33:29.976668449Z Connection id "0HM8D9L7PD097",
Request id "0HM8D9L7PD097:00000001": An unhandled exception was thrown
by the application. 2021-05-02T09:33:29.976674549Z System.Exception:
An error was encountered while handling the remote login. --->
System.Exception: Correlation failed. 2021-05-02T09:33:29.976679149Z
--- End of inner exception stack trace --- 2021-05-02T09:33:29.976691948Z at
Microsoft.AspNetCore.Authentication.RemoteAuthenticationHandler1.HandleRequestAsync() 2021-05-02T09:33:29.976696448Z at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context) 2021-05-02T09:33:29.976700648Z at Microsoft.AspNetCore.StaticFiles.StaticFileMiddleware.Invoke(HttpContext context) 2021-05-02T09:33:29.976705048Z at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.ProcessRequests[TContext](IHttpApplication1
application) 2021-05-02T09:33:29.976709248Z
[40m[32minfo[39m[22m[49m:
Microsoft.AspNetCore.Hosting.Internal.WebHost[2]
2021-05-02T09:33:29.976713648Z Request finished in 157.9686ms
500
One reason can be that you do not persist the data protection key-ring across deployments. When the Data Protection API key-ring is lost, a new set of keys is generated and existing cookies will no longer be valid. The key that is used to encrypt the cookies must be the same across deployments.
I blogged earlier about one approach to this here:
Storing the ASP.NET Core Data Protection Key Ring in Azure Key Vault
See also this page

Azure SignalR Hub Authorization from .AddAzureADB2CBearer

I've been looking for an answer on Internet for days with regards to [Authorize] over the SignalR Hub class. I'm using Azure B2C to authenticate users. Everything works great when the class is not decorated with [Authorize], however I require the user to be authorized, so that I can access the Claims. All my Controllers are authenticating correctly.
[Authorize]
public class SignalRHub : Hub
{
My SignalR Service is running on Azure and started on the server as follows:
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(AzureADB2CDefaults.BearerAuthenticationScheme)
.AddAzureADB2CBearer(options => Configuration.Bind("AzureAdB2C", options));
.....
services.AddSignalR().AddAzureSignalR(ConnectionString)
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseEndpoints(endpoint =>
{
.....
endpoint.MapHub<AzureSignalRSevice.SignalRHub>("/rhub");
});
}
The Debugger is indicating when the client tries to connect:
Microsoft.AspNetCore.Hosting.Diagnostics: Information: Request starting HTTP/1.1 POST https://localhost:44301/rhub/negotiate?negotiateVersion=1 0
Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler: Information: AzureADB2CJwtBearer was not authenticated. Failure message: No SecurityTokenValidator available for token: {Token}
Microsoft.AspNetCore.Authorization.DefaultAuthorizationService: Information: Authorization failed.
Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler: Information: AuthenticationScheme: AzureADB2CJwtBearer was challenged.
The client code is as follows:
var connection = new HubConnectionBuilder().WithUrl("https://localhost:44301/rhub", options =>
{
options.AccessTokenProvider = () => Task.FromResult(token);
}).Build();
All the articles I have read say that the token is passed as a parameter, however in my instance it is being sent in the Authorization header correctly.
I have tried to configure the JwtBearerOptions and pass the token to context.Token, however I get the same Authentication failure.
services.Configure<JwtBearerOptions>(AzureADB2CDefaults.JwtBearerAuthenticationScheme, options =>
{
}
OnChallenge is hit when it fails with invalid_token in the context.
All the Packages are the most recent and up to date running on Core 3.1.2
I've been though many articles, this was the best so far
https://github.com/dotnet/aspnetcore/issues/10582
It doesn't use B2C Authetication though.
I have it working !
The solution is to include the Authentication Scheme
[Authorize(AuthenticationSchemes = AzureADB2CDefaults.BearerAuthenticationScheme + ", " + AzureADB2CDefaults.JwtBearerAuthenticationScheme)]
public class SignalRHub : Hub
{
}

Different authentication schema (Windows, Bearer) for each route

I need to add single-sign-on using Windows Authentication to my intranet Angular web application (hosted on IIS) which uses a JWT Bearer token for authentication. The controllers are secured using the [Authorize] attribute and JWT Bearer token authentication is working. All of the controllers are exposed under the api/ route.
The idea is to publish a new SsoController under the sso/ route, which should be secured with Windows Authentication and that exposes a WindowsLogin action that returns a valid bearer token for the application.
Back when I was using ASP.net Web Forms it was quite easy, you only had to enable Windows Authentication in the web.config/system.webServer section, disable it application-wide in the system.web section and then enable it again under a <location path="sso"> tag. This way ASP.net generated the NTLM/Negotiate challenges only for requests under the sso route.
I got it almost working - the SsoController gets the Windows user name and creates the JWT token just fine, but the pipeline is still generating the WWW-Authenticate: NTLM and WWW-Authenticate: Negotiate headers for all HTTP 401 responses, not just for the ones under the sso route.
How can I tell the pipeline that I want only Anonymous or Bearer auth for all of the api/ requests?
Thanks in advance for your help.
Program.cs
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.UseIISIntegration();
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
// Set up data directory
services.AddDbContext<AuthContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("AuthContext")));
services.AddAuthentication(IISDefaults.AuthenticationScheme);
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = "AngularWebApp.Web",
ValidAudience = "AngularWebApp.Web.Client",
IssuerSigningKey = _signingKey,
ClockSkew = TimeSpan.Zero //the default for this setting is 5 minutes
};
options.Events = new JwtBearerEvents
{
OnAuthenticationFailed = context =>
{
if (context.Exception.GetType() == typeof(SecurityTokenExpiredException))
{
context.Response.Headers.Add("Token-Expired", "true");
}
return Task.CompletedTask;
}
};
});
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
// In production, the Angular files will be served from this directory
services.AddSpaStaticFiles(configuration =>
{
configuration.RootPath = "ClientApp/dist";
});
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseSpaStaticFiles();
app.UseAuthentication();
app.UseWhen(context => context.Request.Path.StartsWithSegments("/sso"),
builder => builder.UseMiddleware<WindowsAuthMiddleware>());
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller}/{action=Index}/{id?}");
});
app.UseSpa(spa =>
{
// To learn more about options for serving an Angular SPA from ASP.NET Core,
// see https://go.microsoft.com/fwlink/?linkid=864501
spa.Options.SourcePath = "ClientApp";
if (env.IsDevelopment())
{
spa.UseAngularCliServer(npmScript: "start");
}
});
}
WindowsAuthMiddleware.cs
public class WindowsAuthMiddleware
{
private readonly RequestDelegate next;
public WindowsAuthMiddleware(RequestDelegate next)
{
this.next = next;
}
public async Task Invoke(HttpContext context)
{
if (!context.User.Identity.IsAuthenticated)
{
await context.ChallengeAsync(IISDefaults.AuthenticationScheme);
return;
}
await next(context);
}
}
web.config
<system.webServer>
<aspNetCore processPath="%LAUNCHER_PATH%" arguments="%LAUNCHER_ARGS%" stdoutLogEnabled="false" stdoutLogFile=".\logs\stdout" forwardWindowsAuthToken="true"/>
<security>
<authentication>
<anonymousAuthentication enabled="true" />
<windowsAuthentication enabled="true" />
</authentication>
</security>
</system.webServer>
So, I spent the last few days investigating this problem and I got a working - if a bit hacky - solution.
It turns out that the main problem is that IIS will handle the Windows Authentication negotiation for all 401 responses sent by the application. It's something that's done at a lower level as soon as you enable Windows Authentication in IIS (or in the system.webServer section), and I haven't been able to find a way to bypass this behaviour. I actually did a test with a classic Web Form app and it works the same - the reason I never noticed this is that classic Forms Authentication rarely generates 401 responses, rather it uses redirects (30x) to take the user to the login page.
This gave me an idea: I could add another middleware to the pipeline that rewrites 401 responses generated by the authorization infrastructure to another, rarely used HTTP code, and detect that in my client Angular app to make it behave as a 401 (by refreshing an access token, or denying router navigation, etc). I used HTTP error 418 "I'm a teapot" since it's an existing but unused code. Here is the code:
ReplaceHttp401StatusCodeMiddleware.cs
public class ReplaceHttp401StatusCodeMiddleware
{
private readonly RequestDelegate next;
public ReplaceHttp401StatusCodeMiddleware(RequestDelegate next)
{
this.next = next;
}
public async Task Invoke(HttpContext context)
{
await next(context);
if (context.Response.StatusCode == 401)
{
// Replace all 401 responses, except the ones under the /sso paths
// which will let IIS trigger the Windows Authentication mechanisms
if (!context.Request.Path.StartsWithSegments("/sso"))
{
context.Response.StatusCode = 418;
context.Response.Headers["X-Original-HTTP-Status-Code"] = "401";
}
}
}
}
Startup.cs
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
...
// Enable the SSO login using Windows Authentication
app.UseWhen(
context => context.Request.Path.StartsWithSegments("/sso"),
builder => builder.UseMiddleware<WindowsAuthMiddleware>());
app.UseMiddleware<ReplaceHttp401StatusCodeMiddleware>();
...
}
The middleware also injects the original status code in the response for further reference.
I also applied to my code the suggestion from Mickaël Derriey to use Authorization policies because it makes the controllers cleaner, but it's not necessary for the solution to work.
Welcome to StackOverflow! That's an interesting quesiton you have here.
First, let me state that I didn't test any of the content in this answer.
Using authorization policies to drive sources of authentication
I like the idea behind the WindowsAuthMiddleware you created, and how it's conditionally inserted in the pipeline if the URL starts with /sso.
MVC integrated with the authorization system and provides the same capabilities with authorization policies. The result is the same, and prevents you from having to write low-level code.
You can define authorization policies in the ConfigureServices method. In your case, if I'm not mistaken, there are two policies:
all requests to /sso should be authenticated with Windows authenticated; and
all other requests should be authenticated with JWTs
services.AddAuthorization(options =>
{
options.AddPolicy("Windows", new AuthorizationPolicyBuilder()
.AddAuthenticationSchemes(IISDefaults.AuthenticationScheme)
.RequireAuthenticatedUser()
.Build());
options.AddPolicy("JWT", new AuthorizationPolicyBuilder()
.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme)
.RequireAuthenticatedUser()
.Build());
});
You can then reference those policies by name in the [Authorize] attributes used to decorate your controllers and/or actions.
[Authorize("Windows")]
public class SsoController : Controller
{
// Actions
}
[Authorize("JWT")]
public class ApiController : Controller
{
// Actions
}
Doing so means that the Windows authentication handler will not run against /api requests, hence the responses should not contain the WWW-Authenticate: NTLM and WWW-Authenticate: Negotiate headers.
Removing automatic authentication of all requests
When you pass an authentication scheme as an argument of AddAuthentication, this means the authentication middleware will try to authenticate every request against that scheme.
This is useful when you have one authentication scheme, but in this case, you could think about removing it, as even for requests to /sso, the JWT handler will analyze the request for a token.
Two calls to AddAuthentication
You should only have one call to AddAuthentication:
the first one sets the IIS authentication scheme as a default so the handler should run on every request;
the second call overwrites that setting and set the JWT scheme as the default one
Let me know how you go!

Functioning App on local server fails on Azure

I have an Angular Web app with an API that is functioning perfectly (well, as perfectly as any app under development functions) on my local server, but when I migrate it and its associated databases to an App Service on Azure every /api call fails with 500.
So thinking that the problem was with the databases I altered the connection strings on my local development server to point to the Azure databases. I found one small problem this way, I has mispelled the username in the connection string. SO I fixed that and it runs perfectly on my local server while accessing the Azure databases, but as soon as I run it on the Azure App Service using the same connection strings every call to /api fails with Internal Server Error 500.
All regular pages are served perfectly and Angular routing works just fine. Only accessing content from the DB fails. I have been at this for a few days and have no idea what to do next. Any advice welcomed.
I am using OpenIddict for authentication so I tagged that, but I can't see anyway that is relevant. Oddly though, and there is a clue here somewhere, the authentication call to "/connect/token" works and returns a valid token, but "/api/..." URLs do not.
I am using Asp Net Core 2.1 if that is relevant.
More Information
I tried the detailed logs as suggested, but they were hardly detailed. But I did note one interesting item. In the error there was the following information:
Requested URL: https://mysite.azuurewebsites.net/api/accounts/getusers
Physical Path: D:\home\site\wwwroot\api\accounts\getusers
Now this app is using MVC so there is no such Physical Path. The Controller is decorated with:
[Route("api/accounts")]
and the Action is decorated as:
[Authorize(Roles = "Somerole")]
[HttpGet("GetUsers"), Produces("application/json")]
It seems to me the route mapping is failing. But this works beautifully on my local development computer. What could be different on the Azure App Service? Is there some special setting I need to set in the portal to allow MVC? I can't imagine why the portal should care about such matters.
Even More Information
Using Postman, if I access /api/someValidUrl with a valid Bearer token I get a 500 error. If I remove the Authorization header then I get a 401 returned.
I started off by saying I didn't think it had anything to do with OpenIddict, but maybe I was wrong. My Authorization Controller simply creates the token. All the checking for validity is done by OpenIddict.
A Huge Clue
I added an ExceptionHandler and then used Postman to make an API request and that yielded the following exception:
<h1>Error: IDX20803: Unable to obtain configuration from: '[PII is hidden by default. Set the 'ShowPII' flag in IdentityModelEventSource.cs to true to reveal it.]'.</h1>
at Microsoft.IdentityModel.Protocols.ConfigurationManager`1.GetConfigurationAsync(CancellationToken cancel)
at Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler.HandleAuthenticateAsync()
at Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler.HandleAuthenticateAsync()
at Microsoft.AspNetCore.Authentication.AuthenticationHandler`1.AuthenticateAsync()
at Microsoft.AspNetCore.Authentication.AuthenticationService.AuthenticateAsync(HttpContext context, String scheme)
at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.StaticFiles.StaticFileMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.Cors.Infrastructure.CorsMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware.Invoke(HttpContext context)
I found an explanation here but I don't fully understand this but it looks as though MS middleware on Azure is incorrectly trying to interpret it as an Azure AD request. The only thing I know for sure is I do not have a file called IdentityModelEventSource.cs in my project.
For reference https://mywebsite.azurewebsites.net/.well-known/openid-configuration returns:
{
"issuer": "https://mywebsite.azurewebsites.net/",
"token_endpoint": "https://mywebsite.azurewebsites.net/connect/token",
"jwks_uri": "https://mywebsite.azurewebsites.net/.well-known/jwks",
"grant_types_supported": [
"password"
],
"scopes_supported": [
"openid",
"email",
"profile",
"roles"
],
"claims_supported": [
"aud",
"exp",
"iat",
"iss",
"jti",
"sub"
],
"subject_types_supported": [
"public"
],
"token_endpoint_auth_methods_supported": [
"client_secret_basic",
"client_secret_post"
],
"claims_parameter_supported": false,
"request_parameter_supported": false,
"request_uri_parameter_supported": false
}
Perhaps with this information someone can point me in the right direction.
New Startup.cs
I took Pinpoint's advice and changed from JWT. The new Startup follows:
using AspNet.Security.OpenIdConnect.Primitives;
using Microsoft.AspNetCore.Builder;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using SIAngular.DBContexts;
using SIAngular.Models;
using SIAngular.Services;
using OpenIddict.Abstractions;
using Microsoft.IdentityModel.Tokens;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Identity;
using System;
using Microsoft.Extensions.Logging;
namespace SIAngular
{
public class Startup
{
private readonly IHostingEnvironment env;
public Startup(IHostingEnvironment env, IConfiguration configuration)
{
Configuration = configuration;
this.env = env;
SIDBConnectionString = Configuration.GetConnectionString("SIDB");
}
public static string SIDBConnectionString;
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
SymmetricSecurityKey _ssk = new SymmetricSecurityKey(Convert.FromBase64String(Configuration["Jwt:Key"]));
services.AddDbContext<ApplicationDbContext>(options =>
{
options.UseSqlServer(Configuration.GetConnectionString("SqlConnection"));
options.UseOpenIddict();
});
services.AddCors();
// Register the Identity services.
services.AddIdentityCore<ApplicationUser>(config =>
{
config.SignIn.RequireConfirmedEmail = true;
config.Password.RequireDigit = true;
config.Password.RequiredLength = 8;
config.Password.RequireLowercase = true; config.Password.RequireNonAlphanumeric = true;
config.User.RequireUniqueEmail = true;
})
.AddRoles<IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders()
.AddRoleValidator<RoleValidator<IdentityRole>>()
.AddRoleManager<RoleManager<IdentityRole>>()
.AddSignInManager<SignInManager<ApplicationUser>>();
// Configure Identity to use the same JWT claims as OpenIddict instead
// of the legacy WS-Federation claims it uses by default (ClaimTypes),
// which saves you from doing the mapping in your authorization controller.
services.Configure<IdentityOptions>(options =>
{
options.ClaimsIdentity.UserNameClaimType = OpenIdConnectConstants.Claims.Name;
options.ClaimsIdentity.UserIdClaimType = OpenIdConnectConstants.Claims.Subject;
options.ClaimsIdentity.RoleClaimType = OpenIdConnectConstants.Claims.Role;
});
services.AddOpenIddict()
// Register the OpenIddict core services.
.AddCore(options =>
{
// Configure OpenIddict to use the Entity Framework Core stores and models.
options.UseEntityFrameworkCore()
.UseDbContext<ApplicationDbContext>();
})
// Register the OpenIddict server services.
.AddServer(options =>
{
// Register the ASP.NET Core MVC services used by OpenIddict.
// Note: if you don't call this method, you won't be able to
// bind OpenIdConnectRequest or OpenIdConnectResponse parameters.
options.UseMvc();
// Enable the token endpoint.
options.EnableTokenEndpoint("/connect/token");
options.AcceptAnonymousClients();
options.DisableScopeValidation();
// Note: the Mvc.Client sample only uses the code flow and the password flow, but you
// can enable the other flows if you need to support implicit or client credentials.
options.AllowPasswordFlow();
// Mark the "email", "profile" and "roles" scopes as supported scopes.
options.RegisterScopes(OpenIdConnectConstants.Scopes.Email,
OpenIdConnectConstants.Scopes.Profile,
OpenIddictConstants.Scopes.Roles);
// During development, you can disable the HTTPS requirement.
if (env.IsDevelopment())
options.DisableHttpsRequirement();
options.AddSigningKey(_ssk);
})
.AddValidation();
services.AddSingleton<IConfiguration>(Configuration);
services.AddScoped<IPasswordHasher<ApplicationUser>, SqlPasswordHasher>();
services.AddMvc();
services.AddTransient<IEmailSender, AuthMessageSender>();
services.AddTransient<ISmsSender, AuthMessageSender>();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
app.UseCors(builder =>
builder.AllowAnyOrigin()
.AllowAnyHeader()
.AllowAnyMethod()
);
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
//app.UseWebpackDevMiddleware(new Microsoft.AspNetCore.SpaServices.Webpack.WebpackDevMiddlewareOptions { HotModuleReplacement = true });
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseStaticFiles();
app.UseAuthentication();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
app.MapWhen(x => !x.Request.Path.Value.StartsWith("/api"), builder =>
{
builder.UseMvc(routes =>
{
routes.MapSpaFallbackRoute(
name: "spa-fallback",
defaults: new { controller = "Home", action = "Index" });
});
});
}
}
}
Now the problem is an exception:
InvalidOperationException: No authenticationScheme was specified, and there was no DefaultChallengeScheme found.

Resources