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

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?

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).

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);

What you can do is assign your own CookieManager class that modifies the expiration time of the TwoFactorRememberBrowserCookie. This seems better than modifing the cookie in Application_PostAuthenticateRequest.
This works around the problem that you can either persist all or none of the authentication cookies.
Put this in your ConfigureAuth, the last line sets your custom cookie manager.
public void ConfigureAuth(IAppBuilder app)
{
// left out all but the modified initialization of the TwoFactorRememberBrowserCookie
var CookiePrefix = ".AspNet.";
app.UseCookieAuthentication(new CookieAuthenticationOptions {
AuthenticationType = DefaultAuthenticationTypes.TwoFactorRememberBrowserCookie,
AuthenticationMode = Microsoft.Owin.Security.AuthenticationMode.Passive,
CookieName = CookiePrefix + DefaultAuthenticationTypes.TwoFactorRememberBrowserCookie,
ExpireTimeSpan = TimeSpan.FromDays(14),
CookieManager = new TwoFactorRememberBrowserCookieManager()
});
}
Use this CookieManager class only for the TwoFactorRememberBrowserCookie.
When you do not persist cookies in TwoFactorSignInAsync, unfortunately the ExpirationTimeout is ignored.
So just set it again in the CookieManager (This is a modified version of the cookie manager coming from Microsoft.Owin.Infrastructure.CookieManager):
public class TwoFactorRememberBrowserCookieManager : Microsoft.Owin.Infrastructure.ICookieManager
{
string CookiePrefix = ".AspNet.";
Microsoft.Owin.Infrastructure.ICookieManager cm = new Microsoft.Owin.Infrastructure.ChunkingCookieManager();
public void AppendResponseCookie(IOwinContext context, string key, string value, CookieOptions options)
{
if (key == CookiePrefix + DefaultAuthenticationTypes.TwoFactorRememberBrowserCookie) {
options.Expires = DateTime.UtcNow.AddDays(14);
}
cm.AppendResponseCookie(context, key, value, options);
}
public void DeleteCookie(IOwinContext context, string key, CookieOptions options)
{
cm.DeleteCookie(context, key, options);
}
public string GetRequestCookie(IOwinContext context, string key)
{
return cm.GetRequestCookie(context, key);
}
}
This is what you will get:
Works for me that way.

Related

Redirect users to login page after session timeout using OpenIdConnectAuthentication

I'm using Azure B2C in an ASP.NET application with OpenIdConnectAuthentication. My sign-in policy has an absolute session length of 90 minutes.
After the session expires, I'd like the user to be automatically redirected to the login page, so they know they've been logged out. This doesn't seem to happen automatically. I was hoping this could be done automatically, perhaps using OWIN.
For now, as a workaround, I set a Refresh header in all HTTP responses that uses the remaining session life (calculated based on the iat claim and current time). But I was hoping this could just be handled automatically from the server. Is there any way to make the redirect occur automatically from the server-side once the session expires?
Below is my Startup class in Startup.Auth.cs:
public void ConfigureAuth(IAppBuilder app)
{
// ... other code ... //
app.UseOpenIdConnectAuthentication(CreateOptionsFromPolicy(SignInPolicyId));
}
private OpenIdConnectAuthenticationOptions CreateOptionsFromPolicy(string policy) {
var options = new OpenIdConnectAuthenticationOptions {
// For each policy, give OWIN the policy-specific metadata address, and
// set the authentication type to the id of the policy
MetadataAddress = String.Format(aadInstance, tenant, policy),
AuthenticationType = policy,
UseTokenLifetime = true,
// These are standard OpenID Connect parameters, with values pulled from
// Web.config
ClientId = clientId,
RedirectUri = redirectUri,
PostLogoutRedirectUri = logoutUri,
Notifications =
new OpenIdConnectAuthenticationNotifications {
AuthenticationFailed = AuthenticationFailed,
RedirectToIdentityProvider =
(context) => {
var value = context.OwinContext.Request.Path.Value;
if (value != "/default.aspx") {
context.OwinContext.Response.Redirect("/");
context.HandleResponse();
}
return Task.FromResult(0);
}
},
Scope = "openid",
ResponseType = "id_token",
TokenValidationParameters =
new TokenValidationParameters {
NameClaimType = "name",
},
};
return options;
}

User unauthorized after Azure AD login to different application simultaneously

I have two MVC applications AppA and AppB, and implemented Azure AD authentication for login.
I am able to sign-in successfully to both applications.
But the issue is, after I login to AppA and then to AppB, after sometime when I return back to AppA I am facing the issue where user has been logged out, and it again redirects to login screen (in AppA).
After I login to AppA (second time) and go back to AppB (user in AppB is logged out).
Client IDs are different ; TenandID is same. Both apps are hosted in same server.
Startup file:
public void ConfigureAuth(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = CookieAuthenticationDefaults.AuthenticationType,
SlidingExpiration = true,
Provider = new CookieAuthenticationProvider
{
OnResponseSignIn = context =>
{
context.Properties.AllowRefresh = true;
context.Properties.ExpiresUtc = DateTimeOffset.UtcNow.AddDays(1);
},
OnValidateIdentity = MyCookieValidateIdentity
},
ExpireTimeSpan = TimeSpan.FromDays(2)
});
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = appId,
//CookieManager=new SameSiteCookieManager(new SystemWebCookieManager()),
Authority = "https://login.microsoftonline.com/xxxxxx/v2.0",
Scope = $"openid email profile offline_access {graphScopes}",
RedirectUri = redirectUri,
PostLogoutRedirectUri = redirectUri,
TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
},
Notifications = new OpenIdConnectAuthenticationNotifications
{
RedirectToIdentityProvider = (context) =>
{
context.ProtocolMessage.DomainHint = "xyz.com";
return Task.FromResult(0);
},
// SecurityTokenValidated = OnSecurityTokenValidated,
AuthenticationFailed = OnAuthenticationFailedAsync,
AuthorizationCodeReceived = OnAuthorizationCodeReceivedAsync
}
}
);
}
actionContext.RequestContext.Principal.Identity.IsAuthenticated is returning False
I am assuming it has to do something with the cookie. Can someone please help resolve this ?
Edit:
Debugged further and found:
Initially if the cookies for AppA are set as:
.AspNet.Cookies = A_abc123 ; ASP.NET_SessionId = A_def456
And for AppB .AspNet.Cookies = B_mno123 ; ASP.NET_SessionId = B_pqr456
Then after I click any link in AppA, the cookie's values are updated with AppB's cookies, i.e. .AspNet.Cookies = B_mno123 ; ASP.NET_SessionId = B_pqr456
.AspNet.Cookies ASP.NET_SessionId
AppA A_abc123 A_def456
AppB B_mno123 B_pqr456
AppA B_mno123 B_pqr456
One thing that you need to do is to configure the Data Protection API so that both services uses the same cookie protection key. Out of the box each service creates its own unique key, and a cookie from one service is not valid in a different service.
I also did a blog post about the data protection API here.
See
How to: Use Data Protection
Get started with the Data Protection APIs in ASP.NET Core
public void ConfigureAuth(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
//AuthenticationType = CookieAuthenticationDefaults.AuthenticationType,// DefaultAuthenticationTypes.ApplicationCookie,
CookieName = ".AspNet.AppA.Cookies",
SlidingExpiration = true,
CookieManager = new SystemWebCookieManager(),
Provider = new CookieAuthenticationProvider
{
OnResponseSignIn = context =>
{
context.Properties.AllowRefresh = true;
context.Properties.ExpiresUtc = DateTimeOffset.UtcNow.AddDays(1);
},
},
ExpireTimeSpan = TimeSpan.FromDays(2)
});
//... code removed for brevity //
}
The Default Cookie Name set by the application was: .AspNet.Cookies
And when I modified the default cookie name, the issue got resolved. Each application was generating its own cookiename and hence the other application was not signing out the user.

ASP.NET MVC 5 OWIN Cookie Expiration

I am using OWIN middleware in my web app to manage logins/claims and issuing a cookie. The initial requirement was that the user should automatically be logged out when the browser was closed. That was easy enough, and accomplished by setting AuthenticationProperties.IsPersistent = false.
A new requirement came in that not only should the user be automatically logged out upon browser close, but they should also timeout after one hour of inactivity. I updated my ConfigureAuth method within my Startup class to provide an ExpireTimeSpan and set SlidingExpiration = true. To make those changes work, I then set AuthenticationProperties.IsPersistent = true.
Those changes made the user timeout after an hour, but now, since the cookie is persistent, the user is no longer automatically logged out upon browser close. Is there anyway to get both of these functionalities? I want my cake and eat it too!
Code used for reference:
public partial class Startup
{
public void ConfigureAuth(IAppBuilder app)
{
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
ExpireTimeSpan = TimeSpan.FromHours(1),
SlidingExpiration = true,
LoginPath = new PathString("/Consent/Index"),
ReturnUrlParameter = "returnUrl"
});
}
}
and
private void UpdateClaims(User user)
{
var claims = new List<Claim>();
claims.Add(new Claim(BlahBlahBlah));
claims.Add(new Claim(MoreBlahBlah));
var identity = new ClaimsIdentity(claims, DefaultAuthenticationTypes.ApplicationCookie, ClaimTypes.Name, ClaimTypes.Role);
Authentication.SignIn(new AuthenticationProperties
{
IsPersistent = true
}, identity);
}

How to Release a change that renames an User Role name

We're working on changes to an ASP.NET MVC app.
We're using Owin and OAuth2 to manage User permissions, but are managing the User DB object ourselves.
We have these on App Startup:
app.UseKentorOwinCookieSaver();
app.UseCookieAuthentication(GetCookieAuthenticationOptions(AuthenticationType))
.UseOpenIdConnectAuthentication(GetOpenIdConnectOptions(AuthenticationType));
And we manually assign Claims to users when they log in Role is an enum:
claimsIdentity.AddClaim(new Claim(ClaimTypes.Role, user.Role.ToString()));
If more detail is needed, the auth code is included at the end.
All of this is has been working fine, but we need to rename a role.
The code rename is trivial, and it all works just fine when I log in after the role is renamed. But if I'm already logged in, when the code changes, then my old role Claim string is still in my Auth Cookie, and is no longer recognised by the Auth code.
Becuase I'm already logged in, it doesn't take me to the LogIn page - it just shows me the "Forbidden" error page (As though I'd entered a link to a page I shouldn't have visited)
And because our Auth works by checking whether you have "Role 'x' or any Role greater than 'x'", thus we get Forbidden on every page (because now the user doesn't have any Role and thus fails every Auth test, because their Role isn't recognised as passing any test.
As a result the user has no way to log out.
As a developer, I can wipe my browser cookies and log in from scratch (at which point it works just fine) but a normal user (probably?) won't be able to do that.
My first thought was do somehting like this: http://www.britishdeveloper.co.uk/2010/09/force-client-refresh-browser-cache.html, to all users to log out and get them to log in again, once after the release.
Unfortunately, since EVERY page will fail, I've got nowhere to put that code that will run for the relevant users :(
I could hack around with the Authentication Code so that it knows about the old Roles and grants that Claim permission, but that seem hideous.
Another option would be to modify the Authorisation code so that it logged users out if they don't have any recognised Roles, but that doesn't really feel right either, for some reason I can't put my finger on.
Any suggestions or opinions about the right way to release such a change?
=-=-=-=-=-=-=-=-=-=
Auth code:
private const string AuthenticationType = "FrontEnd" + CookieAuthenticationDefaults.AuthenticationType;
private const string IdTokenClaimName = "id_token";
public void Configuration(IAppBuilder app)
{
app.UseKentorOwinCookieSaver();
app.UseCookieAuthentication(GetCookieAuthenticationOptions(AuthenticationType))
.UseOpenIdConnectAuthentication(GetOpenIdConnectOptions(AuthenticationType));
}
private static CookieAuthenticationOptions GetCookieAuthenticationOptions(string authenticationType)
{
return new CookieAuthenticationOptions
{
AuthenticationType = authenticationType,
};
}
private OpenIdConnectAuthenticationOptions GetOpenIdConnectOptions(string authenticationType)
{
return new OpenIdConnectAuthenticationOptions
{
Authority = AuthenticationConstants.AuthenticationAuthority,
ClientId = AuthenticationConstants.ClientId,
RedirectUri = AuthenticationConstants.ClientRedirectUrl,
ResponseType = "id_token",
Scope = "openid profile email",
SignInAsAuthenticationType = authenticationType,
UseTokenLifetime = false,
Notifications = new OpenIdConnectAuthenticationNotifications
{
SecurityTokenValidated = n => Task.Run(() => AuthorizeIfUserExists(n)),
RedirectToIdentityProvider = n => Task.Run(() => SendIdTokenToLogout(n))
}
};
}
private static void SendIdTokenToLogout(RedirectToIdentityProviderNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> n)
{
if (n.ProtocolMessage.RequestType == OpenIdConnectRequestType.LogoutRequest)
{
var idTokenHint = n.OwinContext.Authentication.User.FindFirst(IdTokenClaimName).Value;
n.ProtocolMessage.IdTokenHint = idTokenHint;
}
}
private void AuthorizeIfUserExists(SecurityTokenValidatedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> authContext)
{
var identity = authContext.AuthenticationTicket.Identity;
var userIdClaim = GetUserIdClaim(identity);
var emailClaim = GetEmailClaim(identity);
var claimsIdentity = new ClaimsIdentity(
identity.AuthenticationType,
ClaimTypes.Name,
ClaimTypes.Role);
claimsIdentity.AddClaim(new Claim(IdTokenClaimName, authContext.ProtocolMessage.IdToken));
claimsIdentity.AddClaim(userIdClaim);
claimsIdentity.AddClaim(emailClaim);
using (var context = new DbDataContext())
{
var user = GetAndInitializeUserIfNecessary(context, userIdClaim.Value, emailClaim.Value);
// We add role and name claims to all successful logins that are also registered in our database.
if (user != null && !user.IsSoftDeleted)
{
claimsIdentity.AddClaim(new Claim(ClaimTypes.Role, user.Role.ToString()));
claimsIdentity.AddClaim(new Claim(ClaimTypes.Name, String.Format("{0} {1}", user.FirstName, user.Surname)));
}
}
authContext.AuthenticationTicket = new AuthenticationTicket(
claimsIdentity,
authContext.AuthenticationTicket.Properties);
}
I could hack around with the Authentication Code so that it knows about the old Roles and grants that Claim permission, but that seem hideous.
That seems best to me.
You have made a change which breaks backwards compatibility for users with active sessions. The usual approach for zero-downtime in that general case is to release code which supports both old and new clients, until you are sure that there are no old clients remaining, then delete the legacy code.

Session timeout increases tremendously when the user picks to stay in the system by clicking a button (asp.net mvc 5.0 using OWIN)

I am trying to implement session timeout popup. I have made a fair progress. Everything seems to work fine except when the user clicks the "Stay in the system" button in the popup window. What this button does is to make an ajax call to the following Action in the Account Controller:
public async Task<ActionResult> RefreshTheUserSession(string UserId)//this will be called from the ajax timeout
{
var myuser = await UserManager.FindByIdAsync(UserId);
if (myuser != null)
{
await SignInManager.SignInAsync(myuser, true, true);
}
return null;
}
This could works fine and keeps the user in the system. However, the new session timeout timespan becomes around 1209596. This value is initially set to 2 minutes for testing purposes as shown in the following (Startup.Auth.cs).
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Home/Index"),
ExpireTimeSpan = TimeSpan.FromMinutes(2),
Provider = new CookieAuthenticationProvider
{
OnValidateIdentity = MyCustomValidateIdentity
}
});
And, actually, when the user logins the system for the first time, the ExpireTimeSpan is set to 2 mins correctly. However, when the time comes for the user to click to stay in the system, the session timeout timespan gets crazy.
Here is the login code just in case:
public async Task<ActionResult> Login(LoginViewModel model, string returnUrl)
{
if (!ModelState.IsValid)
{
return View(model);
}
var result = await SignInManager.PasswordSignInAsync(model.Username, model.Password, model.RememberMe, shouldLockout: false);
switch (result)
{
case SignInStatus.Success:
return RedirectToLocal(returnUrl);
case SignInStatus.LockedOut:
return View("Lockout");
}
}
Later, I have added this in the Web.Config, but did not help. (and not sure it would help since I am using OWIN)
<sessionState mode="InProc" timeout="20" />
Does anyone have any idea why this is happening? Why the session timeout value gets crazy when I try to (re)-sign in the user?
1209596 seconds is approximately 14 days, or 2 weeks. What you're doing here is setting the "remember me" flag for the user and that's exactly what it does. It sets the timeout on the user's auth cookie to 2 weeks, so the user is not required to login again for 2 weeks.
If that's not what you want, then change the action to:
public async Task<ActionResult> RefreshTheUserSession(string UserId)//this will be called from the ajax timeout
{
var myuser = await UserManager.FindByIdAsync(UserId);
if (myuser != null)
{
await SignInManager.SignInAsync(myuser);
}
return null;
}
In other words, don't pass true for the isPersistent or rememberBrowser params. You may also need to call AuthenticationManager.SignOut(), first, as it may not let you sign in an already signed in user. With the code you have now, you're technically updating the authentication by making it persistent, requiring a new cookie to be sent, so that's a little different.

Resources