0

Environments : localhost / azure, .netcore 3.1 mvc identityserver4 + mvc api client.

When I run my application locally, the login/logout works fine, there is : - an identityserver4 mvc .netcore 3.1 - a client mvc api .netcore 3.1

I can login / logout as much as I want, the login always redirects to the identityserver4 login and the login works.

When the same application with the identityserver4 hosted on Azure The first login correctly redirects to the azure identityserver4, and login works fine. Then after the logout (cockies seem to be removed), when I try login again, the redirection to the login page doesn't work and there is an "implicit" login and a direct redirection to the homepage of the website.

The client mvc api is configured like this :

{
  "ClientId": "IdentityServer.WebApi",
  "ClientSecret": "IdentityServer.WebApi",
  "AllowedGrantTypes": "GrantTypes.CodeAndClientCredentials",
  "RedirectUris": [
    "https://localhost:44372/signin-oidc",
    "https://localhost:5001/signin-oidc",
    "https://192.168.1.7:44372/signin-oidc",
    "https://mogui:44372/signin-oidc"
  ],
  "PostLogoutRedirectUris": [
    "https://localhost:44372/signout-callback-oidc",
    "https://localhost:5001/signout-callback-oidc",
    "https://192.168.1.7:44372/signout-callback-oidc",
    "https://mogui:44372/signout-callback-oidc"
  ],
  "AllowedScopes": [
    "openid",
    "profile"
  ],
  "RequireConsent": true,
  "RequirePkce": true,
  "AllowOfflineAccess": true
},

The identityserver4 locally / on azure have this kind of code on its Startup class :

public void ConfigureServices(IServiceCollection services)
{
    try
    {
        telemetryClient.TrackTrace("============== Startup ConfigureServices ============== ");

        // uncomment, if you wan to add an MVC-based UI
        services.AddControllersWithViews();
        //services.AddMvc();

        string connectionString = Configuration.GetConnectionString("IdentityDbContextConnection");
        //const string connectionString = @"Data Source=(LocalDb)\MSSQLLocalDB;database=IdentityServer4.Quickstart.EntityFramework-3.0.102;trusted_connection=yes;";
        var migrationsAssembly = typeof(Startup).GetTypeInfo().Assembly.GetName().Name;

        services.AddDbContext<IdentityServer.Models.IdentityDbContext>(options =>
            options.UseSqlServer(connectionString, sql => sql.MigrationsAssembly(migrationsAssembly))
        );

        services.AddIdentity<ApplicationUser, IdentityRole>(options =>
        {
            options.SignIn.RequireConfirmedEmail = true;
        })
        .AddEntityFrameworkStores<IdentityServer.Models.IdentityDbContext>()
        .AddDefaultTokenProviders();

        services.AddMvc(options =>
        {
            options.EnableEndpointRouting = false;
        })
        .SetCompatibilityVersion(CompatibilityVersion.Latest);

        var builder = services.AddIdentityServer(options =>
        {
            options.Events.RaiseErrorEvents = true;
            options.Events.RaiseInformationEvents = true;
            options.Events.RaiseFailureEvents = true;
            options.Events.RaiseSuccessEvents = true;
            options.UserInteraction.LoginUrl = "/Account/Login";
            options.UserInteraction.LogoutUrl = "/Account/Logout";
            options.Authentication = new AuthenticationOptions()
            {
                CookieLifetime = TimeSpan.FromHours(10), // ID server cookie timeout set to 10 hours
                CookieSlidingExpiration = true
            };
        })
        .AddSigningCredential(X509.GetCertificate("B22BBE7C991CEF13F470481A4042D1E091967FCC"))   // signing.crt thumbprint
        .AddValidationKey(X509.GetCertificate("321ABA505F6FCDDD00AA5EC2BD307F0C9002F9A8"))       // validation.crt thumbprint
        .AddConfigurationStore(options =>
        {
            options.ConfigureDbContext = b => b.UseSqlServer(connectionString, sql => sql.MigrationsAssembly(migrationsAssembly));
        })
        .AddOperationalStore(options =>
        {
            options.ConfigureDbContext = b => b.UseSqlServer(connectionString, sql => sql.MigrationsAssembly(migrationsAssembly));
            options.EnableTokenCleanup = true;
        })
        .AddAspNetIdentity<ApplicationUser>();

        services.AddAuthentication()
        .AddGoogle("Google", options =>
        {
            options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;

            options.ClientId = "174637674775-7bgu471gtme25sr5iagq5agq6riottek.apps.googleusercontent.com";
            options.ClientSecret = "V_UsR825ZWxCB9i2xrN-u1Kj";
        });

        services.AddTransient<IEmailSender, IdentityEmailSender>();

        services.AddCors(options => options.AddPolicy("AllowAll", p => p.AllowAnyOrigin()
        .AllowAnyMethod()
        .AllowAnyHeader()));

        services.Configure<CookiePolicyOptions>(options =>
        {
            options.CheckConsentNeeded = context => true;
            options.MinimumSameSitePolicy = SameSiteMode.Strict;
        });

        services.AddScoped<IProfileService, ProfileService>();

        telemetryClient.TrackTrace("============== Startup ConfigureServices finish OK ============== ");

    }
    catch (Exception e)
    {
        telemetryClient.TrackTrace("Exception general in ConfigureServices");
        telemetryClient.TrackException(e);
        throw;
    }
}

and this :

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    try
    {
        telemetryClient.TrackTrace("============== Startup Configure ============== ");

        InitializeDatabase(app);

        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        app.UseStaticFiles();
        app.UseCors("AllowAll");
        app.UseRouting();

        app.UseIdentityServer();
        app.UseAuthorization();

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

        app.UseMvcWithDefaultRoute();

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

        telemetryClient.TrackTrace("============== Startup Configure finish OK============== ");
    }
    catch (Exception e)
    {
        telemetryClient.TrackTrace("Exception general in Configure");
        telemetryClient.TrackException(e);
        throw;
    }
}

So the problem is with

the identityserver4 localhost the login / logout works find

the idnetityserver4 hosted on azure the login is skipped and go diectly to the homepage (the user is authenticated with previous login).

Sorry to be a little long, I haven't seen this exact problem on stackoverflow or somewhere else.

Thanx in advance !

fguigui
  • 294
  • 1
  • 4
  • 11
  • some precisions observed after posting, it seems there is an interference between the authentication of the identityserver4 itself and the authentication of a client mvc/api. If I logout from identityserver4 then I'm able to login to the client app (challenge is done, login view appears, and login works). – fguigui May 15 '20 at 07:59

2 Answers2

0

There are several things that can go wrong with moving your app to production.

I suspect that if you are redirected back to your Homepage, that the auth cookies are not being removed by your SignOutAsync("Cookies) call.

Check these:

  1. PostLogoutRedirectUris contain your azure domain + "signout-callback-oidc"
  2. Check on what path is your Auth cookies created. If different from "/" - add the default path. I guess in your case it would be somewhat among the lines of:

    options.Authentication = new AuthenticationOptions()
    {
        CookieLifetime = TimeSpan.FromHours(10), // ID server cookie timeout set to 10 hours
        CookieSlidingExpiration = true,
        Path = "/"
    };
    
Riste
  • 143
  • 1
  • 8
  • Thanx Riste, I don't think it is a matter of cookies management, I suppose your code is to be put on the identityServer4 ? Anyway, I've just seen this recently, it is about the session managed by idenstityServer4. We have to end this session, then redirect to the client application. There examples at : https://andersonnjen.com/2019/03/22/identityserver4-global-logout/ and https://stackoverflow.com/questions/56477130/how-to-redirect-user-to-client-app-after-logging-out-from-identity-server. It almost works, but I'm still stuck in IdentityServer4 side after logout, can't redirect to client app. – fguigui May 25 '20 at 06:09
  • The code is to be put in on the client-side... I suspected that your cookie on the client-side has not been removed because of some virtual directory on azure... PostLogoutRedirectUris list (on IDP) must contain the URL you want to be redirected at, sent from your client which is set in CallbackPath (on client-side, which is a relative path because of cookie clearing). CallbackPath is by default "/signout-callback-oidc" that is why in your PostLogoutRedirectUris (on IDP) you must have "client-domain/signout-callback-oidc". – Riste May 25 '20 at 20:37
  • Also, try finding the code in your IdentityServer4 which builds the logout model: var logout = await _interaction.GetLogoutContextAsync(logoutId); and see if in your LoggedOutViewModel you have the property set: AutomaticRedirectAfterSignOut = AccountOptions.AutomaticRedirectAfterSignOut – Riste May 25 '20 at 20:37
0

You're right Riste, According to differents posts I've seen, we can do such a thing :

- first of all, we have to put FrontChannelLogoutUri parameter 

(it should be an mvc client controller/action called by identityserver4, in our case should be something like https://localhost:999/Account/FrontChannelLogout) for the client mvc app, generally it is put in Config.cs and add this parameter for the client Mvc (with RedirectUris, PostLogoutRedirectUris, ...)

- on the client mvc, in an account controller (for instance) where is managed the login , 

we can add / modifiy the logout management :

[Authorize]
public async Task<IActionResult> Logout()
{
    var client = new HttpClient();

    var disco = await client.GetDiscoveryDocumentAsync($"https://{Startup.Configuration["Auth0:Domain"]}");

    return Redirect(disco.EndSessionEndpoint);
}

public async Task<IActionResult> FrontChannelLogout(string sid)
{
    if (User.Identity.IsAuthenticated)
    {
        var currentSid = User.FindFirst("sid")?.Value ?? "";
        if (string.Equals(currentSid, sid, StringComparison.Ordinal))
        {
            await HttpContext.SignOutAsync("oidc");
            await HttpContext.SignOutAsync("Identity.Application");
            await _signInManager.Context.SignOutAsync("_af");
            await _signInManager.Context.SignOutAsync("idsrv.session");
            await _signInManager.Context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
        }
    }

    return NoContent();
}

On the identityserver4 side :

In the QuickStart Account Controller, we have to update the BuildLoggedOutViewModelAsync method :

    private async Task<LoggedOutViewModel> BuildLoggedOutViewModelAsync(string logoutId)
    {
        // get context information (client name, post logout redirect URI and iframe for federated signout)
        var logout = await _interaction.GetLogoutContextAsync(logoutId);

        var client = await _clientStore.FindEnabledClientByIdAsync(logout.ClientIds.First());

        if (!string.IsNullOrEmpty(client.FrontChannelLogoutUri))
        {
            //var pos = GetNthIndex(client.FrontChannelLogoutUri, '/', 3);
            //logout.PostLogoutRedirectUri = client.FrontChannelLogoutUri.Substring(0, Math.Min(client.FrontChannelLogoutUri.Length, pos));
            // Here TODO =====> get the real PostLogoutRedirectUri, it should be a controller/action url on the client mvc side and put it in **logout.PostLogoutRedirectUri**
        }

        var vm = new LoggedOutViewModel
        {
            AutomaticRedirectAfterSignOut = AccountOptions.AutomaticRedirectAfterSignOut,
            PostLogoutRedirectUri = logout?.PostLogoutRedirectUri,
            ClientName = string.IsNullOrEmpty(logout?.ClientName) ? logout?.ClientId : logout?.ClientName,
            SignOutIframeUrl = logout?.SignOutIFrameUrl,
            LogoutId = logoutId
        };

        if (User?.Identity.IsAuthenticated == true)
        {
            var idp = User.FindFirst(JwtClaimTypes.IdentityProvider)?.Value;
            if (idp != null && idp != IdentityServer4.IdentityServerConstants.LocalIdentityProvider)
            {
                var providerSupportsSignout = await HttpContext.GetSchemeSupportsSignOutAsync(idp);
                if (providerSupportsSignout)
                {
                    if (vm.LogoutId == null)
                    {
                        // if there's no current logout context, we need to create one
                        // this captures necessary info from the current logged in user
                        // before we signout and redirect away to the external IdP for signout
                        vm.LogoutId = await _interaction.CreateLogoutContextAsync();
                    }

                    vm.ExternalAuthenticationScheme = idp;
                }
            }
        }

        return vm;
    }

====> Apparently _interaction.GetLogoutContextAsync(logoutId) never return a PostLogoutRedirectUri even though it has been set up for the mvc client (in the Config.cs).

====> by filling this parameter logout.PostLogoutRedirectUri on identityServer4 side it'll redirect the logout to the client app.

Here is what I can say, I don't know if the logout redirect to the client app is a "standard" behavior, don't know if it was planned in identityserver4.

Some links :

https://andersonnjen.com/2019/03/22/identityserver4-global-logout/ How to redirect user to client app after logging out from identity server?

Thanx !

fguigui
  • 294
  • 1
  • 4
  • 11