I'm using asp.net Forms Authentication to require users to log in when they visit a specific page. I want the users to have to log in again after 5 minutes of inactivity but no matter what I put in the Timeout value of the forms section in the web.config the user only gets kicked off after the session state expires.
One of the tests I've tried involves this configuration:
<system.web>
<sessionState timeout="1" />
<authentication mode="Forms">
<forms loginUrl="~/authentication" timeout="2" slidingExpiration="true"/>
</authentication>
</system.web>
If I log in and remain idle for a minute I am asked to log in again if I refresh the page. However, I was under the impression that I should be able to continue working until the forms authentication timeout expires. I understand that at the 1 minute mark it would be too late for the slidingExpiration setting to renew my cookie but I should still have another minute before the cookie actually expires.
If I remove the Sessiontate timeout section I'm not asked to log in after two minutes have passed. It takes a long time(probably 30 minutes) before I'm asked to log back in. This to me sounds like I'm only asked to log back in when the sessionState expires.
Am I missing something here?
Here's the basic layout of the controllers and methods involved. First, the user tries to go to the Recruiter page:
public class HireController : Controller
{
[Authorize]
public ActionResult Recruiter()
{
//do stuff after we've been authorized to access this page
}
}
Because the user needs to be authorized they are redirected to the login page in the authentication controller:
public class AuthenticationController : BaseAuthenticationController
{
private readonly IAuthenticationService AuthenticationService;
public AuthenticationController(IAuthenticationService authService)
: base(authService)
{
AuthenticationService = authService;
}
[AcceptVerbs(HttpVerbs.Get)]
public ActionResult Index(string returnUrl)
{
var special= false;
return View("~/Views/Login/Login.cshtml", new LoginModel(special) { ReturnUrl = returnUrl });
}
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Index(LoginCredentials credentials, string returnUrl)
{
try
{
if (!ModelState.IsValid)
{
throw new ApplicationException(GeneralError);
}
base.DoLogin(credentials.Username, credentials.Password);
}
catch (Exception ex)
{
string message = (ex is ApplicationException) ? ex.Message : GeneralError;
ModelState.AddModelError("", message);
return View("~/Views/Login/Login.cshtml", new LoginModel { Username = credentials.Username, ReturnUrl = returnUrl });
}
return RedirectToLocal(returnUrl);
}
private ActionResult RedirectToLocal(string returnUrl)
{
if (Url.IsLocalUrl(returnUrl))
{
return Redirect(returnUrl);
}
if (User.Identity != null && User.Identity.IsAuthenticated)
{
return RedirectToAction("Recruiter", "Hire");
}
return RedirectToAction("Recruiter", "Hire");
}
}
Here is the BaseAuthenticationController class:
public class BaseAuthenticationController : Controller
{
private readonly IAuthenticationService AuthenticationService;
protected const string GeneralError = "Login failure please try again";
public BaseAuthenticationController(IAuthenticationService authService)
{
AuthenticationService = authService;
}
public void DoLogin(string username, string password)
{
AuthenticationService.Login(username, password);
}
}
Here is the concrete IAuthenticationService class:
public class WebAuthenticationService : IAuthenticationService
{
private const string InvalidError = "Invalid User Credentials Please try again";
private const string LockoutError = "You have been locked out of the Hiring Center. You will receive an email shortly once your password has been reset.";
readonly string uri = ConfigurationManager.AppSettings["HiringLoginApiBaseUrl"];
private readonly ISecurityContext SecurityContext;
public WebAuthenticationService(ISecurityContext securityContext)
{
SecurityContext = securityContext;
}
private LoginResult GetUserLogin(string username, string password)
{
using (var httpClient = new HttpClient())
{
httpClient.BaseAddress = new Uri(uri);
var content = new FormUrlEncodedContent(new[]
{
new KeyValuePair<string, string>("username", username),
new KeyValuePair<string, string>("password", password)
});
var postResult = httpClient.PostAsync("/api/Login/Post", content).Result;
var loginResult = postResult.Content.ReadAsAsync<LoginResult>().Result;
return loginResult;
}
}
public MembershipProvider AuthenticationProvider
{
get { return Membership.Provider; }
}
public void Login(string userName, string password)
{
var loginResult = this.GetUserLogin(userName, password);
if (!loginResult.IsValid)
{
throw new ApplicationException(InvalidError);
}
if (loginResult.IsLockedOut)
{
throw new ApplicationException(LockoutError);
}
// Maintain the location
IUser current = SecurityContext.Current;
SecurityContext.SetCurrent(User.CreateAuthorized(userName, current.Location, current.Preferences));
FormsAuthentication.SetAuthCookie(userName, false);
}
}
I'm not too clear on what the point is of the following line is in the WebAuthenticationService class:
SecurityContext.SetCurrent(User.CreateAuthorized(userName, current.Location, current.Preferences));
the SetCurrent() method is defined as follows:
public class HttpSecurityContext : ISecurityContext
{
public static string SECURITY_CONTEXT_KEY = "SECURITY_CONTEXT";
public IUser Current
{
get
{
IUser user = HttpContext.Current.User as IUser;
if (user == null)
{
throw new ApplicationException("Context user is invalid;");
}
return user;
}
}
public void SetCurrent(IUser user)
{
HttpContext.Current.User = user;
}
}
Web.Config Membership Provider:
<membership>
<providers>
<clear />
<add name="AspNetSqlMembershipProvider" type="System.Web.Security.SqlMembershipProvider, System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=asdfasf" connectionStringName="mydb" enablePasswordRetrieval="false" enablePasswordReset="true" requiresQuestionAndAnswer="false" requiresUniqueEmail="false" passwordFormat="Hashed" maxInvalidPasswordAttempts="3" minRequiredPasswordLength="6" minRequiredNonalphanumericCharacters="0" passwordAttemptWindow="10" passwordStrengthRegularExpression="" applicationName="/" />
</providers>
</membership>
Login
, in which caseloginUrl="~/authentication/Login"
in the config for forms element. – SunilloginUrl="~/authentication"
setting fires off the Index method in the authentication controller which in turn takes the user to the Login.cshtml view after checking if the ModelState is valid. The login logic seems to work ok for the most part. The only thing that's off is the interval after which the user is logged off and is asked to re-enter their credentials. – BrunologinUrl="~/authentication/Login"
. I do not know what your app requirements are, but for redirecting a user to login page, there should not be any need to check ModelState. However, when you are calling the Login action with HttpPost i.e. when user submits his/her credentials, then you need to check ModelState. So I am guessing, you have not wired up the authentication part correctly. – Sunil