Identity server 4 post login redirect not working in chrome only

11,558

Solution 1

I had issues with chrome and edge recently but a few months ago was only chrome. So for me with .Net Core 3 and IdentityServer4 version 3.1.2 started working by adding the following code to startup.cs:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ...)
    {
            app.UseCookiePolicy(new CookiePolicyOptions
            {
                MinimumSameSitePolicy = SameSiteMode.Lax
            });

Note: Make sure that you add this policy to the beginning of the Configure method not end in the startup.cs otherwise is not working.

Solution 2

I had a similar issue with IdentityServer4 on .NET Core 2.2. Your problem might be connected with those breaking changes in new browsers versions like Chrome or Firefox:

https://docs.microsoft.com/en-gb/dotnet/core/compatibility/3.0-3.1#http-browser-samesite-changes-impact-authentication

For me the working solution was to turn off the SameSite configuration for cookies at all. Such possibility for .NET Core 2.2 is described here:

https://docs.microsoft.com/en-us/aspnet/core/security/samesite?view=aspnetcore-3.1

(if your solution is on .NET Core 3.1 then in code below, instead using (SameSiteMode)(-1) you should use SameSiteMode.Unspecified)

FIX: in Startup.cs file in ConfigureServices method, right after IdentityServerBuilder is created...

var builder = services.AddIdentityServer(options =>
            {....});

...I've added the following configuration change:

builder.Services.ConfigureExternalCookie(options => {
   options.Cookie.IsEssential = true;
      options.Cookie.SameSite = (SameSiteMode)(-1); //SameSiteMode.Unspecified in .NET Core 3.1
   });

builder.Services.ConfigureApplicationCookie(options => {
   options.Cookie.IsEssential = true;
      options.Cookie.SameSite = (SameSiteMode)(-1); //SameSiteMode.Unspecified in .NET Core 3.1
});

Solution 3

You will get below console warring in Google Chrome and your Identity server failed to redirect to Client App for Chrome version 80.

A cookie associated with a resource at was set with SameSite=None but without Secure. It has been blocked, as Chrome now only delivers cookies marked SameSite=None if they are also marked Secure. You can review cookies in developer tools under Application>Storage>Cookies and see more details at https://www.chromestatus.com/feature/5633521622188032.

To Fix this , you need to do changes mention in below link -

https://www.thinktecture.com/en/identity/samesite/prepare-your-identityserver/

NOTE : For .Net Core 2.2 , set SameSite = (SameSiteMode)(-1) , For .Net Core 3.0 or above , set SameSite = SameSiteMode.Unspecified

Also , for Chrome 80 version , add this extra condition -

 if (userAgent.Contains("Chrome/8"))
 {
     return true;
 }

Solution 4

Setting OpenidConnectionOptions worked for me

CorrelationCookie.SameSite = SameSiteMode.Lax;
NonceCookie.SameSite = SameSiteMode.Lax;
Share:
11,558
Pasha Guterman
Author by

Pasha Guterman

Updated on June 14, 2022

Comments

  • Pasha Guterman
    Pasha Guterman almost 2 years

    i use identity server 4 let call it "auth-server" run on .net core 3.1. there is angular app request authentication after redirected to auth-server and provide credentials submiting the login it's not redirect back to client app. the issue is only in chrome browser (firefox & edge works fine) i can see the redirect request - Request-Url but it just go back to login page Client congig:

    public static IEnumerable<Client> GetClients()
    {
        return new List<Client>(){
                new Client() {
                                 RequireConsent =false,
                                 RequireClientSecret = false,
                                 ClientId = "takbull-clientapp-dev",
                                 ClientName = "Takbull Client",
                                 AllowedGrantTypes = GrantTypes.ImplicitAndClientCredentials,
                                 AllowedScopes = new List<string> 
                                 {
                                  IdentityServerConstants.StandardScopes.OpenId,
                                  IdentityServerConstants.StandardScopes.Email,
                                  IdentityServerConstants.StandardScopes.Profile,
                                  "takbull",
                                  "takbull.api" 
                                 },
                                 // where to redirect to after login
                                 RedirectUris = new List<string>()
                                 {
                                     "http://localhost:4200/auth-callback/",
                                     "http://localhost:4200/silent-refresh.html",
                                 },
                                 //TODO: Add Production URL
                                 // where to redirect to after logout
                                 PostLogoutRedirectUris =new List<string>() 
                                 {
                                     "http://localhost:4200"
                                 },
                                 AllowedCorsOrigins = {"http://localhost:4200"},
                                 AllowAccessTokensViaBrowser = true,
                                 AccessTokenLifetime = 3600,
                                 AlwaysIncludeUserClaimsInIdToken = true
                             },
            };
        }
    

    Login Code:

    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> Login(LoginInputModel model, string button)
    {
        // check if we are in the context of an authorization request
        var context = await _interaction.GetAuthorizationContextAsync(model.ReturnUrl);
    
        // the user clicked the "cancel" button
        if (button != "login")
        {
            if (context != null)
            {
                // if the user cancels, send a result back into IdentityServer as if they 
                // denied the consent (even if this client does not require consent).
                // this will send back an access denied OIDC error response to the client.
                await _interaction.GrantConsentAsync(context, ConsentResponse.Denied);
    
                // we can trust model.ReturnUrl since GetAuthorizationContextAsync returned non-null
                if (await _clientStore.IsPkceClientAsync(context.ClientId))
                {
                    // if the client is PKCE then we assume it's native, so this change in how to
                    // return the response is for better UX for the end user.
                    return View("Redirect", new RedirectViewModel { RedirectUrl = model.ReturnUrl });
                 }
    
                 return Redirect(model.ReturnUrl);
             }
             else
             {
                // since we don't have a valid context, then we just go back to the home page
                return Redirect("~/");
             }
         }
    
         if (ModelState.IsValid)
         {
             // validate username/password against in-memory store
             var ValidResp = await _users.ValidateCredentials(model.Username, model.Password);
             if (ValidResp.LogInStatus == LogInStatus.Success)
             {
                 var user = _users.FindByUsername(model.Username);
                 //await _events.RaiseAsync(new UserLoginSuccessEvent(user.Username, user.SubjectId, user.Username));
                 await _events.RaiseAsync(new UserLoginSuccessEvent(user.Email, user.UserId.ToString(), user.Email));
    
                 // only set explicit expiration here if user chooses "remember me". 
                 // otherwise we rely upon expiration configured in cookie middleware.
                 AuthenticationProperties props = null;
                 if (AccountOptions.AllowRememberLogin && model.RememberLogin)
                 {
                     props = new AuthenticationProperties
                     {
                        IsPersistent = true,
                        ExpiresUtc = DateTimeOffset.Now.Add(AccountOptions.RememberMeLoginDuration)
                     };
                 };
    
                 // issue authentication cookie with subject ID and username
                 //await HttpContext.SignInAsync(user.SubjectId, user.Username, props);
                 // issue authentication cookie with subject ID and username
                 await HttpContext.SignInAsync(user.UserId.ToString(), user.FirstName + " " + user.LastName, props, _users.GetClaims(user).ToArray());
    
                 if (context != null)
                 {
                     if (await _clientStore.IsPkceClientAsync(context.ClientId))
                     {
                         // if the client is PKCE then we assume it's native, so this change in how to
                         // return the response is for better UX for the end user.
                         return View("Redirect", new RedirectViewModel { RedirectUrl = model.ReturnUrl });
                     }
    
                     // we can trust model.ReturnUrl since GetAuthorizationContextAsync returned non-null
                     return Redirect(model.ReturnUrl);
                 }
    
                 // request for a local page
                 if (Url.IsLocalUrl(model.ReturnUrl))
                 {
                     return Redirect(model.ReturnUrl);
                 }
                 else if (string.IsNullOrEmpty(model.ReturnUrl))
                 {
                     return Redirect("~/");
                 }
                 else
                 {
                     // user might have clicked on a malicious link - should be logged
                     throw new Exception("invalid return URL");
                 }
             }
    
             await _events.RaiseAsync(new UserLoginFailureEvent(model.Username, ValidResp.ResponseDescription));
             ModelState.AddModelError(string.Empty, ValidResp.ResponseDescription);
         }
    
         // something went wrong, show form with error
         var vm = await BuildLoginViewModelAsync(model);
         return View(vm);
    }
    
  • Roel
    Roel over 3 years
    The Chrome 80 extra condiction can be added to this code: github.com/IdentityServer/IdentityServer4/blob/… Here you call the extenstion method: github.com/IdentityServer/IdentityServer4/blob/main/src/…. But if Chrome hits version 90 or more it won't work anymore!