19

My Identity Server is using identityserver4 framework (http://localhost:9000). And I register the client on Identity Server as below.

clients.Add(
     new Client
     {
         ClientId = "customer.api",
         ClientName = "Customer services",
         AllowedGrantTypes = GrantTypes.HybridAndClientCredentials,
         RequireConsent = false,
         AllowAccessTokensViaBrowser = true,

         RedirectUris = { "http://localhost:60001/signin-oidc" },
         PostLogoutRedirectUris = { "http://localhost:60001/signout-callback-oidc" },
         ClientSecrets = new List<Secret>
         {
             new Secret("testsecret".Sha256())
         },
         AllowedScopes = new List<string>
         {
             IdentityServerConstants.StandardScopes.OpenId,
             IdentityServerConstants.StandardScopes.Profile,
             IdentityServerConstants.StandardScopes.Email,
             IdentityServerConstants.StandardScopes.OfflineAccess,
             "customerprivatelinesvn.api",                        
         },
         AllowOfflineAccess = true,
         AlwaysIncludeUserClaimsInIdToken = true,
         AllowedCorsOrigins = { "http://localhost:60001" }
     });  

Here is the authentication on my client app (http://localhost:60001).

private void AddAuthentication(IServiceCollection services)
{
    JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();

    services.AddAuthentication(options =>
    {
        options.DefaultAuthenticateScheme = "Cookies";
        options.DefaultChallengeScheme = "oidc";       
    })
    .AddCookie()
    .AddOpenIdConnect("oidc", options =>
    {
        Configuration.GetSection("OpenIdConnect").Bind(options);        
    });    
}    

"OpenIdConnect": {
    "SignInScheme": "Cookies",
    "Authority": "http://localhost:9000/",
    "RequireHttpsMetadata": false,
    "ClientId": "customer.api",
    "ClientSecret": "testsecret",
    "Scope": [ "customerprivatelinesvn.api", "offline_access" ],
    "CallbackPath": "/signin-oidc",
    "ResponseType": "code id_token token",
    "GetClaimsFromUserInfoEndpoint": true,
    "SaveTokens": true
  }

HomeController of client app

[Authorize]
public class HomeController : Controller
{
    public IActionResult Index()
    {
        return View();
    }    
}

Here are the Cookies of client app after user logs in. enter image description here

I try to implement the signout action as below

public class AccountController : Controller
{
    public async Task<IActionResult> Signout()
    {
        await HttpContext.SignOutAsync("Cookies");
        await HttpContext.SignOutAsync("oidc");

        return RedirectToAction("Index", "Home");                  
    }
}

But when user signs out, it doesn't call the endsession endpoint of identity server. I look at the traffic of fiddler, there is no request to identity server.

enter image description here

My expectation is when user signs out, it will call endsession endpoint of identity server and redirect to logout link of identity server as below.

enter image description here

enter image description here

We can do this easily on MVC application by calling OwinContext signout

private void LogoutOwin(IOwinContext context)
        {
            context.Authentication.SignOut();
        }

But the signout method doesn't work anymore on ASP.NET Core 2.

Note: I'm calling the signout action from an AJAX post because my client app is angular 5 app.

Does anyone know how to implement the signout correctly on ASP.NET Core 2?

Thank you very much.

Regards,

Kevin

Julian
  • 33,915
  • 22
  • 119
  • 174
Kevin Hoang
  • 922
  • 1
  • 10
  • 25

4 Answers4

25

To allow the signing out to occur use the following Logout action:

public async Task Logout()
{
    await HttpContext.SignOutAsync("Cookies");
    await HttpContext.SignOutAsync("oidc");
}

This is exactly what the quickstart says to use (http://docs.identityserver.io/en/release/quickstarts/3_interactive_login.html). You (and I) have been too clever. I looked at the action in the tutorial and thought 'That's not complete, it doesn't return an action result, lets redirect back to my page'.

Actually what happens is HttpContext.SignOutAsync("oidc"); sets a default ActionResult (Which is to redirect to the OpenIdConnect provider to complete the sign-out). By specifying your own with return RedirectToAction("Index", "Home"); you override this, so the sign-out action never happens.

Redirect after logout

From this answer, the way you specify a redirect URL after the logout is completed is by using AuthenticationProperties

public async Task Logout()
{
   await context.SignOutAsync("Cookies");
   var prop = new AuthenticationProperties
   {
       RedirectUri = "/logout-complete"
   };
   // after signout this will redirect to your provided target
   await context.SignOutAsync("oidc", prop);
}
M. Jahedbozorgan
  • 6,914
  • 2
  • 46
  • 51
Wilco
  • 974
  • 8
  • 14
  • 1
    Calling `controller.SignOut` produces a `SignOutResult` object that can then be returned from an `IActionResult` controller action. The `SignOutResult` class then calls `HttpContextExtensions.SignOutAsync`. It's not a good idea to call `HttpContextExtensions.SignOutAsync` directly from within a controller action. – Dai Aug 24 '19 at 09:35
18

I can resolve my problem now.

1) Return SignOutResult will call endsession endpoint.

2) Change AJAX post to submit form.

public class AccountController : Controller
{
    public IActionResult Signout()
    {
        return new SignOutResult(new[] { "oidc", "Cookies" });            
    }
}


<form action="/Account/Signout" id="signoutForm" method="post" novalidate="novalidate">
    <ul class="nav navbar-nav navbar-right">
        <li><a href="javascript:document.getElementById('signoutForm').submit()">Sign out</a></li>
    </ul>
</form>
Kevin Hoang
  • 922
  • 1
  • 10
  • 25
8

On Net Core 2.0 change your code to use the enumerations CookieAuthenticationDefaults and OpenIdConnectDefaults

    services.AddAuthentication(options =>
        {
            options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
            options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
        })
        .AddCookie()
        .AddOpenIdConnect(SetOpenIdConnectOptions);


private static void SetOpenIdConnectOptions(OpenIdConnectOptions options)
{
    options.ClientId = "auAuthApp_implicit";
    options.Authority = "http://localhost:55379/";

    options.SignInScheme = "Cookies";
    options.RequireHttpsMetadata = false;

    options.SaveTokens = true;
    options.ResponseType = "id_token token";
    options.GetClaimsFromUserInfoEndpoint = true;

}

and...

public async Task<IActionResult> Logout()
{
    await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
    await HttpContext.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme);

    return RedirectToAction("Index", "Home");
}
alhpe
  • 1,424
  • 18
  • 24
  • I tried this (which appears to just change the display name of the authentication schemes) and still had the same problem – Wilco Jul 31 '18 at 11:55
2

.Net Core 3.1, using AuthenticationProperties to set the redirect url.

    public async Task Logout()
    {
        await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
        await HttpContext.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme, new AuthenticationProperties { RedirectUri = "/Home/Logout-Complete" });
    }
Fordy
  • 720
  • 1
  • 7
  • 11