Microsoft Authentication Logout not working

Viewed 3747

Been fighting this for 2 days...need some help now.

I am working on a project with ASP.NET Core 3.1 using Razor Pages in Visual Studio 2019. The project has local accounts with the ability to register additional external accounts like Microsoft, Facebook, etc. I followed tutorials on Microsoft's documents on setup of Microsoft Authentication and while the login works correctly, the logout does not clear the session.

To test the problem, I built the application from scratch with no modifications and following the instructions I still had the same problem...the logout does not redirect to Microsoft to sign out.

The experience: When I logon and/or register an account is created in the dbo.AspNetUsers datatable. I am able to logon using my Microsoft account without a problem, the redirect works, etc. When I logout, I get the standard ASP.NET logout page, but no Microsoft logout page. Now when I go back and click on Login, there is no prompt for a username/password. The concern here is that on a system with multiple users, if a user does not clear cookies and history, they will gain access to the previous users info...and they will not be able to login since the cycle repeats itself until the cookie is cleared manually. I do not want to use the new Azure AD Authentication because it does not work with local accounts so it is not an option for me at this time since it is also still in PREVIEW.

My settings for the App Registration are:

Redirect URIs

  • https://localhost:44323/
  • https://localhost:44323/signin-microsoft

Logout URL

  • https://localhost:44323/signout-oidc

Any pointers to help with the logout would be great.

Below is my code samples (can be found on Microsoft Docs Microsoft Account Documentation) :

Startup.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.UI;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.EntityFrameworkCore;
using MSAuth.Data;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace MSAuth
{
    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.AddDbContext<ApplicationDbContext>(options =>
                options.UseSqlServer(
                    Configuration.GetConnectionString("DefaultConnection")));
            services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
                .AddEntityFrameworkStores<ApplicationDbContext>();
            services.AddRazorPages();

            services.AddAuthentication().AddMicrosoftAccount(microsoftOptions =>
            {
                microsoftOptions.ClientId = Configuration["Authentication:Microsoft:ClientId"];
                microsoftOptions.ClientSecret = Configuration["Authentication:Microsoft:ClientSecret"];
            });
        }

        // 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.UseDatabaseErrorPage();
            }
            else
            {
                app.UseExceptionHandler("/Error");
                // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
                app.UseHsts();
            }

            app.UseHttpsRedirection();
            app.UseStaticFiles();

            app.UseRouting();

            app.UseAuthentication();
            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapRazorPages();
            });
        }
    }
}

Logout.cshtml.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Logging;

namespace MSAuth.Areas.Identity.Pages.Account
{
    [AllowAnonymous]
    public class LogoutModel : PageModel
    {
        private readonly SignInManager<IdentityUser> _signInManager;
        private readonly ILogger<LogoutModel> _logger;

        public LogoutModel(SignInManager<IdentityUser> signInManager, ILogger<LogoutModel> logger)
        {
            _signInManager = signInManager;
            _logger = logger;
        }

        public void OnGet()
        {
        }

        public async Task<IActionResult> OnPost(string returnUrl = null)
        {
            await _signInManager.SignOutAsync();
            _logger.LogInformation("User logged out.");
            if (returnUrl != null)
            {
                return LocalRedirect(returnUrl);
            }
            else
            {
                return RedirectToPage();
            }
        }
    }
}

_LoginPartial.cshtml

@using Microsoft.AspNetCore.Identity
@inject SignInManager<IdentityUser> SignInManager
@inject UserManager<IdentityUser> UserManager

<ul class="navbar-nav">
@if (SignInManager.IsSignedIn(User))
{
    <li class="nav-item">
        <a  class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Manage/Index" title="Manage">Hello @User.Identity.Name!</a>
    </li>
    <li class="nav-item">
        <form class="form-inline" asp-area="Identity" asp-page="/Account/Logout" asp-route-returnUrl="@Url.Page("/", new { area = "" })" method="post" >
            <button  type="submit" class="nav-link btn btn-link text-dark">Logout</button>
        </form>
    </li>
}
else
{
    <li class="nav-item">
        <a class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Register">Register</a>
    </li>
    <li class="nav-item">
        <a class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Login">Login</a>
    </li>
}
</ul>
2 Answers

Have a look at these

http://www.binaryintellect.net/articles/3d6ce8b3-cb62-42b7-bedc-5e7f2fb9d017.aspx

http://docs.identityserver.io/en/latest/topics/signout_external_providers.html

It looks like signing out the external user is your responsibility...

public IActionResult SignOut(string signOutType)
{
    if (signOutType == "app")
    {
        HttpContext.SignOutAsync().Wait();
    }
    if (signOutType == "all")
    {
        return Redirect("https://login.microsoftonline.com/common/oauth2/v2.0/logout");
    }
    return RedirectToAction("Index");
}

Instead of using @if (SignInManager.IsSignedIn(User)) in _LoginPartial.cshtml change it to _signInManager which is injected field. SignInManager is a class injected.

Related