Share-n-dipity

SharePoint serendipity is the effect by which one accidentally discovers something fortunate, especially while looking for something else entirely. In this case, it is the occassional musings, observations, and Ouija board readings about the phabulously

Writing A Custom Forms Login Page for SharePoint 2010 Part 2

Writing A Custom Forms Login Page for SharePoint 2010 Part 2

  • Comments 14
  • Likes

NEW PREFACE TO THIS POSTING, AND OPEN COMMENT TO THE SITE OWNER:

Since the blog site was upgraded to the latest version of whatever the blazes this thing is, the formatting has stunk like a pig on the 4th of July.  I usually spend a fair amount of time trying to jury-rig the pathetic markup (or lack thereof) this new version provides.  PLEASE CONSIDER CONTACTING THE WEB MASTER AND LETTING THEM KNOW HOW DIFFICULT IT IS TO READ THESE AWFULLY FORMATTED POSTS!  In the meantime, I will try and attach a Word document with the original post as I typed it whenever possible.  Who says technology doesn't stink??  Okay, here we go...

In part 1 of this series, which you can find at http://blogs.technet.com/b/speschka/archive/2010/07/21/writing-a-custom-forms-login-page-for-sharepoint-2010-part-1.aspx, I described how to create an entirely new forms login page.  The scenario behind it is when you need something beyond what is available with the out of the box UI – for example, two factor authentication.  In this post I’m going to walk through a different scenario.  In this case the out of the box UI is fine, but we want to do “something” additional at login time. 

For this particular scenario, I want all users to agree to the terms of use for the site before they ever access it.  So I’m going to start by creating a new login page.  I’m going to add a handler for the logging on event, and in that event I’m going to see if they have a cookie that says they’ve agreed to the terms of use.  If they haven’t, then I’ll redirect them to a page where they can check a box saying that they agree, and then they’ll be redirected back to the login page.  When they log in again, I’ll see they have the terms of use cookie so I can just send them onto whatever resource they requested – the home page, a document, etc.

To start with, I’m going to create my new login page.  I just added a new ASPX page to my project.  In this project I’m going to need to add references to Microsoft.SharePoint and Microsoft.SharePoint.IdentityModel.   For details on how to add the reference to Microsoft.SharePoint.IdentityModel, see part 1, which I’ve linked to above.  In the code behind for my page I’m going to use a gaggle of using statements again.  Here’s which one’s I’ve used:

using System;

using System.Collections.Generic;

using System.Linq;

using System.Web;

using System.Web.UI;

using System.Web.UI.WebControls;

using Microsoft.SharePoint.IdentityModel.Pages;

using System.Diagnostics;

Because the page I’m writing is a login page for forms based authentication, it needs to inherit from FormsSignInPage.  My class declaration looks like this:

public partial class AgreeLogin : FormsSignInPage

The first thing I’m going to do in code is to override the page Load event.  In there I’ll add a handler for the logging in event for the page’s ASP.NET Login control.  Here’s what the override looks like for Page Load:

protected override void OnLoad(EventArgs e)

{

try

       {

             base.OnLoad(e);

             this.signInControl.LoggingIn +=

                   new LoginCancelEventHandler(signInControl_LoggingIn);

}

catch (Exception ex)

       {

              Debug.WriteLine(ex.Message);

       }

}

One note here – I put this in a try…catch block because if you goof up the page markup, which I’ll explain below, you will toss an error when base.OnLoad(e) is called.  Next, I’ll go ahead and show you the implementation of the LoggingIn event handler:

void signInControl_LoggingIn(object sender, LoginCancelEventArgs e)

{

//look for a cookie; if not there then redirect to our accept terms page

       const string AGREE_COOKIE = "SignedTermsAgreement";

     try

       {

       //we want to check for our cookie here

              HttpCookieCollection cookies = HttpContext.Current.Request.Cookies;

              HttpCookie ck = cookies[AGREE_COOKIE];

              if (ck == null)

              //user doesn't have the cookie indicating that they've signed the

              //terms of use so redirect them

              Response.Redirect("AgreeTerms.aspx?" +

                     Request.QueryString.ToString());

}

       catch (Exception ex)

       {

       Debug.WriteLine("There was an error processing the request: " +

              ex.Message);

       }

}

So what I’m doing here is when I start logging on, I’m first going to look for my cookie that says the user has agreed to the terms of use for the site.  If I don’t find the cookie then I’m going to redirect the user to my AgreeTerms.aspx page.  I’m including the full query string that the login page received because it tells me what resource the user was requesting when they hit the login page, so I’ll know where to send them again after they’ve agreed to terms of use.  One important thing to remember here is that I need to make a change to my web.config for my SharePoint web application so that the terms of use page can be accessed anonymously.  Otherwise, if it were a secured resource like every other page in the site I’d be caught in an endless loop – I’d say you don’t have the cookie, I’d redirect you to the AgreeTerms.aspx page, but since it’s secured it would redirect me back to my custom login page, and round and round we go.  So I modified the web.config to include this entry right below the closing </system.web> tag:

<location path="_layouts/AgreeTerms.aspx">

<system.web>

<authorization>

<allow users="*" />

</authorization>

</system.web>

</location>

So now I can hit my AgreeTerms.aspx page without being authenticated.  I won’t bore you with the UI of the page now, but in the code behind for this page it is pretty simple:

protected void SubmitBtn_Click(object sender, EventArgs e)

{

const string AGREE_COOKIE = "SignedTermsAgreement";

try

       {

              //make sure the checkbox is checked

if ((AgreeChk.Checked) && (Request.Browser.Cookies))

              {

                     //create the cookie

HttpCookie ck = new HttpCookie(AGREE_COOKIE, "true");

//set the expiration so it's persisted

ck.Expires = DateTime.Now.AddYears(3);

//write it out

HttpContext.Current.Response.Cookies.Add(ck);

//get the src attribute from the query string and redirect there

Response.Redirect(Request.QueryString["Source"]);

}

else

StatusLbl.Text = "You must agree to the terms of use before continuing.";

}

catch (Exception ex)

       {

              string msg = "There was an error processing the request: " + ex.Message;

              Debug.WriteLine(msg);

              StatusLbl.Text = msg;

}

} 

This should be pretty clear – if you check the box then I give you the cookie and I Response.Redirect you to the source you requested originally.  This will send you back to the login page, and this time when you login we’ll see the cookie and everything will just work easy shmeezy.  If you don’t agree to the terms of use then you won’t be going anywhere.

That’s all there is to it from a coding perspective, but there is one other thing that is SUPER important to have down, and that’s the mark up for the login page.  As I described earlier, if the mark up is wrong you’ll be in all sorts of trouble.  So to start with, here’s the mark up you should start with:

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="AgreeLogin.aspx.cs" Inherits="FormsLoginPage.AgreeLogin,FormsLoginPage, Version=1.0.0.0, Culture=neutral, PublicKeyToken=cf32e76ff986e00f" MasterPageFile="~/_layouts/simple.master" %>

 

<%@ Assembly Name="Microsoft.SharePoint.ApplicationPages, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c"%>

<%@ Import Namespace="Microsoft.SharePoint.ApplicationPages" %> <%@ Register Tagprefix="SharePoint" Namespace="Microsoft.SharePoint.WebControls" Assembly="Microsoft.SharePoint, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>

<%@ Register Tagprefix="Utilities" Namespace="Microsoft.SharePoint.Utilities" Assembly="Microsoft.SharePoint, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %> <%@ Import Namespace="Microsoft.SharePoint" %>

<%@ Assembly Name="Microsoft.Web.CommandUI, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>

<asp:Content ContentPlaceHolderId="PlaceHolderPageTitle" runat="server">

    <SharePoint:EncodedLiteral ID="ClaimsFormsPageTitle" runat="server" text="<%$Resources:wss,login_pagetitle%>" EncodeMethod='HtmlEncode'/>

</asp:Content>

<asp:Content ContentPlaceHolderId="PlaceHolderPageTitleInTitleArea" runat="server">

    <SharePoint:EncodedLiteral ID="ClaimsFormsPageTitleInTitleArea" runat="server" text="<%$Resources:wss,login_pagetitle%>" EncodeMethod='HtmlEncode'/>

</asp:Content>

<asp:Content ContentPlaceHolderId="PlaceHolderSiteName" runat="server"/>

<asp:Content ContentPlaceHolderId="PlaceHolderMain" runat="server">

<SharePoint:EncodedLiteral runat="server" EncodeMethod="HtmlEncode" ID="ClaimsFormsPageMessage" />

 <asp:login id="signInControl" FailureText="<%$Resources:wss,login_pageFailureText%>" runat="server" width="100%">

    <layouttemplate>

        <asp:label id="FailureText" class="ms-error" runat="server"/>

        <table class="ms-input">

          <colgroup>

          <col width="25%"/>

          <col width="75%"/>

          </colgroup>

        <tr>

            <td nowrap="nowrap"><SharePoint:EncodedLiteral ID="EncodedLiteral3" runat="server" text="<%$Resources:wss,login_pageUserName%>" EncodeMethod='HtmlEncode'/></td>

            <td><asp:textbox id="UserName" autocomplete="off" runat="server" class="ms-long ms-login-textbox"/></td>

        </tr>

        <tr>

            <td nowrap="nowrap"><SharePoint:EncodedLiteral ID="EncodedLiteral4" runat="server" text="<%$Resources:wss,login_pagePassword%>" EncodeMethod='HtmlEncode'/></td>

            <td><asp:textbox id="password" TextMode="Password" autocomplete="off" runat="server" class="ms-long ms-login-textbox"/></td>

        </tr>

        <tr>

            <td colspan="2" align="right"><asp:button id="login" commandname="Login" text="<%$Resources:wss,login_pagetitle%>" runat="server" /></td>

        </tr>

        <tr>

            <td colspan="2"><asp:CheckBox id="RememberMe" text="<%$SPHtmlEncodedResources:wss,login_pageRememberMe%>" runat="server" /></td>

        </tr>

        </table>

    </layouttemplate>

 </asp:login>

</asp:Content>

 

The one and only thing you really want to change is the very first line, the @Page directive.

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="AgreeLogin.aspx.cs" Inherits="FormsLoginPage.AgreeLogin,FormsLoginPage, Version=1.0.0.0, Culture=neutral, PublicKeyToken=cf32e76ff986e00f" MasterPageFile="~/_layouts/simple.master" %>

The part highlighted in yellow should be changed to be the strong name for your custom assembly.

Once you’ve got this all put together and compiled, just register your assembly in the GAC and copy your ASPX page(s) to the layouts directory.  The last step before you actually try it out is to change login page for the web application zone where you are using FBA.  Go to central administration, Application Management, Manage web applications.  Select your web application and then click the Authentication Providers button on the toolbar.  Click the link to the zone you want to change, and in the dialog that comes up click the Custom Sign In Page radio button and enter the Url to your login page.  In my case it was _layouts/AgreeLogin.aspx.   Save your changes and you’re ready to log into your site.

Finally, here’s what it looked like as I logged into my site with my custom pages – hope this post helps you do the same!

1.       Initial Login

2.       Terms of Use Page

3.       Finally Hitting the Site

 

Attachment: Writing A Custom Forms Login Page Part 2.docx
Comments
Your comment has been posted.   Close
Thank you, your comment requires moderation so it may take a while to appear.   Close
Leave a Comment