Why does the end user have to log out twice?

In the Account/Logout page, which lives under Areas/Identity/Account/Logout.cshtml.cs in your scaffolded ASP.NET Core Identity code, there is an OnGet handler that looks like this:

public void OnGet() { }

Because this is using ASP.NET Core Razor Pages, all this does is render the corresponding Logout.cshtml page. In your example, when you hit Logout in the MVC app, it clears its own cookies and then passes you over to the IS4 app (the OnGet, specifically). Because this OnGet handler is empty, it's not really doing anything and it's certainly not signing you out of the IS4 app.

If you look at the OnPost handler inside of Logout.cshtml.cs, you'll see it looks something like this:

public async Task<IActionResult> OnPost(string returnUrl = null)
{
    await _signInManager.SignOutAsync();
    // ...
}

This call to SignOutAsync does exactly what it suggests: it signs you out of IS4 itself. However, in your current workflow, this OnPost handler is not being called. The OnGet handler is called indirectly when you use Logout in the MVC app, as I've already mentioned.

Now, if you look at the controller/action implementation of IS4 logout in the Quickstart.UI project, you'll see that essentially it passes the GET request over to the POST request. Here's the code, with comments stripped out:

[HttpGet]
public async Task<IActionResult> Logout(string logoutId)
{
    var vm = await BuildLogoutViewModelAsync(logoutId);

    if (vm.ShowLogoutPrompt == false)
        return await Logout(vm);

    return View(vm);
}

When logging out, there's a setting that controls whether or not the user should first be prompted to confirm whether or not they want to log out. That's mostly what this code is taking care of - it passes it straight over to the POST request handler if the prompt is not required. Here's a snippet of the code for the POST:

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Logout(LogoutInputModel model)
{
    var vm = await BuildLoggedOutViewModelAsync(model.LogoutId);

    if (User?.Identity.IsAuthenticated == true)
    {
        await HttpContext.SignOutAsync();

        // ...
    }

    // ...

    return View("LoggedOut", vm);
}

The important line here is the call to HttpContext.SignOutAsync - this ends up removing the cookie that IS4 is using to keep you signed in. Once this has been removed, you're signed out of IS4. Ultimately, this is what is missing from your current implementation.

At the simplest level, you can fix your issue by updating your OnGet to look like this:

public async Task<IActionResult> OnGet()
{
    if (User?.Identity.IsAuthenticated == true)
    {
        await _signInManager.SignOutAsync();          
        return RedirectToPage(); // A redirect ensures that the cookies has gone.
    }

    return Page();
}

This doesn't support the ShowLogoutPrompt option I've detailed above, simply just to keep this answer a little bit shorter. Apart from that, it's just using _signInManager to do the logout given that you're in the ASP.NET Core Identity world.

I encourage you to explore the full source-code from the Quickstart.UI implementation in order to support ShowLogoutPrompt, returnUrl, etc - I can't possibly do that here without writing a book.