Bruno
Bruno

Reputation: 563

Forms Authentication Timeout Being Ignored

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>

Upvotes: 2

Views: 4266

Answers (3)

Bruno
Bruno

Reputation: 563

I found the cause of all my problems. I found a bunch of code in global.asax that manipulates the user's session and essentially overrides forms authentication. This code runs on every request to the server and keeps the user logged in as long as they are still authenticated in the session. This means that even if the forms auth cookie expired(which it did!) the user would remain logged in. I'm guessing the previous developers started out with forms auth and then decided to write their own thing for some reason. We decided to change the session timeout so that users would be logged out after 5 minutes instead of the default 20.

Here is some of the code from global.asax that is responsible for me almost going bald:

 protected void Application_PreRequestHandlerExecute()
    {
        HttpSessionState session = HttpContext.Current.Session;
        if (session == null)
            return;

        IUser user = (session[HttpSecurityContext.SECURITY_CONTEXT_KEY] as IUser) ?? CreateUser();
        securityContext.SetCurrent(user);
    }

    protected void Application_PostRequestHandlerExecute()
    {
        HttpSessionState session = HttpContext.Current.Session;
        if (session == null) return;

        session[HttpSecurityContext.SECURITY_CONTEXT_KEY] = securityContext.Current;
    }

 private IUser CreateUser()
    {
        IUserLocation location = LocateUser();
        IUser user = Common.Security.User.CreateAnonymous(location);
        SetupUserPreferences(user);
        return user;
    }

And this is what we changed in web.config for the session timeout:

<system.web>
    <sessionState timeout="5"/>
</system.web>

Upvotes: 2

Lesmian
Lesmian

Reputation: 3952

You just use wrong parameter value for FormsAuthentication.SetAuthCookie method. As per documentation https://msdn.microsoft.com/pl-pl/library/twk5762b(v=vs.110).aspx second parameter sets persistent cookie if set to true. Otherwise cookie is not persisted and lost on session timeout. So if you want to preserve authentication cookie through different sessions (after session timeout) then use this:

FormsAuthentication.SetAuthCookie(userName, true);

However keep in mind that after session timeout user will lost all his session variables and that may cause errors in your web app.

Upvotes: 2

Bruno
Bruno

Reputation: 563

I had to add the following to the web.config file to prevent unauthorized users from accessing the page:

<authorization>
    <deny users="?" />
</authorization>

Apparently this is pretty standard procedure and I'm surprised I didn't run into it much earlier in my search. This msdn article actually mentions adding that section.

Upvotes: 0

Related Questions