Customize “Sign in as Different User” for an expired Active Directory password

Here is the scenario.  Supposedly a password in AD is expired and we try to log in into the SharePoint site as an AD user.  You will get to see these exceptions, “You are not authorized to view this page” / “Unauthorized: Access is denied due to invalid credentials”.  The user may not  know that the password got expired in AD and he need to change the password before he can log in again.  Out of the box we don’t have option to verify the password against AD and let the user know that the password expired.

So, what can be done?  Let us go with customization after all :-) Here is the logic at top level view.  Firstly we need to compare the credentials entered against AD.  If the password is expired we need to display a friendly message saying “Your password is expired.  Please enter a new password to change and then log in again.”  Enter new password, change the password in AD and then allow the user to log in to the SharePoint site as a Windows Authenticated user.  How cool is it!!  

For this we need to customize OOB sign in. There are two options available.  Either create a sign in dialog quite similar to OOB sign in page (here we can use OOB “Sign in as Different User” option) or create a new sign in aspx page all together (Create a new custom sign in menu).  The out of box sign in menu is shown up from Welcome.ascx as MenuItemTemplate. 

 <SharePoint:MenuItemTemplate runat="server" id="ID_LoginAsDifferentUser"
                 Text="<%$Resources:wss,personalactions_loginasdifferentuser%>"
                 Description="<%$Resources:wss,personalactions_loginasdifferentuserdescription%>"
                 MenuGroupId="200"
                 Sequence="100"
                 UseShortId="true"
                 />

On clicking Sign in as Different User, it actually redirects to AccessDenied.aspx page found under layouts folder of 12 hive and displays the dialog box.  Customizing AccessDenied.aspx page may not be really feasible option if we want to do multiple operations on it like displaying custom message and then ask user to enter new credentials to change in AD, but yes it is possible!

So, the second option is to remove out of box sign in option and implement our custom sign in menu in it.  On click of custom sign in, it will redirect to our custom page ( CustomSignin.aspx)where we can implement our custom logic and display custom messages.

As said before, the sign in option is displayed from Welcome.ascx as MenuItemTemplate. This user control is used in the default.master page and application.master page.

<%@Register TagPrefix="wssuc"TagName="Welcome" src="~/_controltemplates/Welcome.ascx" %>

Since directly modifying any OOB pages are not supported, we need to use custom user control and custom master page for it.  For this, first we copy Welcome.ascx, name it customwelcome.ascx page and then place it under ..\12\TEMPLATE\CONTROLTEMPLATES\CustomWelcome.ascx.  Then we copy default.master page, name it customdefault.master and place it under ..\12\TEMPLATE\GLOBAL\customdefault.master.  In the custom master page make sure that you point to custom welcome user control.

<%@ Register TagPrefix="wssuc" TagName="Welcome" src="~/_controltemplates/CustomWelcome.ascx" %>

Then go to Master page gallery (make sure that the Publishing Infrastructure feature is activated), and upload our customdefault.master page.  Then in Look and Feel, master page, choose customdefault.master page for both Site and System master page.  This works fine for site and system pages.

But for application pages (like settings.aspx) it uses application.master page.  So we need to customize application.master page.  Do the same stuff as we did for default master page but the only question is how we will point to customapplication.master page instead of application.master page.  The best approach to customize application master page is to use custom HTTPModule.  Create a custom solution for HTTPModule build it and just place the dll under the bin folder of the web application found under  ..\Inetpub\wwwroot\wss\VirtualDirectories\.

 

Code snippet for Custom HTTPModule:

 void page_PreInit(object sender, EventArgs e)
  {
      Page page = sender as Page;
      if (page != null)
      {
          // Is there a master page defined?
          if (page.MasterPageFile != null)
          {
              // only change the application.master files as those are the offenders
              if (page.MasterPageFile.Contains("application.master"))
              {
                  page.MasterPageFile = "/_layouts/customapplication.master";
              }
          }
      }
  } 

Then in the web.config file of the web application add custom HTTPModule and do IIS reset.  Remember, we can also use custom HTTPModule for redirection of default.master page as well!

 <system.web>
 ...
 <httpModules>
        ...
              <add type="CustomHTTPModule.BaseModuleRewriter, CustomHTTPModule" name="BaseModuleRewriter" />
     </httpModules>
 </system.web>

Now you will notice  that the OOB sign in option will not be seen in the entire site. The next step is to add our Custom Sign in option.  To add our custom sign in option, we create a new feature and use CustomAction Element in elements.xml file.  There we can specify the respective .aspx page (CustomSignin.aspx) for redirection.

 

Sample Element XML File

 <CustomAction
       Id="8ADC90DD-294C-4706-97EE-171CEECD7073"
       GroupId="PersonalActions"
       Location="Microsoft.SharePoint.StandardMenu"
       Sequence="1000"
       Title="Custom SignIn"
       Description="Custom SignIn as a Different User "
       >
   <UrlAction Url="_layouts/CustomSignin.aspx"/>
 </CustomAction>

Install and then activate the feature and then we should be able to see our custom sign in option in the Menu.  The next step is to have our custom sign in page(CustomSignin.aspx) where we write code to check the entered credentials against AD.  If we find that the password has been expired, we display a friendly message to the user saying “Password expired.  Please change your password”.  In the custom sign in page we show text boxes for old password and new password / confirm password.  Now the question is how to get Windows token from the username and password?

For this, download advapi32.dll from https://www.dll-files.com/dllindex/dll-files.shtml?advapi32. Use LogonUser method of it.

Code Snippet to get Windows Token:

 [DllImport("advapi32.dll", SetLastError = true)]
 static extern bool LogonUser(
                       string principal,
                       string authority,
                       string password,
                       LogonSessionType logonType,
                       LogonProvider logonProvider,
                       out IntPtr token);
  
 enum LogonSessionType : uint
 {
     Interactive = 2,
     Network,
     Batch,
     Service,
     NetworkCleartext = 8,
     NewCredentials
 }
 enum LogonProvider : uint
 {
     Default = 0, // default for platform (use this!)
     WinNT35,     // sends smoke signals to authority
     WinNT40,     // uses NTLM
     WinNT50      // negotiates Kerb or NTLM
 }
  
 static void Main(string[] args)
 {
     IntPtr token = IntPtr.Zero;
  
     bool result = LogonUser("Chandrasekar",
                             "MyDomain",
                             "MyPassword",
                             LogonSessionType.Interactive,
                             LogonProvider.Default,
                             out token);
 }

The out parameter, token, will be our Windows Token.  Now, the challenge is how to pass Windows Token back to SharePoint so that it gets logged in as Windows Authenticated user.  Custom HTTPModule comes to the rescue again!

As now we have windows token, we can easily generate WindowsPrincipal object and then store it in a session.  Later we can use this session variable.

 if (result)
 {
     WindowsIdentity objWinIdentity = new WindowsIdentity(token);
     WindowsPrincipal objWinPrincipal = new WindowsPrincipal(objWinIdentity);
     Session.Add("AuthID", (object)objWinPrincipal);
 }

Here again we have two options.  Either store the WindowsPrincipal object in session and then use it Custom HTTPModule or store Username / Password in cookies and then generate WindowsPrincipal object later.  If we use Session, then the problem is in AuthenticateRequest Event, HttpContext.Current.Session is always going to be null and hence we will not get Session["AuthID"].

 void context_AuthenticateRequest(object sender, EventArgs e)
 {
    HttpSessionState httpSession = (HttpSessionState)HttpContext.Current.Session;
    //Here, session is always null !
    if (httpSession["AuthID"] != null)
    {
      HttpContext.Current.User = (System.Security.Principal.IPrincipal)httpSession["AuthID"];
    }
 }

But in PreRequestHandlerExecute Event which follows AuthenticateRequest Event, will have the session created and hence we would be able to get Session["AuthID"].  But can we use the code here to get Session value and assign it to HttpContext user?  The answer is NO!!  What will happen to authentication and autorization check. In the ASP.NET pipeline , these events happen before PreRequestHandlerExecute so it means that the authentication/authorization will be checked on the wrong user (the one stored into context.user).  So setting the User context in PreRequestHandlerExecute method will bo of no use, at least in our case.

So, we can use cookies instead.  Then use Response.Cookies in AuthenticateRequest Event and you will get Cookie values.  Since you will not be able to add WindowsPrincipal object in cookie,  you can add cookies having Username and password in it.  Then create WindowsPrincipal object from the Username and Password from the Cookie. 

 HttpCookie username = new HttpCookie("username");
 MyCookie1.Value = "";//Username value
 Response.Cookies.Add(username);
 //Same for setting cookie for password
  
 void context_AuthenticateRequest(object sender, EventArgs e)
 {
   HttpCookie usernameCookie = HttpContext.Current.Response.Cookies[“username”]; 
   HttpCookie passwordCookie = HttpContext.Current.Response.Cookies[“password”];
   //Generate WindowsPrincipal Object using username and password. 
   //Then set  HttpContext.Current.User to the generated WindowsPrincipal Object.
 }
  

For security reasons, make sure that you Encrypt / Decrypt Cookies.  You may use any of the algorithms available https://msdn.microsoft.com/en-us/library/system.security.cryptography.aspx

Last but not least,  What if the cookies are tampered?  Just to make sure that we have the right windows identity we can actually checked the decrypted value of cookie with the session value that we get in PreRequestHandlerExecute Event.  Make sure that the values match.  And that’s it!!  You have your custom sign in page where user can change password and log in back as Windows authenticated in SharePoint.