2

We have an angular 5 application built on top of MVC authentication. The application is served up from the Home/Index action, and once the application is loaded, the angular routing takes care of pretty much everything. We are doing this primarily because we wanted to use MVC's Authentication processes (we are using Identity Server 4 as our Oath system).

This works well with one exception: logout. When we attempt to logout, the application seems to be immediately reauthorized and reloads instead of returning us to our Identity Server login page.

Originally, we had success in our development environment through this code:

[HttpPost]
public async Task<IActionResult> Logout()
{
  foreach (string key in Request.Cookies.Keys)
  {
    Response.Cookies.Delete(key);
  }

  await HttpContext.SignOutAsync();

  return Ok();
}

But it was a false positive, because all of our applications were running on localhost, so they had access to each other's cookies. Due to this, the Identity Server cookies were cleared along with the cookies for the Angular application.

We attempted to replace that logic with something like this:

    public async Task Logout()
    {
        if (User?.Identity.IsAuthenticated == true)
        {
            // delete local authentication cookie
            await HttpContext.SignOutAsync("Cookies");
            await HttpContext.SignOutAsync("oidc");
        }
    }

However, it did not log out in either environment (however that code works for one of our MVC applications using the same identity server).

To give some background, our logout process for the Angular application comes from a material menu, where we then prompt the user if they really want to logout with a modal, before calling the logout function. Code snippets of this process:

Method called by logout button:

public openDialog(): void {
    let dialogRef = this.dialog.open(ModalComponent, {
      width: '250px',
      data: { text: this.logoutText, ok: this.okText, cancel: this.cancelText }
    });

    dialogRef.afterClosed().subscribe(result => {
      if (result !== undefined) {
        switch (result.data) {
          case 'ok':
            this.logout();
            break;
          case 'cancel':
            break;
          default:
            break;
        }
      }
   });
}

private logout(): void {
   this.sessionService.logOut();
}

Session service:

public logOut(): void {
    this.auth.revokeToken();
    this.http.post(this.logoutUrl, undefined).subscribe(x => window.location.reload());
}

As mentioned, calling logout this way ends up with a page refresh and the user not really being logged out. There's probably something simple we are missing, but all of the tinkering I've done so far has not led to any success.

EDIT:

Our angular routing is fairly simple (though the pathing prevents us from calling something like home/logout):

{
  path: 'parts',
  component: PartsComponent,
  canActivate: [AuthGuard],
  runGuardsAndResolvers: 'always',
  data: { title: 'Parts' }
},
{
  path: '',
  redirectTo: 'parts',
  pathMatch: 'full'
},
{
  path: '**',
  redirectTo: 'parts'
}

Our MVC routes are also fairly straightforward

    app.UseMvc(routes =>
    {
      routes.MapRoute(
       name: "default",
       template: "{controller=Home}/{action=Index}/{id?}");

      routes.MapRoute(
       "Sitemap",
       "sitemap.xml",
       new { controller = "Home", action = "SitemapXml" });

      routes.MapSpaFallbackRoute(
        name: "spa-fallback",
        defaults: new { controller = "Home", action = "Index" });
    });

I'm not certain how we could easily route to home/logout with the angular routes the way they are. I'm guessing we'd have to add a route for it. We tried that at one point but it never routed correctly. I'm trying to find my notes on what we tried.

Marshall Tigerus
  • 3,675
  • 10
  • 37
  • 67
  • When the page refresh, the user does log out? or he is still logged in, but the page just refreshed ? – dAxx_ Jul 26 '18 at 13:48
  • @dAxx_ It clears the cookies that it has for the angular app. The URL shown in the address bar after refresh is the URL of the login page (including the redirect querystring) but it never shows the login page, just redirects again to the angular application, as if the user was already logged in. – Marshall Tigerus Jul 26 '18 at 14:01
  • You didnt provide any routes configuration or guards, but do you use any of these? in you http.POST you just do a window.loaction.reload(), which is not the job for the service, you should return the Observable to the component, and subscribe to it there. perform a navigation to Login component through Router. – dAxx_ Jul 26 '18 at 14:05
  • @MarshallTigerus Probably your ajax request clears only client app cookies. To clear identityserver cookie, i think you should redirect user to logout page. – adem caglin Jul 26 '18 at 14:05
  • Added additional information. – Marshall Tigerus Jul 26 '18 at 15:01
  • @MarshallTigerus Did you try window.location.href=this.logoutUrl in logOut method? – adem caglin Jul 26 '18 at 16:11

2 Answers2

2

After you cleared the local cookies of your application send the user to the end_session_endpoint of your Idsrv. (The code you showed to clear your session should work, if not I'd the config in startup.cs and debug to check if the redirect really removed the cookie in the browser).

E.g. https://demo.identityserver.io/.well-known/openid-configuration

There you see the endpoint. This should remove the session on the Idsrv. Depending on your setup this could kill your sessions on your other applications that uses the same Identity Server instance.

You can read more about this in the docs.

I don't see a problem with the Angular routing interfering with your server side routing. As long as you ofcourse really do a redirect to that page using a regular window.location.replace

And ofcourse like @Win mentioned it would be a good security guideline to revoke refresh_token and reference_tokens if you use those.

ErazerBrecht
  • 1,583
  • 2
  • 20
  • 37
  • I found a slightly easier solution than this. We've already implemented a logout view/endpoint on our identity server, so I just called that. Which is a lot simpler answer than I thought it would be. However,calling the end_session_endpoint also worked – Marshall Tigerus Jul 26 '18 at 16:17
  • Are you saying that I need to clear the cookies manually from my Angular application? I thought that, since it's `SignInAsync()` is creating the session cookie, then it should be `SignOutAsync()` removing it. More specifically, what my current problem regards, is that I send the user to *endsession* providing identity token and IDS4 send them back to my controller with `logoutId`. However, using that. I don't et anything in the context except the name of the client. And so, the cookie isn't removed... – Konrad Viltersten Aug 24 '21 at 06:06
0

I could not answer for Angular; I'm still working on it. However, client web app could ask IDP to revoke access token and refresh token when user signs out at client. For example, in ASP.Net Core -

using System;
using System.Threading.Tasks;
using IdentityModel.Client;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;

namespace MY_APP.Controllers
{
    public class AccountController : Controller
    {
        [HttpGet]
        public async Task<IActionResult> SignOut()
        {
            var discoveryClient = new DiscoveryClient("IDP_URL");
            var metaDataResponse = await discoveryClient.GetAsync();

            var revocationClient = new TokenRevocationClient(
                metaDataResponse.RevocationEndpoint, 
                "CLIENT_NAME",
                "CLIENT_SECRET");

            // revoke the access token
            string accessToken = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.AccessToken);

            if (!string.IsNullOrWhiteSpace(accessToken))
            {
                var revokeAccessTokenResponse = await revocationClient.RevokeAccessTokenAsync(accessToken);

                if (revokeAccessTokenResponse.IsError)
                    throw new Exception("Problem encountered while revoking the access token.", 
                        revokeAccessTokenResponse.Exception);
            }

            // revoke the refresh token
            string refreshToken = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.RefreshToken);

            if (!string.IsNullOrWhiteSpace(refreshToken))
            {
                var revokeRefreshTokenResponse = await revocationClient.RevokeAccessTokenAsync(refreshToken);

                if (revokeRefreshTokenResponse.IsError)
                    throw new Exception("Problem encountered while revoking the refresh token.",
                        revokeRefreshTokenResponse.Exception);
            }

            return SignOut(
                new AuthenticationProperties { RedirectUri = "CALL_BACK_URL" },
                CookieAuthenticationDefaults.AuthenticationScheme,
                OpenIdConnectDefaults.AuthenticationScheme);
        }
    }
}
Win
  • 61,100
  • 13
  • 102
  • 181