Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Google chrome losing MVC Auth Cookie (Set-Cookie directive) in subsequent calls

I'm using ASP.net Core 1, MVC 6. I am using SignInManager and UserManager, to authenticate a user in a web api application (MVC6 / C#) from another MVC application (the web api Logon method is actually called from a Jquery Ajax request).

In IE, I call the Login method and when successful, it gives me a Set-Cookie response with an ASP.net auth cookie. I can then see subsequent requests have the ASP.net auth cookie attached.

In chrome, the Set-Cookie directive is returned in the response, but subsequent requests do not have the cookie attached.

Why is this happening?

The only difference I can see is that in Chrome, there is a pre-flight OPTIONS request being sent, but I have handled that in the startup.cs file in the web api and am essentially ignoring it.

Internet Explorer

My request to Login web api looks like this:

   Accept */*
   Accept-Encoding gzip, deflate
   Accept-Language en-IE
   Cache-Control no-cache
   Connection Keep-Alive
   Content-Length 246
   Content-Type application/x-www-form-urlencoded; charset=UTF-8
   Cookie BeaeN4tYO5M=CfDJ8KMNkK4F2ylMlo1LFxNzxWLNDECVWfhxBYRQrw_MkNQBrVIwfO6FoMIMqg1PP-nZa8Dhp3IV1ZS1uXKpknUDYegiMlEvFaNG-wqUXErvQ5wkMMc_HBI88j-7bCbD2Q7P_B6fEQOQSTKHoL5sTcH0MoM
   DNT 1
   Host localhost:44338
   Referer https://localhost:44356/
   Request POST /api/account/Login HTTP/1.1
   User-Agent Mozilla/5.0 (Windows NT 6.3; WOW64; Trident/7.0; rv:11.0) like Gecko
   X-ACL-Key 4A6F0007-95E7-4423-B786-6FBF981799FE

Response like this:

   Response HTTP/1.1 200 OK
   Cache-Control no-cache
   Pragma no-cache
   Content-Type application/json; charset=utf-8
   Expires -1
   Server Kestrel
   Set-Cookie oAuthInterop=CfDJ8Asqo6qO2cNHlXpsdNLsuoQWhLxXcnaNkAMTB-VvpkMRIz2AiM_7feoIM29gza_zZz97qaE6TKdqK8y1jDPjDDyiiMdOMiuCmCoV5X4IQ9xtHvpGgmFoxOSiYFVeVOBbHsLx4BccL647F9sJ07M55zvjMx_7wrt32omhONH64vmc12P3nepwZjNSIFYfom1U0Z4r4EX_0tZjKRH7FrdvO0PI2iY5SMaKhCcBw1QXpQHSUxL6Hm-Wr8Q46gFAYoa6YffJV0Rx80FvJHmr1LMAA6PAF0dU_DzNdRVHdXm14t_nbfl-6xb6o7WQN259moUhkT1ZQ9CZsYwWvn7VBmpjfIXNJvIu0FDnRaHnNMrj3uN77_cAMdO3OcyCuy-CAKJ9c-0PxKToStb9juGSNa9ClpVQPADzpUxFqxZU029AXBPavXQK2Ezvy7YT4FwCkL8TEf5AnB5hfOZ5YCBlqD30n2heMdHDbXRHpxeaQB4aoY_6uSpJ3cPazBDsbvGi4fV2-0g5NvoTGgJUXa5p4UntRmuiJ2tZHbMmEjXzf-GV6QtTFIhseKsS3n6TMX68yqQOhYOzxvHdJXPjYxvjmm6-vJw5w2FDgiEXoQJQ7qaSmGzRwOA_cE4VBV_RhzrZELmp3A; path=/; secure; httponly
   X-SourceFiles =?UTF-8?B?QzpcVXNlcnNcUm9iZXJ0XERlc2t0b3BcSEJFIE1hbmFnZXJcTUFJTlxCbHVlem9uZSBXZWJBcGlcc3JjXEJ6LkFwcGxpY2F0aW9uXEJ6LkFwcGxpY2F0aW9uLkFwaVx3d3dyb290XGFwaVxhY2NvdW50XExvZ2lu?=
   X-Powered-By ASP.NET
   Access-Control-Allow-Methods GET,PUT,POST,DELETE
   Access-Control-Allow-Headers Content-Type,x-xsrf-token,X-ACL-Key
   Date Fri, 06 May 2016 14:23:22 GMT
   Content-Length 16

Subsequent test web api call (IsLoggedIn):

   Request GET /api/account/IsLoggedIn HTTP/1.1
   X-ACL-Key 4A6F0007-95E7-4423-B786-6FBF981799FE
   Accept */*
   Referer https://localhost:44356/
   Accept-Language en-IE
   Accept-Encoding gzip, deflate
   User-Agent Mozilla/5.0 (Windows NT 6.3; WOW64; Trident/7.0; rv:11.0) like Gecko
   Host localhost:44338
   DNT 1
   Connection Keep-Alive
   Cache-Control no-cache
   Cookie BeaeN4tYO5M=CfDJ8KMNkK4F2ylMlo1LFxNzxWLNDECVWfhxBYRQrw_MkNQBrVIwfO6FoMIMqg1PP-nZa8Dhp3IV1ZS1uXKpknUDYegiMlEvFaNG-wqUXErvQ5wkMMc_HBI88j-7bCbD2Q7P_B6fEQOQSTKHoL5sTcH0MoM; oAuthInterop=CfDJ8Asqo6qO2cNHlXpsdNLsuoQWhLxXcnaNkAMTB-VvpkMRIz2AiM_7feoIM29gza_zZz97qaE6TKdqK8y1jDPjDDyiiMdOMiuCmCoV5X4IQ9xtHvpGgmFoxOSiYFVeVOBbHsLx4BccL647F9sJ07M55zvjMx_7wrt32omhONH64vmc12P3nepwZjNSIFYfom1U0Z4r4EX_0tZjKRH7FrdvO0PI2iY5SMaKhCcBw1QXpQHSUxL6Hm-Wr8Q46gFAYoa6YffJV0Rx80FvJHmr1LMAA6PAF0dU_DzNdRVHdXm14t_nbfl-6xb6o7WQN259moUhkT1ZQ9CZsYwWvn7VBmpjfIXNJvIu0FDnRaHnNMrj3uN77_cAMdO3OcyCuy-CAKJ9c-0PxKToStb9juGSNa9ClpVQPADzpUxFqxZU029AXBPavXQK2Ezvy7YT4FwCkL8TEf5AnB5hfOZ5YCBlqD30n2heMdHDbXRHpxeaQB4aoY_6uSpJ3cPazBDsbvGi4fV2-0g5NvoTGgJUXa5p4UntRmuiJ2tZHbMmEjXzf-GV6QtTFIhseKsS3n6TMX68yqQOhYOzxvHdJXPjYxvjmm6-vJw5w2FDgiEXoQJQ7qaSmGzRwOA_cE4VBV_RhzrZELmp3A

Response like this:

   Response HTTP/1.1 200 OK
   Content-Type application/json; charset=utf-8
   Server Kestrel
   X-SourceFiles =?UTF-8?B?QzpcVXNlcnNcUm9iZXJ0XERlc2t0b3BcSEJFIE1hbmFnZXJcTUFJTlxCbHVlem9uZSBXZWJBcGlcc3JjXEJ6LkFwcGxpY2F0aW9uXEJ6LkFwcGxpY2F0aW9uLkFwaVx3d3dyb290XGFwaVxhY2NvdW50XElzTG9nZ2VkSW4=?=
   X-Powered-By ASP.NET
   Access-Control-Allow-Methods GET,PUT,POST,DELETE
   Access-Control-Allow-Headers Content-Type,x-xsrf-token,X-ACL-Key
   Date Fri, 06 May 2016 14:23:22 GMT
   Content-Length 68

CHROME

My request to Login web api looks like this:

 POST /api/account/Login HTTP/1.1
 Host: localhost:44338
 Connection: keep-alive
 Content-Length: 246
 Accept: */*
 Origin: https://localhost:44356
 Content-Type: application/x-www-form-urlencoded; charset=UTF-8
 User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.94 Safari/537.36
 X-ACL-Key: 4A6F0007-95E7-4423-B786-6FBF981799FE
 Referer: https://localhost:44356/
 Accept-Encoding: gzip, deflate
 Accept-Language: en-GB,en-US;q=0.8,en;q=0.6

Response like this:

   HTTP/1.1 200 OK
   Cache-Control: no-cache
   Pragma: no-cache
   Content-Type: application/json; charset=utf-8
   Expires: -1
   Vary: Origin
   Server: Kestrel
   Set-Cookie: oAuthInterop=CfDJ8Asqo6qO2cNHlXpsdNLsuoRvlRjfUBWrkt3W3NzBJIoFYA6DcQivnfYmZV2O5xuiqpd75oRjZ-JeHBcjiOK0HoFJQ9f61RyJ2HDeuCNmQk0H-pA3Lzs5ft_F49dpQt0kFn3_-FzEh5-NScCbY4N6TiuYlWY4VSoKsdJJ91k7Z4LQO-0Wm3cZ6HfX0E6pLzGG4lWaZGuV-gOsVCRygR5nv_O_YpWwfaLsT_51aX6fNXVSotU6MECEkFdfWseqOGyYVj7KJrxY2mPwksE0XGACs12TnmfJzCABrzd06FnTPy3RuqJF2IWOobX6ZAHGMoAVFR07mhy9gMPyaHQ12RKmhBhZSXE-Yi3BHow2ER9d2Niligx7JjwYR7UfHFHWJdoYzewLRkZZGE5pw67O710hYyA2UCM2ODB9l9x-WDQ1A_3xjxu2Mrkp0lrF0V-h3y6V2gzEP9RyQAjDISEEZQqvb-GzfZrsRzzQcMn0TMhq5_LUKkX3AScSGRiarBzZ2O9Af3jzwTmN1BciJknJwMKRefq_zrXH7kymCD1kJM89aGkswqp2bycMQjlsjqg5k8EEhv8u1kLA7hA9NyE2ZaamB1PAWYz4NXi3Agccgw83nFi4bs6VE8ZLnyZFEwxdyEGyvQ; path=/; secure; httponly
   Access-Control-Allow-Origin: https://localhost:44356
   Access-Control-Allow-Credentials: true
   X-SourceFiles: =?UTF-8?B?QzpcVXNlcnNcUm9iZXJ0XERlc2t0b3BcSEJFIE1hbmFnZXJcTUFJTlxCbHVlem9uZSBXZWJBcGlcc3JjXEJ6LkFwcGxpY2F0aW9uXEJ6LkFwcGxpY2F0aW9uLkFwaVx3d3dyb290XGFwaVxhY2NvdW50XExvZ2lu?=
   X-Powered-By: ASP.NET
   Access-Control-Allow-Methods: GET,PUT,POST,DELETE
   Access-Control-Allow-Headers: Content-Type,x-xsrf-token,X-ACL-Key
   Date: Fri, 06 May 2016 12:59:36 GMT
   Content-Length: 16

Subsequent test web api call (IsLoggedIn):

GET /api/account/IsLoggedIn HTTP/1.1
   Host: localhost:44338
   Connection: keep-alive
   Accept: */*
   Origin: https://localhost:44356
   User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.94 Safari/537.36
   X-ACL-Key: 4A6F0007-95E7-4423-B786-6FBF981799FE
   Referer: https://localhost:44356/
   Accept-Encoding: gzip, deflate, sdch
   Accept-Language: en-GB,en-US;q=0.8,en;q=0.6

Response like this:

HTTP/1.1 401 Unauthorized
   Content-Length: 0
   Content-Type: text/plain; charset=utf-8
   Server: Kestrel
   X-SourceFiles: =?UTF-8?B?QzpcVXNlcnNcUm9iZXJ0XERlc2t0b3BcSEJFIE1hbmFnZXJcTUFJTlxCbHVlem9uZSBXZWJBcGlcc3JjXEJ6LkFwcGxpY2F0aW9uXEJ6LkFwcGxpY2F0aW9uLkFwaVx3d3dyb290XGFwaVxhY2NvdW50XElzTG9nZ2VkSW4=?=
   X-Powered-By: ASP.NET
   Access-Control-Allow-Methods: GET,PUT,POST,DELETE
   Access-Control-Allow-Headers: Content-Type,x-xsrf-token,X-ACL-Key
   Date: Fri, 06 May 2016 12:59:43 GMT

My web api controller code looks like this:

[Authorize]
    [EnableCors("AllowAll")]
    [Route("api/[controller]")]
    public class AccountController : Controller
    {
        private readonly UserManager<ApplicationUser> _userManager;
        private readonly SignInManager<ApplicationUser> _signInManager;

        public AccountController(UserManager<ApplicationUser> userManager, SignInManager<ApplicationUser> signInManager)
        {
            _userManager = userManager;
            _signInManager = signInManager;
        }

        [HttpPost("login")]
        [AllowAnonymous]
        public async Task<IActionResult> Login(UserLogin model)
        {
            if (ModelState.IsValid) {
                var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, lockoutOnFailure: false);

                if (result.Succeeded) {
                    return Json(new { success = true });
                }
                if (result.RequiresTwoFactor) {
                    return Json(new { success = false, errType = 1 });
                }
                if (result.IsLockedOut) {
                    return Json(new { success = false, errType = 2 });
                } else {
                    ModelState.AddModelError(string.Empty, "Invalid login attempt.");
                    return Json(new { success = false, errType = 3 });
                }
            }

            return Json(new { success = false, errType = 0 });
        }

        [HttpGet("IsLoggedIn")]
        public IActionResult IsLoggedIn()
        {
            return Json(new {
                loggedon = (HttpContext.User.Identity.Name != null && HttpContext.User.Identity.IsAuthenticated),
                isauthenticated = HttpContext.User.Identity.IsAuthenticated,
                username = HttpContext.User.Identity.Name
            });
        }
    }

Startup.cs for my web api looks like this:

public class Startup
    {
        public static int SessionLength { get; private set; }
        private string Connection;

        public Startup(IHostingEnvironment env)
        {
            // Set up configuration sources.
            var builder = new ConfigurationBuilder()
                .AddJsonFile("appsettings.json")
                .AddEnvironmentVariables();
            Configuration = builder.Build();

            SessionLength = 30;
        }

        public IConfigurationRoot Configuration { get; set; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            // Get the configured connection string.
            Connection = Configuration["Data:DefaultConnection:ConnectionString"];

            var userStore = new CustomUserStore();
            var roleStore = new CustomRoleStore();
            var userPrincipalFactory = new CustomUserPrincipalFactory();

            services.AddInstance<IUserStore<ApplicationUser>>(userStore);
            services.AddInstance<IRoleStore<ApplicationRole>>(roleStore);
            services.AddInstance<IUserClaimsPrincipalFactory<ApplicationUser>>(userPrincipalFactory);

            services.AddIdentity<ApplicationUser, ApplicationRole>(options => {
                options.Cookies.ApplicationCookie.Events = new CookieAuthenticationEvents() {
                    OnRedirectToAccessDenied = ctx =>
                    {
                        if (ctx.Response.StatusCode == (int)HttpStatusCode.Unauthorized || ctx.Response.StatusCode == (int)HttpStatusCode.Forbidden) {
                            return Task.FromResult<object>(null);
                        }
                        ctx.Response.Redirect(ctx.RedirectUri);
                        return Task.FromResult<object>(null);
                    },
                    OnRedirectToLogin = ctx =>
                    {
                        if (ctx.Response.StatusCode == (int)HttpStatusCode.Unauthorized || ctx.Response.StatusCode == (int)HttpStatusCode.Forbidden) {
                            return Task.FromResult<object>(null);
                        }
                        ctx.Response.Redirect(ctx.RedirectUri);
                        return Task.FromResult<object>(null);
                    }
                };

                //options.Cookies.ApplicationCookie.CookieHttpOnly = false;
                options.Cookies.ApplicationCookieAuthenticationScheme = "ApplicationCookie";
                options.Cookies.ApplicationCookie.AuthenticationScheme = "ApplicationCookie";
                options.Cookies.ApplicationCookie.CookieName = "oAuthInterop";
                options.Cookies.ApplicationCookie.AutomaticChallenge = true;
                options.Cookies.ApplicationCookie.AutomaticAuthenticate = true;
                options.Cookies.ApplicationCookie.DataProtectionProvider = new DataProtectionProvider(new DirectoryInfo("d:\\development\\artefacts"),
                    configure =>
                    {
                        configure.SetApplicationName("TestAuthApp");
                        //configure.ProtectKeysWithCertificate("thumbprint");
                    });
                options.Cookies.ApplicationCookie.ExpireTimeSpan = TimeSpan.FromMinutes(SessionLength);

            }).AddDefaultTokenProviders();

            // Add framework services.
            services.AddMvc();

            // Add cross site calls.
            //TODO: implement with better security instead of allowing everything through.
            services.AddCors(options => options.AddPolicy("AllowAll", p => p.AllowAnyOrigin()
                                                                            .AllowAnyMethod()
                                                                            .AllowAnyHeader().AllowCredentials()));
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
        {
            loggerFactory.AddConsole(Configuration.GetSection("Logging"));
            loggerFactory.AddDebug();

            app.UseIISPlatformHandler(options => options.AuthenticationDescriptions.Clear());

            app.UseStaticFiles();

            app.UseIdentity();

            app.UseMvc();

        }
    }
like image 416
Rob McCabe Avatar asked May 06 '16 14:05

Rob McCabe


1 Answers

A wild guess would be you are not setting withCredentials flag on your XMLHttpRequest when making cross-domain request from javascript via ajax. This flag basically controls whether to include credentials (such as cookies, authorization headers or client certificates) in cross-domain request. Why it still works in IE? Not completely sure, but maybe because proper implementation of this flag only appeared in IE10, and you might use another version of IE. If you use jquery to make requests, see here how to set this flag.

If that is not the case, please include your client-side code + request and response headers of Chrome's OPTIONS request.

like image 55
Evk Avatar answered Oct 18 '22 02:10

Evk