In the end, I didn't use the ASP:Login control. I just designed my own login box with two textboxes. Username and password.
My login box, and my change password box are in two panels on the page. One hidden the other not. The person logs in, checks with the database if it is valid and if they need to change their password, if so, hides the login box panel, and displays the change password box. At this point, the user still isn't logged into SharePoint.
The user changes the password, the user is updated that they do not need to change their password on next login, and the panels are switched again. This is to allow the user to log in with their new password. This time as they log in they are properly logged in and authorized into SharePoint.
<%@ Assembly Name="$SharePoint.Project.AssemblyFullName$" %>
<%@ Assembly Name="Microsoft.SharePoint, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="SignInPage.aspx.cs" Inherits="SharePoint.FBA.Layouts.Login.SignInPage" %>
<%@ Register TagPrefix="SharePoint" Namespace="Microsoft.SharePoint.WebControls" Assembly="Microsoft.SharePoint, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html>
<head>
<link rel="icon" href="/_layouts/Images/Login/favicon.ico" type="image/x-icon" />
<link rel="shortcut icon" href="/_layouts/Images/Login/favicon.ico" type="image/x-icon"/>
<title>Sign In Page</title>
<style type="text/css">
html, body {height:100%; margin:0; padding:0;}
#page-background {position:fixed; top:0; left:0; width:100%; height:100%;}
#content {position:relative; z-index:1; padding:10px;}
.messages {font-family:Arial;font-size:12px;color:Red;font-style: italic;}
</style>
</head>
<body>
<form id="form1" runat="server">
<center>
<!-- Container for Sign-In Controls-->
<asp:PlaceHolder runat="server" ID="phPart1" Visible="true">
<div id="SignInContent" style="width: 300px; height: auto; margin-top: 15%; background:black;">
<table border="0" cellpadding="1" cellspacing="0" style="margin:20px; width:100%">
<tr>
<td align="left">
<asp:Label ID="UserNameLabel" runat="server" ForeColor="White">User Name:</asp:Label>
</td>
</tr>
<tr>
<td>
<asp:TextBox ID="UserName" Width="300px" runat="server"></asp:TextBox>
</td>
<td>
<asp:RequiredFieldValidator ID="UserNameRequired" runat="server" ControlToValidate="UserName"
ErrorMessage="User Name is required." ToolTip="User Name is required." ValidationGroup="SignInContent">*</asp:RequiredFieldValidator>
</td>
</tr>
<tr>
<td align="left" style="padding-top:10px">
<asp:Label ID="PasswordLabel" runat="server" ForeColor="White" AssociatedControlID="Password">Password:</asp:Label>
</td>
</tr>
<tr>
<td>
<asp:TextBox ID="Password" Width="300px" runat="server" TextMode="Password"></asp:TextBox>
</td>
<td>
<asp:RequiredFieldValidator ID="PasswordRequired" runat="server" ControlToValidate="Password"
ErrorMessage="Password is required." ToolTip="Password is required." ValidationGroup="SignInContent">*</asp:RequiredFieldValidator>
</td>
</tr>
<tr>
<td align="center" colspan="3" style="color: Red; font-size: small;">
<asp:Literal ID="FailureTextLogin" runat="server" EnableViewState="False"></asp:Literal>
</td>
</tr>
<tr>
<td align="left" style="padding-top:10px">
<asp:CheckBox ID="RememberMe" runat="server" ForeColor="White" Text="Remember me next time."/>
</td>
</tr>
<tr>
<td align="right">
<asp:Button ID="LoginButton" runat="server" Text="Log in" OnClick="btnSubmit_OnClick" ValidationGroup="SignInContent"/>
</td>
</tr>
</table>
</div>
</asp:PlaceHolder>
<!--Container for changing password-->
<asp:PlaceHolder runat="server" ID="phPart2" Visible="false">
<div id="ChangePassword" style="width: 300px; height: auto; margin-top: 15%; background:black;">
<table border="0" cellpadding="1" cellspacing="0" style="margin:20px; width:100%">
<tr>
<td align="left">
<asp:Label ID="CurrentPasswordLabel" runat="server" ForeColor="White">Password:</asp:Label>
</td>
</tr>
<tr>
<td>
<asp:TextBox ID="CurrentPassword" Width="300px" runat="server" TextMode="Password"></asp:TextBox>
</td>
<td>
<asp:RequiredFieldValidator ID="CurrentPasswordRequired" runat="server" ControlToValidate="CurrentPassword"
ErrorMessage="Password is required." ToolTip="Password is required." ValidationGroup="ChangePassword">*</asp:RequiredFieldValidator>
</td>
</tr>
<tr>
<td align="left">
<asp:Label ID="NewPasswordLabel" runat="server" ForeColor="White" Enabled="True">New Password:</asp:Label>
<asp:CompareValidator ID="CompareValidator1" runat="server" ControlToCompare="NewPassword"
ControlToValidate="ConfirmNewPassword" ErrorMessage="The confirm new password must match the new password."
ToolTip="The confirm new password must match the new password." ValidationGroup="ChangePassword">*</asp:CompareValidator>
</td>
</tr>
<tr>
<td>
<asp:TextBox ID="NewPassword" runat="server" Width="300px" TextMode="Password"></asp:TextBox>
</td>
<td>
<asp:RequiredFieldValidator ID="NewPasswordRequired" runat="server" ControlToValidate="NewPassword"
ErrorMessage="New password is required." ToolTip="New password is required."
ValidationGroup="ChangePassword">*</asp:RequiredFieldValidator>
</td>
</tr>
<tr>
<td align="left">
<asp:Label ID="ConfirmNewPasswordLabel" runat="server" ForeColor="White">Confirm New Password:</asp:Label>
<asp:CompareValidator ID="NewPasswordCompare" runat="server" ControlToCompare="NewPassword"
ControlToValidate="ConfirmNewPassword" ErrorMessage="The confirm new password must match the new password."
ToolTip="The confirm new password must match the new password." ValidationGroup="ChangePassword">*</asp:CompareValidator>
</td>
</tr>
<tr>
<td>
<asp:TextBox ID="ConfirmNewPassword" Width="300px" runat="server" TextMode="Password"></asp:TextBox>
</td>
<td>
<asp:RequiredFieldValidator ID="ConfirmNewPasswordRequired" runat="server" ControlToValidate="ConfirmNewPassword"
ErrorMessage="Confirm new password is required." ToolTip="Confirm new password is required."
ValidationGroup="ChangePassword">*</asp:RequiredFieldValidator>
</td>
</tr>
<tr>
<td align="center" style="color: Red; font-size: small; ">
<asp:Literal ID="FailureTextPasswordChange" runat="server" EnableViewState="False"></asp:Literal>
</td>
</tr>
<tr>
<td align="right" style="padding-top:10px">
<asp:Button ID="ChangePasswordPushButton" runat="server" OnClick="ChangePasswordBtn_Click"
Text="Change Password" ValidationGroup="ChangePassword" Padding-Right="5px" />
<asp:Button ID="CancelPushButton" runat="server" CausesValidation="False" OnClick="Cancel_Click"
Text="Cancel" />
</td>
</tr>
</table>
</div>
</asp:PlaceHolder>
</center>
</form>
</body>
</html>
The code behind looks like the following.
using System;
using System.Diagnostics;
using System.IdentityModel.Tokens;
using System.Text.RegularExpressions;
using SharePoint.FBA.Code.HelperClasses;
using Microsoft.SharePoint;
using Microsoft.SharePoint.IdentityModel;
using Microsoft.SharePoint.WebControls;
namespace SharePoint.FBA.Layouts.Login
{
public partial class SignInPage : System.Web.UI.Page
{
private string _userName
{
get { return ViewState["vs_username"].ToString(); }
set { ViewState["vs_username"] = value; }
}
private string _password
{
get { return ViewState["vs_password"].ToString(); }
set { ViewState["vs_password"] = value; }
}
private bool _remember
{
get { return Convert.ToBoolean(ViewState["vs_remember"].ToString()); }
set {ViewState["vs_remember"] = value;}
}
protected void btnSubmit_OnClick(object sender, EventArgs e)
{
_userName = UserName.Text.Trim();
_password = Password.Text;
_remember = RememberMe.Checked;
if (string.IsNullOrEmpty(_userName))
{
FailureTextLogin.Text = "The server could not sign you in. The user name cannot be empty.";
return;
}
if (string.IsNullOrEmpty(_password))
{
FailureTextLogin.Text = "The server could not sign you in. The password cannot be empty.";
return;
}
var mem = (MyMembershipProvider) Utils.BaseMembershipProvider();
if (!mem.ValidateUser(_userName, _password))
{
FailureTextLogin.Text = "The server could not sign you in. Make sure your username and password are correct, and then try again.";
return;
}
var user = (MyMembershipUser) mem.GetUser(_userName, false);
if (user != null)
{
if (user.IsPasswordChanged)
{
phPart1.Visible = false;
phPart2.Visible = true;
}
else
{
SignInUser();
}
}
}
protected void ChangePasswordBtn_Click(object sender, EventArgs e)
{
var mem = (MyMembershipProvider) Utils.BaseMembershipProvider();
if (!mem.ValidateUser(_userName, CurrentPassword.Text))
{
FailureTextPasswordChange.Text = "Unable to change your password. Please make sure your current password is correct, and then try again.";
return;
}
if (NewPassword.Text.Length < mem.MinRequiredPasswordLength)
{
FailureTextPasswordChange.Text =
String.Format("Your new password is too short. Minimum length {0} characters.",
mem.MinRequiredPasswordLength);
return;
}
int num =0;
for (int i = 0; i < NewPassword.Text.Length; i++)
{
if (!char.IsLetterOrDigit(NewPassword.Text, i))
{
num++;
}
}
if (num < mem.MinRequiredNonAlphanumericCharacters)
{
FailureTextPasswordChange.Text =
String.Format("Your password requires {0} or more non-alphanumeric characters.", mem.MinRequiredNonAlphanumericCharacters);
return;
}
Match match = Regex.Match(NewPassword.Text, mem.PasswordStrengthRegularExpression);
if (!match.Success)
{
FailureTextPasswordChange.Text =
String.Format("Password must be at least {0} characters, containing upper, lowercase and numeric.",
mem.MinRequiredPasswordLength);
return;
}
var user = (MyMembershipUser) mem.GetUser(_userName, false);
if (user != null)
{
try
{
bool result = user.ChangePassword(CurrentPassword.Text, NewPassword.Text);
if (!result)
{
FailureTextPasswordChange.Text = "Unable to change your password.";
return;
}
}
catch (Exception ex)
{
FailureTextPasswordChange.Text = ex.Message;
return;
}
FailureTextLogin.Text =
"Successfully changed your password. Please login again with your new password.";
phPart1.Visible = true;
phPart2.Visible = false;
}
}
protected void Cancel_Click(object sender, EventArgs e)
{
//Clear everything. Reset login page.
_userName = "";
_password = "";
_remember = false;
phPart1.Visible = true;
phPart2.Visible = false;
FailureTextLogin.Text =
"Option to change password was cancelled. Unable to log you in unless password is changed.";
}
private void SignInUser()
{
var mem = (MyMembershipProvider) Utils.BaseMembershipProvider();
var role = (MyRoleProvider) Utils.BaseRoleProvider();
if (!mem.ValidateUser(_userName, _password))
{
return;
}
SecurityToken token = SPSecurityContext.SecurityTokenForFormsAuthentication
(new Uri(SPContext.Current.Web.Url),
mem.Name,
role.Name,
_userName,
_password,
_remember);
SPFederationAuthenticationModule.Current.SetPrincipalAndWriteSessionToken(token);
Response.Redirect(Request.QueryString["Source"]);
}
}
}
For completeness, my Utils class (found on the web is as follow)
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web;
using System.Web.Security;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Administration;
using Microsoft.SharePoint.Utilities;
namespace SharePoint.FBA.Code.HelperClasses
{
public class Utils
{
public static MembershipProvider BaseMembershipProvider()
{
return Membership.Providers[GetMembershipProvider()];
}
public static RoleProvider BaseRoleProvider()
{
return Roles.Providers[GetRoleProvider()];
}
public static MembershipProvider BaseMembershipProvider(SPSite site)
{
return Membership.Providers[GetMembershipProvider(site)];
}
public static RoleProvider BaseRoleProvider(SPSite site)
{
return Roles.Providers[GetRoleProvider(site)];
}
public static string GetMembershipProvider()
{
return GetMembershipProvider(SPContext.Current.Site);
}
public static string GetRoleProvider()
{
return GetRoleProvider(SPContext.Current.Site);
}
public static string GetRoleProvider(SPSite site)
{
// get role provider of whichever zone in the web app is fba enabled
SPIisSettings settings = GetFBAIisSettings(site);
return settings.FormsClaimsAuthenticationProvider.RoleProvider;
}
public static string GetMembershipProvider(HttpContext context)
{
using (SPSite site = new SPSite(SPUtility.GetPageUrlPath(context)))
{
return GetMembershipProvider(site);
}
}
public static string GetMembershipProvider(SPSite site)
{
// get membership provider of whichever zone in the web app is fba enabled
SPIisSettings settings = GetFBAIisSettings(site);
if (settings == null) return null;
return settings.FormsClaimsAuthenticationProvider.MembershipProvider;
}
private static SPIisSettings GetFBAIisSettings(SPSite site)
{
SPIisSettings settings = null;
// try and get FBA IIS settings from current site zone
try
{
settings = site.WebApplication.IisSettings[site.Zone];
if (settings.UseFormsClaimsAuthenticationProvider)
return settings;
}
catch
{
// expecting errors here so do nothing
}
// check each zone type for an FBA enabled IIS site
foreach (SPUrlZone zone in Enum.GetValues(typeof(SPUrlZone)))
{
try
{
settings = site.WebApplication.IisSettings[(SPUrlZone)zone];
if (settings.UseFormsClaimsAuthenticationProvider)
return settings;
}
catch
{
// expecting errors here so do nothing
}
}
// return null if FBA not enabled
return null;
}
}
}
It is a long post, but hopefully it provide someone else with the full answer, which was something I was unable to find online.