Introduction
As a Web developer working in a small company, I often encountered the same issue: every time I wanted to release a new version of a Web application, I had to call / send an e-mail to every lovely user in order to ask them to leave the application. The fact is that most of them weren't using it at the moment I was planning to launch the update. At the end, everyone was angry because of the time they lost answering my phone calls, especially those who were in the middle of an exciting Freecell game, or watching their friends' pictures on Facebook.
I decided to put an end to it by developing a small Web page that would tell me who is online. The principle is really simple: on the first load of every page in my WebSite, I check if the current user is authenticated. If this is the case, I check if he's already part of the authenticated users' list (a generic List of string objects). If he's not in the list, I add him into it. When the user navigates to another page, or closes his browser, I remove him from the authenticated users' list. This is done using a little bit of JavaScript with a call to a WebService.
Background
If you're not familiar with FormsAuthentication, you may want to take a look at this article:
This blog explains how to use the ASP.NET Cache and Session State storage:
Finally, this page from MSDN describes and shows a sample of the JavaScript "onbeforeunload
" event:
Using the Code
Your Web.config file should include the following sections:
<configuration>
<system.web>
-->
<authentication mode="Forms">
<forms loginUrl="~/Login.aspx" defaultUrl="~/Default.aspx" />
</authentication>
-->
<compilation debug="true">
<assemblies>
<add assembly="System.Web.Extensions, Version=1.0.61025.0,
Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
<add assembly="System.Design, Version=2.0.0.0, Culture=neutral,
PublicKeyToken=B03F5F7F11D50A3A" />
<add assembly="System.Web.Extensions.Design, Version=1.0.61025.0,
Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
</assemblies>
</compilation>
-->
<httpHandlers>
<remove verb="*" path="*.asmx"/>
<add verb="*" path="*.asmx" validate="false"
type="System.Web.Script.Services.ScriptHandlerFactory,
System.Web.Extensions, Version=1.0.61025.0, Culture=neutral,
PublicKeyToken=31bf3856ad364e35" />
<add verb="*" path="*_AppService.axd" validate="false"
type="System.Web.Script.Services.ScriptHandlerFactory,
System.Web.Extensions, Version=1.0.61025.0, Culture=neutral,
PublicKeyToken=31bf3856ad364e35" />
<add verb="GET,HEAD" path="ScriptResource.axd"
type="System.Web.Handlers.ScriptResourceHandler,
System.Web.Extensions, Version=1.0.61025.0, Culture=neutral,
PublicKeyToken=31bf3856ad364e35" validate="false" />
</httpHandlers>
-->
<httpModules>
<add name="ScriptModule" type="System.Web.Handlers.ScriptModule,
System.Web.Extensions, Version=1.0.61025.0, Culture=neutral,
PublicKeyToken=31bf3856ad364e35" />
</httpModules>
-->
<pages>
<controls>
<add tagPrefix="asp" namespace="System.Web.UI"
assembly="System.Web.Extensions, Version=1.0.61025.0,
Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
</controls>
</pages>
</system.web>
</configuration>
Login.aspx.cs
This is the code-behind for the login page. Nothing fancy here, just a few lines of code that check if the specified username and password are valid. This is the place to put your authentication logic, but keep in mind that the username is the one that will be displayed in the "authenticated users" page. So you probably don't want to use an id here. If you really can't use the username of the login page (for example, if this username is not relevant), you should store the "real" UserName in a Session
variable during your authentication process, then use something like Session["UserName"].ToString()
to retrieve it.
protected void Page_Load(object sender, EventArgs e)
{
AuthenticatedUsersLogin.Focus();
}
protected void AuthenticatedUsersLogin_Authenticate(object sender,
AuthenticateEventArgs e)
{
string userName = AuthenticatedUsersLogin.UserName;
string password = AuthenticatedUsersLogin.Password;
if (userName.Length > 0 && password.Length > 0)
FormsAuthentication.RedirectFromLoginPage(userName, true);
}
BasePage.aspx.cs
All your WebPages must inherit from this class (except for the login page). It stores the authenticated users name in a string List.
public class BasePage : Page
{
protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack && HttpContext.Current.Request.IsAuthenticated)
RegisterUser();
}
protected void RegisterUser()
{
List<string> authenticatedUsers = AuthenticatedUsersList;
if (!authenticatedUsers.Contains(UserName))
{
authenticatedUsers.Add(UserName);
AuthenticatedUsersList = authenticatedUsers;
}
}
protected List<string> AuthenticatedUsersList
{
get
{
if (Cache["AuthenticatedUsers"] == null)
Cache["AuthenticatedUsers"] = new List<string>();
return (List<string>)Cache["AuthenticatedUsers"];
}
set { Cache["AuthenticatedUsers"] = value; }
}
protected string UserName
{
get
{
return HttpContext.Current.Request.IsAuthenticated
? HttpContext.Current.User.Identity.Name
: string.Empty;
}
}
}
AuthenticatedUsersPage.aspx.cs
This is the page that displays the list of the authenticated users. You just need a label and you're done. Of course you should add some links to the other pages, otherwise you won't be able to go back to your WebSite !
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
if (!IsPostBack) DisplayAuthenticatedUsers();
}
private void DisplayAuthenticatedUsers()
{
List<string> authenticatedUsersList = AuthenticatedUsersList;
if (authenticatedUsersList.Count > 0)
{
foreach (string user in authenticatedUsersList)
{
lblAuthenticatedUsers.Text += string.Format("<li>{0}</li><br />", user);
}
}
else lblAuthenticatedUsers.Text = "No one is authenticated";
}
AuthenticatedUsersMasterPage.Master
Placing the following code in a MasterPage saves us from the pain of adding it in each of the WebSite's pages. Basically I first added a reference to my WebService between the <asp:ScriptReference>
tags. Then I added my JavaScript code that will be triggered during the window.onbeforeunload
event. This will call the WebService's UnregisterUser()
method, used to remove the current user from the authenticated users' list.
<form id="form1" runat="server">
<asp:ScriptManager ID="AuthenticatedUsersScriptManager" runat="server">
<Services>
<asp:ServiceReference Path="~/AuthenticatedUsersWebService.asmx" />
</Services>
</asp:ScriptManager>
<script type="text/javascript" language="javascript">
window.onbeforeunload = RemoveUser;
function RemoveUser()
{
result = AuthenticatedUsersWebService.UnregisterUser(OnComplete,
OnTimeOut, OnError);
}
function OnComplete(arg) { }
function OnTimeOut(arg) { }
function OnError(arg) { }
</script>
<asp:ContentPlaceHolder ID="ContentPlaceHolder1" runat="server" />
</form>
AuthenticatedUsersWebService.asmx
This is the final step: create a WebService that will be called from the JavaScript code, in order to remove the current user from the authenticated users' list. Don't forget to add the WebMethod(EnableSession = true)
attribute to the UnregisterUser()
method, otherwise it won't be able to reach the Session
object.
[WebService(Namespace = "http://tempuri.org/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
[ScriptService()]
public class AuthenticatedUsersWebService : WebService
{
private Cache cache = HttpContext.Current.Cache;
[WebMethod(EnableSession = true)]
public void UnregisterUser()
{
string userName = Session["UserName"] != null ?
Session["UserName"].ToString() : string.Empty;
List<string> authenticatedUsers =
cache["AuthenticatedUsers"] != null
? (List<string> )cache["AuthenticatedUsers"]
: new List<string>();
if (authenticatedUsers.Contains(userName))
authenticatedUsers.Remove(userName);
cache["AuthenticatedUsers"] = authenticatedUsers;
}
}
History
- [02.09.2009] First version
- [02.19.2009] Fixed typo in
AuthenticatedUsersMasterPage.Master
- [02.25.2009] Fixed a bug in the authorization logic