Setting Up a Sitecore Extranet

December 26, 2011

*(updated on 5/30/2014) For those interested, I have released an Extranet Module to manage creating and removing an extranet for your site. 

I've recently needed to setup an extranet for several websites and began researching. There are several helpful articles on the topic. I found bolaky.net and blog.wojciech.org as well as using Sitecore's Security API and Security Administrator cookbooks. Although there was enough information to get me started, what I found was disjointed and there were a few specifics that weren't detailed. After some testing I was able to create a functioning extranet and in an effort to create a more comprehensive article I have detailed my results.

1. Create Extranet Sublayouts

The first thing you'll want to do is to create several sublayouts that will be required by the extranet. You can use what suits you for your project but I found these to be useful: login, register user, edit account, forgot password and account navigation. It's up to you whether or not you'll create separate templates for each of these but you'll need to apply them to different pages in your site. The login, register user, edit account and forgot password sublayouts will need to have their own individual pages and urls but the navigation will most likely be added to a global page for accessibility. This will serve your users as a method of logging out (or in, if you choose), seeing their status and accessing their account. I've added the code for each of the sublayouts below. I didn't go so far as to put any form validation or spam protection as I'm sure you will customize that yourself.

Login

The login page is going to be the main gateway for your extranet. This code also has a hardcoded link to the signup and forgot password pages. It would be best for you to manage these urls in content or at least update these urls to match the locations you put those pages in. Also you're going to want to manage the value of the role for each individual site since here it's hardcoded to "Extranet Users" to match the example. Here's the .aspx code:

<div>
    <div>
        <asp:Literal ID="ltlMessage" runat="server" />
    </div>
    <div>
	<span>Username:</span>
	<asp:TextBox ID="txtUser" runat="server" /><br/>
	<span>Password:</span>
	<asp:TextBox ID="txtPass" TextMode="Password" runat="server"  /><br/>
	<asp:Button ID="btnLogin" Text="Submit"  runat="server"/>
    </div>
    
    <div>
        <a href="/extranet/forgot-password.aspx">Forgot Password</a> | 
	<a href="/extranet/register.aspx">Signup</a>
    </div>
</div>

And of course the code behind:

using System;
using System.Collections;
using System.Configuration;
using System.Data;
using System.Linq;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.HtmlControls;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Xml.Linq;
using Sitecore.Security.Authentication;
using Sitecore.Security.Accounts;
using Sitecore.Security;
using System.Collections.Specialized;

namespace YourNamespace.Extranet
{
	public partial class Login : System.Web.UI.UserControl
	{
		protected string returnURL;
		
		protected void Page_Load(object sender, EventArgs e) {
			
			//set the return url
			if (Request.QueryString.HasKey("returnUrl") && !Request.QueryString.HasKey("returnUrl").Equals("")) {
				returnURL = Request.QueryString["returnUrl"];
			}
			
			//if you're logged in and you've got permissions to this site then redirect to home
			if (Sitecore.Context.IsLoggedIn && Sitecore.Context.User.Roles.Where(a => a.Name.Contains("Extranet Users")).Any()) {
				if (string.IsNullOrEmpty(returnURL)) {
					Sitecore.Web.WebUtil.Redirect("/"); 
				} else {
					Sitecore.Web.WebUtil.Redirect(returnURL);
				}
			}

			//if you've been redirected from an activation then show messaging
			if (Request.QueryString.HasKey("activated") && !Request.QueryString.HasKey("activated").Equals("true")) {
				//show a message explaining the user what happened.
				ltlMessage.Text = "you've activated your account!";
			}
		}
		
		protected void btnLogin_Click(object sender, EventArgs e) {
			if (Page.IsValid) {
				try {
					Sitecore.Security.Domains.Domain domain = Sitecore.Context.Domain;
					string domainUser = domain.Name + @"" + txtUser.Text;
					if (Sitecore.Security.Authentication.AuthenticationManager.Login(domainUser, txtPass.Text, false)) {
						if (!string.IsNullOrEmpty(returnURL)) {
							Sitecore.Web.WebUtil.Redirect(returnURL);
						} else {
							Sitecore.Web.WebUtil.Redirect("/");
						}
					} else {
						//throw new System.Security.Authentication.AuthenticationException("Invalid username or password.");
						ltlMessage.Text = "Invalid username or password.";
					}
				} catch (System.Security.Authentication.AuthenticationException) {
					ltlMessage.Text = "Processing error. Please try again later.";
				}
			}
		}
	}
}

I'm also using an extension method "HasKey" for the Request.QueryString NameValueCollection here and on a few other pages:

public static class NameValueCollectionExtensions {
	public static bool HasKey(this NameValueCollection QString, string Key) {

	foreach (string key in QString.Keys) {
		if (key.Equals(Key)) {
			return true;
		}
	}

	return false;
}

Registration

The registration page will most likely require customization. You may need to customize the field inputs to your needs since what I provide here is very basic. Consider what your user looks like and what you'll need to be storing. There is more information about customizing the user and creating a class to handle these additional fields in the Security Cookbook chapter 3.5. There is also another hardcoded link to the login page that you should update also. Here's the .aspx code:

<div>
	<asp:PlaceHolder ID="phChoose" runat="server">
		<asp:LinkButton ID="lnkPass"  runat="server">
			Change Password
		</asp:LinkButton>
		<br /><br />
		<asp:LinkButton ID="lnkEmail"  runat="server">
			Change Email
		</asp:LinkButton>
	</asp:PlaceHolder>
	<asp:Placeholder ID="phPass" Visible="false" runat="server"> 
		<h3>Change Password</h3>
		<div class="required">
			<asp:Literal ID="ltlMessagePass" runat="server" />
		</div>
		<div>
			<label>Old Password</label>
			<asp:TextBox ID="txtPassOld" TextMode="Password" runat="server" />
			<label>New Password</label>
			<asp:TextBox ID="txtPassNew" TextMode="Password" runat="server" />
			<label>New Password Confirm</label>
			<asp:TextBox ID="txtPassConfirm" TextMode="Password" runat="server" />
			<asp:Button id="btnEditPass"  text="Change Password"/>
		</div>
	</asp:Placeholder>
	<asp:Placeholder ID="phEmail" Visible="false" runat="server"> 
		<h3Change Email</h3>
		<div class="required">
			<asp:Literal ID="ltlMessageEmail" runat="server" />
		</div>
		<div>
			<label>Old Email</label>
			<asp:TextBox ID="txtEmailAddressOld" runat="server"/>
			<label>New Email</label>
			<asp:TextBox ID="txtEmailAddressNew" runat="server" />
			<label>New Email Confirm</label>
			<asp:TextBox ID="txtEmailAddressConfirm" runat="server" />
			<asp:Button id=""  text="Change Email"/>
		</div>
	</asp:Placeholder>
</div>

Again, like in the login page, you'll have to change the hardcoded "Extranet Users" role that is being used to something managed. I am also sending an email to the user with a registration code. The user will be created but will not be able to view the hidden pages until the role has been added to their user. This only occurs when the user verifies they are who they say they are by clicking on the link in the email. Also note that to store the email property to the user profile requires you to call the save method. We are doing a simple validation to make sure the email and password match their confirmation fields. After the user is logged in I then redirect the user to a hardcoded url. This url should be updated to match your url values. And the code behind:

using System;
using System.Collections;
using System.Configuration;
using System.Data;
using System.Linq;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.HtmlControls;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Xml.Linq;
using Sitecore.Security.Accounts;
using System.Collections.Generic;
using Sitecore.Data.Items;

namespace YourNamespace.Extranet
{
	public partial class Registration : System.Web.UI.UserControl {
	protected void Page_Load(object sender, EventArgs e) {
		//handle case where the id exists from a registration link
		if (Request.QueryString.HasKey("code")) {
			Guid userKey = new Guid(Request.QueryString.Get("code"));
			if (ExtranetSecurity.RegisterUser(userKey)) {
				Sitecore.Web.WebUtil.Redirect(HttpContext.Current.Request.Url.Host + "/extranet/login.aspx?activated=true");
			}
		}

		//if you're logged in and you've got permissions to this site then redirect to home
		if (Sitecore.Context.IsLoggedIn && Sitecore.Context.User.Roles.Where(a => a.Name.Contains("Extranet Users")).Any()) {
			Sitecore.Web.WebUtil.Redirect("/");
		}
	}

	protected void btnRegister_Click(object sender, EventArgs e) {
		if (Page.IsValid &&
			txtPass.Text.Equals(txtConfirmPass.Text) &&
			txtEmailAddress.Text.Equals(txtConfirmEmailAddress.Text)) {
			//create user
			string domainUser = Sitecore.Context.Domain.GetFullName(txtUser.Text);
			if (System.Web.Security.Membership.GetUser(domainUser) == null && Sitecore.Security.Accounts.User.Exists(domainUser)) {
				try {
					//create user
					User u = Sitecore.Security.Accounts.User.Create(domainUser, txtPass.Text);
					if (u == null) {
						ltlMessage.Text = "null user";
					} else {
						using (new Sitecore.SecurityModel.SecurityStateSwitcher(Sitecore.SecurityModel.SecurityState.Disabled)) {
							//add this user to the site role
							List roles = Sitecore.Context.Domain.GetRoles().Where(a => a.Name.Contains("Extranet Users")).ToList();
							if (roles.Any()) {
								//could also loop through them all if there are multiple
								//need to make sure there is a convention for knowing which to add.
								u.Roles.Add(roles.First());
							}
							//u.Profile.FullName = "some name";
							//u.Profile.Comment = "some comment";
							u.Profile.Email = txtEmailAddress.Text;
							u.Profile.Save();

							HttpRequest req = HttpContext.Current.Request;
							string body = "Hi " + txtUser.Text + ",
 Thanks for registering. " + req.Url.Host + "
 Your new password is: : " + password);
							body += "Click on the link to register and login: http://" + req.Url.Host + "/extranet/register.aspx?code=" + ((Guid)mu.ProviderUserKey).ToString() + ".");

							EmailUtility.SendMail("from@from.com", "to@to.com", "email subject - registration", body.ToString());
						}
						phForm.Visible = false;
						phMessage.Visible = true;
						ltlMessage.Text = "Your user was created and a registration email was sent to you.";
					}
				} catch (System.Web.Security.MembershipCreateUserException ex) {
					ltlMessage.Text = "Processing error. Please try again later.";// +ex.ToString();
				}
			}
		} else {
			//error
			if(!txtPass.Text.Equals(txtConfirmPass.Text)){
				ltlMessage.Text = "Passwords don't match";
			} else if(txtEmailAddress.Text.Equals(txtConfirmEmailAddress.Text)) {
				ltlMessage.Text = "Email Addresses don't match";
			}
		}
	}

	public static bool RegisterUser(Guid userKey) {
			
		MembershipUser newUser = Membership.GetUser(userKey);
		if (newUser != null) {
			User u = (User)User.FromName(newUser.UserName, AccountType.User);
			using (new Sitecore.SecurityModel.SecurityStateSwitcher(Sitecore.SecurityModel.SecurityState.Disabled)) {
				//add this user to the site role
				//also check if the role contains "extranet" to make sure they don't get added to the reader/editor/manager roles
				if (HasExtranetRole()) {
					List<Role> roles = Sitecore.Context.Domain.GetRoles().Where(a => a.Name.Equals("extranetYourNewRole")).ToList();
					if (roles.Any()) {
						//could also loop through them all if there are multiple
						//need to make sure there is a convention for knowing which to add.
						u.Roles.Add(roles.First());
						return true;
					}
				}
			}
		}
		return false;
	}
}

Edit Account

The edit account page is only accounting for the base level fields and anything you customize could end up here. I've only provided the ability to update the email and password but it could be used for much more. Here's the .aspx code

<div class="">
	<h3>Change Your Password</h3>
	<div>
		<asp:Literal ID="ltlMessagePass" runat="server" />
	</div>
	<div>
		<span>Old Password</span>
		<asp:TextBox ID="txtPassOld" TextMode="Password" runat="server" /><br/>

		<span>New Password</span>
		<asp:TextBox ID="txtPassNew" TextMode="Password" runat="server" /><br/>
		<span>Confirm New Password</span>
		<asp:TextBox ID="txtPassConfirm" TextMode="Password" runat="server" /><br/>
		<asp:Button ID="btnEditPass" Text="Submit"  runat="server"/>
	</div>
	<br /><br />
	<h3>Change Your Email Address</h3>
	<div>
		<asp:Literal ID="ltlMessageEmail" runat="server" />
	</div>
	<div>
		<span>Old Email Address</span>
		<asp:TextBox ID="txtEmailAddressOld" runat="server" /><br/>
		<span>New Email Address</span>
		<asp:TextBox ID="txtEmailAddressNew" runat="server" /><br/>
		<span>Confirm New Email</span>
		<asp:TextBox ID="txtEmailAddressConfirm" runat="server" /><br/>
		<asp:Button ID="btnEditEmail" Text="Submit"  runat="server"/>
	</div>
</div>

The code behind

using System;
using System.Collections;
using System.Configuration;
using System.Data;
using System.Linq;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.HtmlControls;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Xml.Linq;
using Sitecore.Configuration;
using Sitecore.SecurityModel.Cryptography;
using Sitecore.Web;
using Sitecore.Security.Authentication;
using Sitecore.Security.Accounts;

namespace YourNamespace.Extranet
{
	public partial class EditAccount : System.Web.UI.UserControl
	{
		protected void Page_Load(object sender, EventArgs e) {

			//if you're not logged in you shouldn't be on this page.
			if (!(Sitecore.Context.IsLoggedIn && Sitecore.Context.User.Roles.Where(a => a.Name.Contains("Extranet Users")).Any())) {
				Response.Redirect(Sitecore.Context.Site.LoginPage);
			}
			
			//set the current email to the form
			txtEmailAddressOld.Text = Sitecore.Context.User.Profile.Email;
		}

		protected void lnkPass_Click(object sender, EventArgs e) {
			phChoose.Visible = false;
			phPass.Visible = true;
		}

		protected void lnkEmail_Click(object sender, EventArgs e) {
			phChoose.Visible = false;
			phEmail.Visible = true;
		}
		
		protected void btnEditPass_Click(object sender, EventArgs e) {
			if (Page.IsValid && txtPassNew.Text.Equals(txtPassConfirm.Text)) {
				string message = "";
				bool updated = UpdatePassword(txtPassOld.Text, txtPassNew.Text, ref message);
				ltlMessagePass.Text = message;
			} else {
				ltlMessagePass.Text = "Password doesn't match.";
			}
		}

		protected void btnEditEmail_Click(object sender, EventArgs e) {
			string newEmail = txtEmailAddressNew.Text;
			if (Page.IsValid && newEmail.Equals(txtEmailAddressConfirm.Text)) {
				string message = "";
				if(UpdateEmail(newEmail, ref message)){
					txtEmailAddressOld.Text = newEmail;
					txtEmailAddressNew.Text = "";
					txtEmailAddressConfirm.Text = "";
				}
				ltlMessageEmail.Text = message;
			} else {
				ltlMessageEmail.Text = "Email doesn't match";
			}
		}

		public static bool UpdatePassword(string oldPassword, string newPassword, ref string message) {
			//get auth helper
			AuthenticationHelper authHelper = new AuthenticationHelper(Sitecore.Security.Authentication.AuthenticationManager.Provider);
			try {
				//check to see if the existing password is correct
				if (!authHelper.ValidateUser(Sitecore.Context.User.Name, oldPassword)) {
					//throw new System.Security.Authentication.AuthenticationException("Incorrect password.");
					message = SitecoreUtility.GetStringContent("/Strings/Extranet/EditAccount/OldPasswordIsIncorrect", LanguageFallback.English);
				} else {
					//get the current user
					System.Web.Security.MembershipUser user = System.Web.Security.Membership.GetUser(Sitecore.Context.User.Name);
					if (user.ChangePassword(oldPassword, newPassword)) {
						message = SitecoreUtility.GetStringContent("/Strings/Extranet/EditAccount/PasswordHasBeenChanged", LanguageFallback.English);
						return true;					
					} else {
						//throw new System.Security.Authentication.AuthenticationException("Unable to change password");
						message = SitecoreUtility.GetStringContent("/Strings/Extranet/EditAccount/UnableToChangePassword", LanguageFallback.English);
					}
				}
			} catch (System.Security.Authentication.AuthenticationException) {
				message = SitecoreUtility.GetStringContent("/Strings/Extranet/EditAccount/AuthenticationError", LanguageFallback.English);
			}
			return false;
		}

		public static bool UpdateEmail(string newEmail, ref string message) {
			//get auth helper
			AuthenticationHelper authHelper = new AuthenticationHelper(Sitecore.Security.Authentication.AuthenticationManager.Provider);
			try {
				//get the current user
				User u = Sitecore.Context.User;
				u.Profile.Email = newEmail;
				u.Profile.Save();

				message = SitecoreUtility.GetStringContent("/Strings/Extranet/EditAccount/EmailHasChanged", LanguageFallback.English);

				return true;
			} catch (System.Security.Authentication.AuthenticationException) {
				message = SitecoreUtility.GetStringContent("/Strings/Extranet/EditAccount/EmailWasntChanged", LanguageFallback.English);
			}
			return false;
		}
	}
}

Forgot Password

There are several different ways you could handle a user forgetting his password. I've opted to reset the password and email the new value to the user. By default your Sitecore installation will not support retrieving passwords. You can setup your environment so that you will be able to do this but it's past the scope of this article. If you want the ability to retrieve passwords and understand the security implications you can start reading up on it over at MSDN.

<div>
	<div>
		<asp:Literal ID="ltlMessage" runat="server" />
	</div>
	<span>Username</span>
	<asp:TextBox ID="txtUser" runat="server" />
	<asp:Button ID="btnForgot" Text="Submit"  runat="server"/>
</div>

The code behind

using System;
using System.Collections;
using System.Configuration;
using System.Data;
using System.Linq;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.HtmlControls;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Xml.Linq;
using Sitecore;
using Sitecore.Diagnostics;
using Sitecore.Configuration;
using Sitecore.Web;
using Sitecore.SecurityModel.Cryptography;

namespace YourNamespace.Extranet
{
	public partial class ForgotPassword : System.Web.UI.UserControl
	{
		protected void Page_Load(object sender, EventArgs e) {
			
		}

		protected void btnForgot_Click(object sender, EventArgs e) {
			if (Page.IsValid) {
				try {
					string domainUser = Sitecore.Context.Domain.GetFullName(txtUser.Text);
					if (!Sitecore.Security.Accounts.User.Exists(domainUser)) {
						//throw new System.Security.Authentication.AuthenticationException(domainUser + " does not exist.");
						ltlMessage.Text = txtUser.Text + " does not exist.";
					} else {
						System.Web.Security.MembershipUser user = System.Web.Security.Membership.GetUser(domainUser);
						if (System.Web.Security.Membership.EnablePasswordRetrieval) {
							ltlMessage.Text = "Password for " + user.UserName + ": ";
							if (System.Web.Security.Membership.RequiresQuestionAndAnswer) {
								ltlMessage.Text += user.GetPassword("");
								//MainUtil.SendMail();
							} else {
								ltlMessage.Text += user.GetPassword();
								////MainUtil.SendMail();
							}
						} else {
							//throw new System.Configuration.ConfigurationErrorsException("Cannot retrieve or reset passwords.");
							ltlMessage.Text = "Cannot retrieve password.";
						}
					}
				} catch (System.Security.Authentication.AuthenticationException) {
					ltlMessage.Text = "Processing error.";
				} catch (System.Configuration.ConfigurationErrorsException) {
					ltlMessage.Text = "Configuration error.";
				}
			}
		}

		protected void btnReset_Click(object sender, EventArgs e) {
			try {
				string domainUser = Sitecore.Context.Domain.GetFullName(txtUser.Text);
				if (!Sitecore.Security.Accounts.User.Exists(domainUser)) {
					//throw new System.Security.Authentication.AuthenticationException(domainUser + " does not exist.");
					ltlMessage.Text = txtUser.Text + " does not exist.";
				} else {
					System.Web.Security.MembershipUser user = System.Web.Security.Membership.GetUser(domainUser);
					if (System.Web.Security.Membership.EnablePasswordReset) {
						ltlMessage.Text = "New password for " + user.UserName + ": ";
						if (System.Web.Security.Membership.RequiresQuestionAndAnswer) {
							ltlMessage.Text += user.ResetPassword("");
							//MainUtil.SendMail();
						} else {
							ltlMessage.Text += user.ResetPassword();
							//MainUtil.SendMail();
						}
					} else {
						//throw new System.Configuration.ConfigurationErrorsException("Cannot retrieve or reset passwords.");
						ltlMessage.Text = "Cannot reset password.";
					}
				}
			} catch (System.Security.Authentication.AuthenticationException) {
				ltlMessage.Text = "Processing error.";
			} catch (System.Configuration.ConfigurationErrorsException) {
				ltlMessage.Text = "Configuration error.";
			}
		}
	}
}

2. Setup Security Handler

Now that the page is being hidden what we want to do is add a pipeline handler that catches the conditions where a page not found is as a result of a permissions restriction not just becuase the page doesn't exist. To do this you'll want to add a security resolver processor to the <httpRequestBegin> event pipeline in the web.config after the LayoutResolver processor. The web.config entry will look like this

<processor type="YourLibrary.Pipelines.HttpRequest.ExtranetSecurityResolver, YourLibrary"></processor>

And the class will look like this:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Sitecore.Sites;
using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.SecurityModel;
using Sitecore.Web;
using System.Web;
using Sitecore.Pipelines.HttpRequest;
using System.Net;
using Sitecore.Security.AccessControl;
using Sitecore.Diagnostics;
using Sitecore.Security.Accounts;
using Sitecore;
using Sitecore.IO;
using Sitecore.Data.Managers;
using Sitecore.Globalization;

namespace YourLibrary.Pipelines.HttpRequest
{
	public class ExtranetSecurityResolver : HttpRequestProcessor {
		public override void Process(HttpRequestArgs args) {
			//make sure you're on the right domain and page mode
			if (!Sitecore.Context.Domain.Name.ToLower().Contains("sitecore") && Sitecore.Context.PageMode.IsNormal) {
				// Get the site context 
				SiteContext site = Sitecore.Context.Site;

				// Check if the current user has sufficient rights to enter this page 
				if (SiteManager.CanEnter(site.Name, Sitecore.Context.User)) {
					string prefix = args.StartPath;

					if (args.LocalPath.Contains(Sitecore.Context.Site.StartPath))
						prefix = String.Empty;

					if (Sitecore.Context.Database == null)
						return;
					
					// Get the item using securityDisabler for restricted items such as permission denied items
					Item contextItem = null;
					using (new SecurityDisabler()) {
						if (Context.Database != null && args.Url.ItemPath.Length != 0) {
							string path = MainUtil.DecodeName(args.Url.ItemPath);
							Item item = args.GetItem(path);
							if (item == null) {
								path = args.LocalPath;
								item = args.GetItem(path);
							}
							if (item == null) {
								path = MainUtil.DecodeName(args.LocalPath);
								item = args.GetItem(path);
							}
							string str2 = (site != null) ? site.RootPath : string.Empty;
							if (item == null) {
								path = FileUtil.MakePath(str2, args.LocalPath, '/');
								item = args.GetItem(path);
							}
							if (item == null) {
								path = MainUtil.DecodeName(FileUtil.MakePath(str2, args.LocalPath, '/'));
								item = args.GetItem(path);
							}
							if (item == null) {
								Item root = ItemManager.GetItem(site.RootPath, Language.Current, Sitecore.Data.Version.Latest, Context.Database, SecurityCheck.Disable);
								if (root != null) {
									string path2 = MainUtil.DecodeName(args.LocalPath);
									item = this.GetSubItem(path2, root);
								}
							}
							if (item == null) {
								int index = args.Url.ItemPath.IndexOf('/', 1);
								if (index >= 0) {
									Item root = ItemManager.GetItem(args.Url.ItemPath.Substring(0, index), Language.Current, Sitecore.Data.Version.Latest, Context.Database, SecurityCheck.Disable);
									if (root != null) {
										string path3 = MainUtil.DecodeName(args.Url.ItemPath.Substring(index));
										item = this.GetSubItem(path3, root);
									}
								}
							}
							if (((item == null) && args.UseSiteStartPath) && (site != null)) {
								item = args.GetItem(site.StartPath);
							}
							contextItem = item;
						}
					}
					
					//Item contextItem = Sitecore.Context.Item;
					if (contextItem != null) {
						User u = Sitecore.Context.User;
						bool isAllowed = AuthorizationManager.IsAllowed(contextItem, AccessRight.ItemRead, u);

						if (!isAllowed && (site.LoginPage.Length > 0)) {
							// Redirect the user 
							WebUtil.Redirect(String.Format("{0}?returnUrl={1}", site.LoginPage, HttpContext.Current.Server.HtmlEncode(HttpContext.Current.Request.RawUrl)));
						}
					}
				}
			}
                }
		
		private Item GetSubItem(string path, Item root) {
			Item child = root;
			foreach (string str in path.Split(new char[] { '/' })) {
				if (str.Length != 0) {
					child = this.GetChild(child, str);
					if (child == null) {
						return null;
					}
				}
			}
			return child;
		}

		private Item GetChild(Item item, string itemName) {
			foreach (Item item2 in item.Children) {
				if (item2.DisplayName.Equals(itemName, StringComparison.OrdinalIgnoreCase)) {
					return item2;
				}
				if (item2.Name.Equals(itemName, StringComparison.OrdinalIgnoreCase)) {
					return item2;
				}
			}
			return null;
		}
	}
}

One of the things that the articles I read previously had done that I had to change was how they determined if the user had the proper credentials to view the current item. I ended up changing "contextItem.Access.CanRead()" to "AuthorizationManager.IsAllowed" which cleared up the issue for me.

3. Setup Login URL

Sitecore provides a location for you to store the url to your login on your site definition. You can set this up by adding the "loginUrl" attribute to your site node in your web.config or in the "loginUrl" field of your site node (if you're using the multi site manager). It's the path, relative to your home page, to the login page. This will be used by you later when the user doesn't have the credentials to view the hidden pages.

4. Create the Extranet Role

When approaching creating a role or set of roles for your visitors you should be aware of how Sitecore sees your visitors and defines the difference between Content Authoring users from web site visitors. Sitecore, by default, has two domains: Sitecore and Extranet. The sitecore domain is used when logging into sitecore and the extranet as you may have surmised is used when you're visiting the front-end of a website. You may, for many reasons, want to define your own domain to define a separate set of users and permissions. This is supported by Sitecore but for the purposes of this article it is unnecessary. When you visit a Sitecore website and you are not logged in the Sitecore.Context.User will define you as the "Extranet/Anonymous" user. You will want to discern the particular group of visitors you wish to target by creating a new role for them to use in the Extranet domain, or your custom domain if you chose to add one. If you log into Sitecore and open the Role Manager.

role manager

Then click on the "New" button you can create a new role.

create role

I've name mine "Extranet User" and selected from the drop down field "Extranet".

5. Hide Pages by Adding Security Through Permissions

When you've created the roles that you'll want for this site you'll want to apply some security permissions relating to your site's role for the pages you want to restrict access to. The Sitecore Security Administrator's Cookbook talks about how you shouldn't specifically deny read permissions. Instead you should break inheritance. Ok, what does that mean. Well there's no single button that's going to break inheritance. You're breaking inheritance on a role or user for a particular item. Which role or user will depend on the domain or domains you're trying to restrict. For this case, since the default role that will capture all web visitors is "ExtranetAnonymous", we'll be breaking inheritance on this role. To do this, select the item you wish to restrict visibility on, jump to the security tab and click on the assign button.

security tab

The following image shows the dialog window where you will need to add the "ExtranetEveryone" role and add the following permission values to break the inheritance.

assign security extranet everyone

What this will do is break the default read for web visitors and the page will not be accessible. Then you will want to allow the read permissions for your new extranet role that registered users will have added to their account. This will allow you to set the permissions once and handle for all users. The image below shows the permissions applied to the extranet role you want to have access to these pages.

assign security extranet users

When you're finished you'll want to publish the item so that these permissions will be applied to the live content.

6. Test It!

Browse to the site and try to go to a page that is now restricted. You should end up with a Page Not Found error. This is because you've just restricted the access and Sitecore is behaving in the way you'd expect. Now you'll want to jump to the registration page and register a user. You should receive an email which then let's you register/login. Now try the page again. Now you should be able to both see the page in the navigation and be able to access it.

7. Check the Users

It's worth mentioning that you can now browse through the users in the user manager in Sitecore. This allows you to manually setup and manage users or if need be, disable users.

8. Additional Considerations

Navigation Permission

Now that you've got everything ready to go you're going to run into the inevitable issue of directing users to the hidden pages. You may perhaps only show links to the hidden pages once a user logs in and have a dedicated link to your extranet login. In this case you're ready to roll and start registering users. You may on the other hand want to have the navigation link directly to your hidden page and only when the user tries to go to that page will you force them to login. To do this when building your navigation you will want to disable the security when querying for the items so that there is still a link even though the current guest user is restricted from viewing these items.

Customization

This framework is a generic one that you will need to customize for your own installation. There are a few things that I will point out that you will want to consider before you deploy your installation.

Form Validation, Captcha and Honeypots
You will not want to deploy these forms without some form of protection against the internet-at-large. It's standard practice to force users to conform form values and it can be a vector of attack if you do not protect the values before you store them. Make sure if you're storing anything that you use parameterized insertion. The last thing you want is to have your data wiped when you can easily protect against it. Also consider that there are a lot of malicious individuals that you will want to protect yourself against by using some simple captcha forms and honeypots that can drastically filter spam and bots attempts from storming your castle.

Limit the number of login attempts for a given time period
This can be a very useful defense for automated attempts to prevent dictionary style login attacks. Any normal person really won't be trying to login any more than a few times before they go and lookup their password. If you're not allowing more than say, 6 attempts in a half hour, you're really limiting a brute force attack to take forever while really not inconveniencing your users at all.

Multiple Domain Support
If you have a multi-site installations you will obviously need separate roles to support these with multiple domains. You will want to consider storing the value of each role somewhere on the site node for each site so that you can do a lookup when it comes time to register a user. Another thing you should also consider is that if you're not going to be creating your own custom domains within Sitecore and plan to create all your users in the existing extranet domain, you will want to prefix user names with the site name or some other convention. This will allow you users to register separately on each different sites without the system saying that the user already exists.

Alright that about wraps it up. It took me a lot longer to write this article than I planned but hopefully you won't run into to many issues setting this up and can get things going a little quicker than I did.