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!
Related
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()
{
}
}
I have a web application targeting .NET 5.0 and I am trying to implement Windows Authentication along with some policies for authorization. We have a database table from another application that holds info on user roles, so I am using that to set permissions for my users.
I created a ClaimsTransformer class:
ClaimsTransformer.cs
public class ClaimsTransformer : IClaimsTransformation
{
// snip constructor which pulls in my DbContext from DI
public async Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
{
var id = ((ClaimsIdentity) principal.Identity);
var ci = new ClaimsIdentity(id.Claims, id.AuthenticationType, id.NameClaimType, id.RoleClaimType);
// snip call to DbContext to get user's role from database
if (roleId == 1 || roleId == 7)
{
ci.AddClaim(new Claim("user-role", "admin"));
}
return new ClaimsPrincipal(ci);
}
}
I have my authentication/authorization setup like this:
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(IISDefaults.AuthenticationScheme);
services.AddTransient<IClaimsTransformation, ClaimsTransformer>();
services.AddAuthorization(options =>
{
options.AddPolicy("Admin", policy =>
policy.RequireClaim("user-role", "admin"));
});
// snip rest of method
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// snip unrelated code
app.UseAuthentication();
app.UseAuthorization();
}
My first use of this is to hide a menu in my _Layout.cshtml:
#if ((await AuthorizationService.AuthorizeAsync(User, "admin").Succeeded)
{
// Admin nav link
}
The issue is, since AuthorizeAsync is running on every HTTP request, my ClaimsTransformer also runs each time, hitting the database to check the user's roles on every request. I'd like to avoid this, but I'm not sure of the best way to do so.
Basically, I'd like the system to check the roles only once, when the user is first authenticated. I read that this is what is supposed to happen when using Windows Authentication with IIS, but I am seeing the roles query running on every request when deployed to my IIS server also.
I could easily add a check in my ClaimsTransformer to see if the "user-role" claim exists, and only hit the DB if it is not present, but is there a better way? Should I be overriding something like UserClaimsPrincipalFactory instead of using a ClaimsTransformer?
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
{
}
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.
I have two services running on Azure :
a web service ( angular app / expressjs )
an app service ( aspnet core app )
All the web service does is query the app service for the following endpoint : my-app-service.azurewebsites.net/.well-known/openid-configuration
My app service is setup to allow CORS requests coming from my web service at the code level via the IdentityServer4 dll and as mentioned in many websites I DID ensure CORS settings were neither overridden by web.config or azure CORS management page.
These are my HTTP request headers :
Accept:application/json, text/plain, */*
Accept-Encoding:gzip, deflate
Host:my-app-service.azurewebsites.net
Origin:http://my-web-service.azurewebsites.net
Pragma:no-cache
Referer:http://my-web-service.azurewebsites.net/
And these are my HTTP response headers
Content-Encoding:gzip
Content-Type:application/json
Date:Fri, 05 Jan 2018 17:22:53 GMT
Server:Kestrel
Set-Cookie:ARRAffinity=da4c4ff244aae03ae3c7548f243f7c2b5c22567a56a76a62aaebc44acc7f0ba8;Path=/;HttpOnly;Domain=Host:my-app-service.azurewebsites.net
Transfer-Encoding:chunked
Vary:Accept-Encoding
X-Powered-By:ASP.NET
As you can see, none of the Access-Control-* headers are present. I have added a custom middleware to the asp.net core app pipeline to trace the response headers and I can clearly see them present.
So somewhere Azure is stripping off my headers and I have no more clues where to look now.
Update #1
I forgot to specify that if everything runs on localhost, it works fine. But it does not on Azure.
Update #2
My identity server 4 code
[...]
using Microsoft.IdentityModel.Tokens;
using IdentityServer4.EntityFramework.Mappers;
using IdentityServer4.EntityFramework.DbContexts;
using IdentityServer4;
namespace My.IdentityServer4
{
public class Startup
{
private const string DEFAULT_DEVELOPMENT_AUTHORITY = "http://localhost:5000/";
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
// [... add db context. identity framework, default token provider]
services.AddMvc();
// Cors ( not required, identity server 4 manages it internally )
//services.AddCors(options =>
// options.AddPolicy("AllowAllOrigins", builder => builder.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader()));
string connectionString = Configuration.GetConnectionString("SQLServer");
var migrationsAssembly = typeof(Startup).GetTypeInfo().Assembly.GetName().Name;
// configure identity server with in-memory stores, keys, clients and scopes
services.AddIdentityServer()
.AddDeveloperSigningCredential()
.AddAspNetIdentity<ApplicationUser>()
// this adds the config data from DB (clients, resources)
.AddConfigurationStore(options =>
{
options.ConfigureDbContext = builder =>
builder.UseSqlServer(connectionString,
sql => sql.MigrationsAssembly(migrationsAssembly));
})
// this adds the operational data from DB (codes, tokens, consents)
.AddOperationalStore(options =>
{
options.ConfigureDbContext = builder =>
builder.UseSqlServer(connectionString,
sql => sql.MigrationsAssembly(migrationsAssembly));
// this enables automatic token cleanup. this is optional.
options.EnableTokenCleanup = true;
options.TokenCleanupInterval = 30;
});
services.AddAuthentication()
.AddOpenIdConnect("oidc", "OpenID Connect", options =>
{
//TODO: enable HTTPS for production
options.RequireHttpsMetadata = false;
options.Authority = DEFAULT_DEVELOPMENT_AUTHORITY;
options.ClientId = "app"; // implicit
options.SaveTokens = true;
options.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
RoleClaimType = "role"
};
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
// [... Some stuff before not useful for this snippet]
// For debug purposes, print out request and response headers
app.UseMiddleware<LogHeadersMiddleware>();
app.UseStaticFiles();
// Cors ( not required, identity server 4 manages it internally )
//app.UseCors("AllowAllOrigins");
app.UseIdentityServer();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
}
public class LogHeadersMiddleware
{
private readonly RequestDelegate next;
private readonly ILogger<LogHeadersMiddleware> logger;
public LogHeadersMiddleware(RequestDelegate next, ILogger<LogHeadersMiddleware> logger)
{
this.next = next;
this.logger = logger;
}
public async Task Invoke(HttpContext context)
{
await this.next.Invoke(context);
logger.LogInformation(
$"------------------------\r\n" +
$"*** Request headers ****\r\n" +
string.Join("\r\n", context.Request.Headers.OrderBy(x => x.Key)) + "\r\n" +
$"*** Response headers ***\r\n" +
string.Join("\r\n", context.Response.Headers.OrderBy(x => x.Key)) + "\r\n" +
$"------------------------\r\n");
}
}
}
Update #3 - CORS on Azure service app is not set
Any hints ? Thanks
#NoName found the answer to my issue on this thread.
In a nutshell, https has to be enabled on Azure in order to work.
A warning from Azure in the logs would have been appreciated though. I wouldn't have lost days on this :S
CORS on Azure service app is not set.
Actually, Azure website is supposed to manage CORS for you. You just need to set the CORS on the Azure service App. I also find a similar SO thread about it.
The good thing is that you can completely disable this middleware and manage CORS by your own means, you just have to remove every single allowed origin (including *) from the CORS settings blade in the portal.