ASP.NET Form 驗證靠 .ASPXAUTH Cookie 識別身分,它是一段用 Machine Key 加密並簽名的內容,包含身份識別、有效期限等資訊。這段機制在 WebForm 與 ASP.NET MVC 是共用的,因此設定相同的 Machine Key,ASP.NET WebForm 與 MVC 也可共用登入身分。基於這個原理,Machine Key 的保管便格外重要,一旦外流,駭客便可輕易冒充任何登入者

以上是我對 ASPXAUTH 的全部理解。今天偵察問題時,發現 ASPXAUTH 有個重設行為,是我以前沒注意到的。在某些情況下,ASP.NET 會重設 .ASPXAUTH Cookie,如下圖,Request 已有 .ASPXAUTH Cookie 9AD515A...,ASPX 回應包含 Set-Cookie 將 .ASPXAUTH 改為 C8B7DE71...,之後瀏覽器將改用 C8B7DE71...:

我懷疑這是 FormsAuthentication SlidingExpiration 的行為,當使用者 SlidingExpiration 時間一半到逾期前這段時間存取網站,有效期限將自動延長,而延長期限的動作便是透過更新 .ASPXAUTH Cookie 完成。參考來源 Sliding expiration resets the expiration time for a valid authentication cookie if a request is made and more than half of the timeout interval has elapsed. )

於是我設計了一個實驗,以便深入觀察 .ASPXAUTH 的運作。

借用之前的程式範例,我寫了一支 ChkCookie.aspx,用以顯示登入帳號、.ASPXAUTH Cookie 及解密還原出的 FormsAuthenticationTicket 物件(建立時間及有效期限),另外再顯示目前時間及 FormsAuthenticationTicket 的壽命方便對照。按下 Reload 時會帶入?t=FormsAuthenticationTicket建立秒數 以利識別:

<%@ Page Language="C#" %>
<%@ Import Namespace="System.Web.Security" %>
<script runat="Server">
	protected string Info { get; set; }
	protected int Age { get; set; }
	void Page_Load(object sender, EventArgs e)
	{
		var req = Request;
		var resp = Response;
		var sb = new StringBuilder();
		if (req.IsAuthenticated)
			sb.AppendLine("User=" + User.Identity.Name);
		else
			sb.AppendLine("Not Authenticated");
		foreach(string key in req.Cookies.AllKeys)
		{
			var val = req.Cookies[key].Value;
			val = val.Substring(0, Math.Min(20, val.Length));
			sb.AppendLine("* " + key + "=" + val);
		}
		var authCookie = req.Cookies[FormsAuthentication.FormsCookieName];
		if (authCookie == null)
			sb.AppendLine("No cookie - " + FormsAuthentication.FormsCookieName);
		else {
			var authTicket = FormsAuthentication.Decrypt(authCookie.Value);
			sb.AppendLine("Ticket.Expired=" + authTicket.Expired);
			sb.AppendLine("Ticket.IssueDate=" + authTicket.IssueDate.ToString("HH:mm:ss"));
			sb.AppendLine("Ticket.Expiration=" + authTicket.Expiration.ToString("HH:mm:ss"));
			Age = Convert.ToInt32((DateTime.Now - authTicket.IssueDate).TotalSeconds);
		}
		Info = sb.ToString();
	}
</script>
<!DOCTYPE html>
<html>

<head></head>

<body>
	<div>
		<button onclick="location.href=location.href.split('?')[0]+'?t='+cnt">Reload</button>
		<span id=t></span>
	</div>
	<pre><%=Info%></pre>
	<script>
		var cnt = <%=Age%>;
		setInterval(function () {
			document.getElementById('t').innerText = 
				new Date().toTimeString().split(' ')[0] + ' (' + cnt + 's)';
			cnt++;
		}, 1000);
	</script>
</body>

</html>

FormsAuthenticationTicket 時效可由程式或 web.conf forms timeout 設定,單位為分鐘:

<authentication mode="Forms">
  <forms loginUrl="Login.aspx" timeout="1" />
</authentication>

經過一連串實驗,我觀察到幾件事:

  1. timeout 會決定 FormsAuthenticationTicket 的有效期限,例如:timeout="1" 時,IssueDate 與 Exipration 就相隔一分鐘:
  2. 在一分鐘內多次 Reload 並觀察 Cookie 異動狀況,FormsAuthenticationTicket 壽命超過一半(30-60 秒)後存取,ASP.NET 會重設 .ASPXAUTH。如下展示,27、29 秒時只有要求 Cookie,30 秒那次回應更新 .ASPXAUTH,從 32 起改用新 Cookie (40AD63...)
  3. 若 FormsAuthenticationTicket 壽命一分鐘,30-60 間要存取才有延壽效果。如下圖,13、17、26 秒有重新整理,但 30-60 間無活動,67 秒再存取時登入身分便失效。另外,已失效的 .ASPXAUTH Cookie 會被 ASP.NET 剔除,即使瀏覽器有送出 Cookie,也不會出現在 Request.Cookies:
  4. 設定 <forms loginUrl="Login.aspx" timeout="1" slidingExpiration="false" /> 後,30-60 秒的存取就不會有 Set-Cookie 動作:

透過以上實驗,我們對 .ASPXAUTH 的行為就有更深一層的認識囉。

In this series of experiments, we look at the behavior of .ASPXAUTH cooke expiration and auto-renewal.


Comments

# by Huang

這個SlidingExpiration設為一半的時間有點trick,user會抱怨說,明明timeout一小時,我在1小時登入前,直到31分鐘前都還有在使用,為什麼現在過期了XD

# by Jeffrey

to Huang,我也覺得 SlidingExpiration 過半後存取才展延的概念不符一般使用者的預期,公告時自動把時間減半是個好點子。例如 Timeout 設一小時,對外宣稱 30 分鐘,29 分最後一次使用,Idle 31 分鐘被踢出,聽起來就合理了。

Post a comment