Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Asp.Net Identity with 2FA - remember browser cookie not retained after session

I'm using the latest sample code for MVC5.2 with Asp.Identity and Two Factor authentication.

With 2FA enabled, when a user logins, the get prompted for a code (sent by phone or email) and they have the option to "Remember Browser" - so that they don't get ask for codes again on that browser.

This is handled in the VerifyCode action

var result = await SignInManager.TwoFactorSignInAsync(model.Provider, model.Code, isPersistent:  model.RememberMe, rememberBrowser: model.RememberBrowser);

Note that model.RememberMe is not used in the default templates so it is false.

I find when I do this the .AspNet.TwoFactorRememberBrowser that gets set, expires on session end (so it does not remember the browser)

Now if I set isPersistent = true, .AspNet.TwoFactorRememberBrowser gets an expiration of 30 days which is great, but the .AspNet.ApplicationCookie also gets a 30 day expiration - which means that when I close the browser and re-open, I am automatically logged in.

I want it so that it doesn't persist my login, but that it will persist my choice of remembering the 2FA code. Ie the user should always have to login, but they should not be asked for a 2fa code if they have already save it.

Has anybody else seen this, or am I missing something?

like image 461
Peter Kerr Avatar asked Feb 26 '15 14:02

Peter Kerr


2 Answers

It doesn't seem like this code was designed to set more than one identity cookie in the same request/response because the OWIN cookie handlers end up sharing the same AuthenticationProperties. This is because the AuthenticationResponseGrant has a single principal, but the principal can have multiple identities.

You can workaround this bug by altering and then restoring the AuthenticationProperties in the ResponseSignIn and ResponseSignedIn events specific to the 2FA cookie provider:

        //Don't use this.
        //app.UseTwoFactorRememberBrowserCookie(DefaultAuthenticationTypes.TwoFactorRememberBrowserCookie);

        //Set the 2FA cookie expiration and persistence directly
        //ExpireTimeSpan and SlidingExpiration should match the Asp.Net Identity cookie setting
        app.UseCookieAuthentication(new CookieAuthenticationOptions()
        {
            AuthenticationType = DefaultAuthenticationTypes.TwoFactorRememberBrowserCookie,
            AuthenticationMode = AuthenticationMode.Passive,
            CookieName = DefaultAuthenticationTypes.TwoFactorRememberBrowserCookie,
            ExpireTimeSpan = TimeSpan.FromHours(2),
            SlidingExpiration = true,
            Provider = new CookieAuthenticationProvider
            {
                OnResponseSignIn = ctx =>
                {
                    ctx.OwinContext.Set("auth-prop-expires", ctx.Properties.ExpiresUtc);
                    ctx.OwinContext.Set("auth-prop-persist", ctx.Properties.IsPersistent);
                    var issued = ctx.Properties.IssuedUtc ?? DateTimeOffset.UtcNow;
                    ctx.Properties.ExpiresUtc = issued.AddDays(14);
                    ctx.Properties.IsPersistent = true;
                },
                OnResponseSignedIn = ctx =>
                {
                    ctx.Properties.ExpiresUtc = ctx.OwinContext.Get<DateTimeOffset?>("auth-prop-expires");
                    ctx.Properties.IsPersistent = ctx.OwinContext.Get<bool>("auth-prop-persist");
                }
            }
        });

Make sure to set the same ExpireTimeSpan and SldingExpiration as your main Asp.Net Identity cookie to preserve those settings (since they get merged in the AuthenticationResponseGrant).

like image 114
Barry Hagan Avatar answered Sep 16 '22 15:09

Barry Hagan


This still appears to be an issue in Identity 2.2.1 (It may be fixed in Asp.Net Identity 3.0 - but that is currently pre-released and requires a later version of .Net framework that 4.5)

The following work around seems ok for now: The cookie is getting set on the SignInManager.TwoFactorSignInAsync with the wrong values, so on Success of the VerifyCode action, I reset the cookie to be persistent and give it the expiry date that I wish (in this case I set it to a year)

  public async Task<ActionResult> VerifyCode(VerifyCodeViewModel model)
  {
        if (!ModelState.IsValid)
        {
            return View(model);
        }            var result = await SignInManager.TwoFactorSignInAsync(model.Provider, model.Code, isPersistent:  model.RememberMe, rememberBrowser: model.RememberBrowser);
        switch (result)
        {
            case SignInStatus.Success:
                // if we remember the browser, we need to adjsut the expiry date as persisted above
                // Also set the expiry date for the .AspNet.ApplicationCookie 
                if (model.RememberBrowser)
                {
                    var user = await UserManager.FindByIdAsync(await SignInManager.GetVerifiedUserIdAsync());
                    var rememberBrowserIdentity = AuthenticationManager.CreateTwoFactorRememberBrowserIdentity(user.Id);
                    AuthenticationManager.SignIn(new AuthenticationProperties { IsPersistent = true, ExpiresUtc = DateTime.UtcNow.AddDays(365) }, rememberBrowserIdentity);
                }

                return RedirectToLocal(model.ReturnUrl);
like image 37
Peter Kerr Avatar answered Sep 18 '22 15:09

Peter Kerr