I'm integrating an ASP.NET Core MVC 3.0 app to Azure AD for authentication and authorization adn everything works well, but when I try to sign out, once login.microsoftonline.com
signs me out, it redirects to my app and then the following error springs up:
No webpage was found for the web address:
https://localhost:5002/Account/SignOut?page=%2FAccount%2FSignedOut
The path I use to invoke the signout process is /AzureAD/Account/SignOut
.
Contents of appsettings.json
:
{
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"Domain": "[OMITTED]",
"TenantId": "[OMITTED]",
"ClientId": "[OMITTED]",
"CallbackPath": "/signin-oidc"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*"
}
These are the contents of my Startup.cs
class:
using System.Collections.Generic;
using System.Globalization;
using MySite.WebSite.Helpers;
using MySite.WebSite.Models.Validators;
using MySite.WebSite.Models.ViewModels;
using FluentValidation;
using FluentValidation.AspNetCore;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.AzureAD.UI;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Localization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Authorization;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Localization;
namespace MySite.WebSite
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.Configure<CookiePolicyOptions>(options =>
{
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services
.AddAuthentication(AzureADDefaults.AuthenticationScheme)
.AddAzureAD(options => Configuration.Bind("AzureAd", options));
services.Configure<CookieAuthenticationOptions>(AzureADDefaults.CookieScheme, options =>
{
options.AccessDeniedPath = "/Home/AccessDenied";
options.LogoutPath = "/";
});
services.Configure<OpenIdConnectOptions>(AzureADDefaults.OpenIdScheme, options =>
{
options.Authority += "/v2.0/";
options.TokenValidationParameters.ValidateIssuer = false;
});
services.AddLocalization(options => options.ResourcesPath = "Resources");
services
.AddControllersWithViews(options => options.Filters.Add(GetAuthorizeFilter()))
.SetCompatibilityVersion(CompatibilityVersion.Version_3_0)
.AddViewLocalization(LanguageViewLocationExpanderFormat.Suffix)
.AddDataAnnotationsLocalization()
.AddFluentValidation();
services.AddTransient<IValidator<ContactIndexViewModel>, ContactIndexViewModelValidator>();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseRequestLocalization(GetLocalizationOptions());
app.UseStaticFiles(GetStaticFileOptions());
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
private RequestLocalizationOptions GetLocalizationOptions()
{
var cookie_request_culture_provider = new CookieRequestCultureProvider
{
CookieName = "UserCulture"
};
var providers = new List<IRequestCultureProvider>()
{
cookie_request_culture_provider,
new AcceptLanguageHeaderRequestCultureProvider()
};
var result = new RequestLocalizationOptions
{
RequestCultureProviders = providers,
SupportedCultures = Cultures.SupportedCultures,
SupportedUICultures = Cultures.SupportedCultures,
DefaultRequestCulture = new RequestCulture(Cultures.DefaultCulture)
};
return result;
}
private StaticFileOptions GetStaticFileOptions()
{
var result = new StaticFileOptions
{
ServeUnknownFileTypes = true,
DefaultContentType = "text/plain"
};
return result;
}
private AuthorizeFilter GetAuthorizeFilter()
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
var result = new AuthorizeFilter(policy);
return result;
}
}
}
Turns out this is a known issue in Microsoft.AspNetCore.Authentication.AzureAD.UI
; that package implements the Azure AD authentication/authorization flow in ASP.NET Core, and part of that is an embedded AccountController
(area AzureAD
) that takes the signin - signout processes out of your shoulders. Problem is, the SignOut
action hardcodes a redirect to /Account/SignOut?page=%2FAccount%2FSignedOut
once the signout process is complete, and there's the problem.
I managed to solve it by implementing a small AccountController (without an area) and adding a single SignOut
action that handles the redirect from Microsoft.AspNetCore.Authentication.AzureAD.UI
's AccountController
:
[AllowAnonymous]
public class AccountController : Controller
{
[HttpGet]
public IActionResult SignOut(string page)
{
return RedirectToAction("Index", "Home");
}
}