azure removes Access-Control-Allow-Origin header returned from my app service - azure

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.

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

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

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

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.

Asp.Net Core 2 with Google Auth & Cookie auth on subdomains

I have 3 web applications on Azure.
Webapp1 is www.mydomain.com
Webapp2 is admin.mydomain.com
Webapp3 is user.mydomain.com
When I log on WebApp1, I want to be logged on all other subdomains.
I want to use social providers to authenticate my users, and use asp.net Identity for authorization.
After reading docs & SO questions here is what I have in my Startup.cs
public void ConfigureServices(IServiceCollection services)
{
/*
* Some code
*/
// Creating a blob storage account to share keys for all applications
var storageAccount = CloudStorageAccount.Parse(configuration.GetConnectionString("IdentityStr"));
CloudBlobClient blobClient = storageAccount.CreateCloudBlobClient();
CloudBlobContainer container = blobClient.GetContainerReference("identity");
AsyncContext.Run(() => container.CreateIfNotExistsAsync());
services.AddDataProtection()
.SetApplicationName("MYAPP")
.PersistKeysToAzureBlobStorage(container, "keys.xml");
/*
* BEGIN DISGUSTING: I recreate the data protection provider here
* because I need the instance of it below for the Cookie options
*/
var serviceCollection = new ServiceCollection();
serviceCollection.AddDataProtection()
.SetApplicationName("MYAPP")
.PersistKeysToAzureBlobStorage(container, "keys.xml");
var service2 = serviceCollection.BuildServiceProvider();
var dataProtector = service2.GetRequiredService<IDataProtectionProvider>();
/*
* END DISGUSTING
*/
services.AddDbContext<AuthDbContext>(options =>
options.UseSqlServer(connectionString));
services.AddIdentity<AuthUser, AuthRole>()
.AddEntityFrameworkStores<AuthDbContext>()
.AddDefaultTokenProviders();
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(o =>
{
o.LoginPath = "/account/login";
o.LogoutPath = "/account/logout";
o.Cookie.Domain = "mydomain.com";
o.DataProtectionProvider = dataProtector;
o.TicketDataFormat = new TicketDataFormat(dataProtector.CreateProtector("Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationMiddleware", "Cookies", "v2"));
})
.AddGoogle(o =>
{
o.ClientId = configuration["Authentication:Google:ClientId"];
o.ClientSecret = configuration["Authentication:Google:ClientSecret"];
});
/*
* Some code
*/
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
/*
* Some code
*/
app.UseAuthentication();
/*
* Some code
*/
}
The cookie is working fine on the Webapp1 but the domain attached is not as the one defined in o.Cookie.Domain but www.mydomain.om
Here is a view of chrome's cookies
And fiddler's view:
I probably missed something...
The identity cookie doesn't have a domain set. You don't need to add Cookie a second time, because Identity already adds it, and you need to configure that instance, not the new one you're creating
So try using ConfigureApplicationCookie
services.AddIdentity<AuthUser, AuthRole>()
.AddEntityFrameworkStores<AuthDbContext>()
.AddDefaultTokenProviders();
services.AddAuthentication()
.AddGoogle(o =>
{
// Google options.
});
services.ConfigureApplicationCookie(options =>
{
// Cookie settings
});

Resources