简体   繁体   中英

How to fix error 404 when logging out on an ASP.NET Core MVC app against Azure AD?

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

In (at least) the latest versions you should add endpoints.MapRazorPages(); in the Configure() method of Startup. This handles the route for you.

app.UseEndpoints(endpoints =>
{
    endpoints.MapControllerRoute(
        name: "default",
        pattern: "{controller=Home}/{action=Index}/{id?}");
    endpoints.MapRazorPages();//ADD THIS LINE
});

I spent half a day chasing this down, realizing it got broken when we migrated to .NET Core 3.0.

We also migrated to Microsoft Identity because the AzureADB2C UI was marked as obsolete. So, in ConfigureServices, we added:

services.AddRazorPages()
            .AddMicrosoftIdentityUI();

...and as Paul said, in Configure we added:

endpoints.MapRazorPages();

This solved the problem for us. We got the hint from creating a brand new MVC Web App with Microsoft Identity authentication from template and compared the differences in our ConfigureServices / Configure methods.

您必须在 _LoginPartial.cshtml 文件中将 asp-area="AzureAD"值更改为asp-area="MicrosoftIdentity"

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM