5233 Views // 0 Comments // Not Rated

The Hybrid Provider - Power Tools

Something that's been passed over in all of my Hybrid Provider work has been some of the cool little tweaks I've come up with to get some of the "supporting characters" to work. If custom SharePoint authentication plays the lead role, then login pages, administrative web parts, and SQL user management tools might not be center stage, but they are certainly crucial.

I've uploaded a ZIP file called "Hybrid Provider - Power Tools" to the Hybrid Provider CodePlex project site So what I'd like to do is give a few descriptions of how these pieces work, and the hacks (er...customizations) I've had to do to get them this far.

  • Login Page
  • Admin Tools
    • Create User
    • Remove User
    • Forgot Password
    • Change Password
    • Reset Password
  • Active Directory Layer

Login Page - UserLogin.aspx

As you'll find with most of this functionality, these pages and controls are essentially wrappers around the standard ASP.NET 2.0 "user" user controls. The login page's bread and butter is, believe it or not, a Login control! (As I've said before, don't rename this to Login.aspx - your page's class will collide with the Login control's class, and ASP.NET will barf.)

All you have to do to hook up a custom provider to a custom login page is set the "MembershipProvider" property of the user control equal to the name of the class of the provider (for example, "HybridProvider"). Then via reflection magic, everything works. (Note: this will be the case for all wrapped ASP.NET "user" user controls mentioned in this article, so I won't have to repeat myself every time.)

The interesting part of this user control is a harmless little "remember be" check box. What this does, on the surface, when checked, is create a cookie on the user's machine that automatically logs them in until they explicitly log out or change to a different user. However, it does a lot more than that behind the scenes.

"Client Integration" in SharePoint means that installed Office applications (Word, Excel, PowerPoint, Access, etc.) can be called upon to open content in SharePoint, and even modify it offline. With Windows Auth, this isn't an issue. However, with forms-based auth, browsers and client apps cannot communicate without the aforementioned cookie.

Therefore, SQL users of the Hybrid Provider will need to check this box if they plan to look at a list in the datasheet view, open a Word document form a form library, or export a list to Excel. And that sucks. So we need to make it un-suck with some JavaScript that automatically checks and hides this box.

This way, users don't have to perform any extra steps when logging in to get all of the Office integration intrinsic to a Windows user's client experience in SharePoint. The only drawback, I guess, would be that if someone on that machine ever needed to get to the login page after the first time they enter the portal, they'd have to explicitly log out. Not a bad tradeoff, since they can do this in SharePoint 2007 from any page.

Here's the markup for UserLogin.aspx. Lines 7 and 12 perform the hiding and checking, respectively:

Code Listing 1

  1. <%@ Page Language="C#" AutoEventWireup="true" CodeBehind="UserLogin.aspx.cs" Inherits="Catalyst.SharePoint.Web.UserLogin" %>
  2. <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 0 Transitional//EN" "http://www.worg/TR/xhtml1/DTD/xhtml1-transitional.dtd">
  3. <html xmlns="http://www.worg/1999/xhtml" >
  4. <head id="hdHeader" runat="server">
  5. <title>Hybrid Provider Login</title>
  6. </head>
  7. <body onload="document.getElementById('logLogin_RememberMe').style.visibility = 'hidden';">
  8. <form id="frmHybridProviderLogin" runat="server">
  9. <table style="text-align: center; width: 100%;">
  10. <tr>
  11. <td>
  12. <asp:Login ID="logLogin" runat="server" MembershipProvider="HybridProvider" RememberMeSet="true" RememberMeText="" />
  13. </td>
  14. </tr>
  15. </table>
  16. </form>
  17. </body>
  18. </html>

Admin Tools - CreateUsers.ascx

All of these controls are intended to be hosted in SmartParts that are on an administrative web part page somewhere in your portal. This page should be locked down, obviously, in case a status meeting goes bad for example, and someone runs back to their computer and deletes their manager's account.

The first control, CreateUsers.ascx, is actually used tocreate. Talk about self-documenting code! In the spirit of the others, it wires up the Hybrid Provider as it's MembershipProvider to add a new account to SQL, respecting properties of the Hybrid Provider, such as password strength rules, requirements of question and answer, etc.

There are two little enhancements I've made to the base functionality of this control. The first one is some simple code behind the wrapped ASP.NET CreateUserWizard's OnCreatedUser event. This code adds the newly-created user to the root web's SiteUsers collection, so that when they log in, they are at least authorized on the home page and don't have to see any access denied error messages. Here's that code:

Code Listing 2

  1. protected void OnCreatedUser_Click(object sender, EventArgs e)
  2. {
  3. //open site
  4. using (SPSite site = new SPSite("http://server"))
  5. {
  6. //open web
  7. site.AllowUnsafeUpates = true;
  8. using (SPWeb web = site.RootWeb)
  9. {
  10. //add user
  11. web.AllowUnsafeUpates = true;
  12. web.SiteUsers.Add(string.Format("hybridprovider:{0}", this.cuwUser.UserName), this.cuwUser.Email, this.cuwUser.UserName, string.Format("Added via Create Users on: {0}.", DateTime.Now));
  13. web.Update();
  14. }
  15. }
  16. }

On Line #9, this.cuwUser is the name of the CreateUserWizard control.

The other enhancement is a trick to get around an annoying aspect of this control. By default, after a new account is successfully created, the control hides all its fields and shows a continue button. Clicking on this button, conveniently, does nothing. There is a property on the CreateUserWizard control called ContinueButtonDestinationPageURL that, when set, will redirect to that page when the button is clicked.

BuBut what if it needs to be dynamic (especially in SharePoint, when we can't anticipate what our URL will be at design time)? So to deal with this dynamically, I handle another event in code: OnContinueButtonClick. The event handler is one cute line of code:

Code Listing 3

  1. protected void OnContinueButtonClick_Click(object sender, EventArgs e)
  2. {
  3. //redirect to current page
  4. this.Response.Redirect(this.Request.Url.AbsolutePath);
  5. }

Why a redirect? Because this will guarantee two things:p>

  • The control will reset itself.
  • Any other admin controls on this page that have a list of SQL users will have their page load events fire, thus this new account will immediately be usable in them.

Admin Tools - RemoveUser.ascx

Ashes to ashes...dust to dust: the admin giveth; the admin shalt deleteth your account! This control literally does the opposite of CreateUsers: deletes the user from SharePoint, deletes the account from SQL, then refreshes the page.

There used to be a separate control that removes users from groups, (since there's no single screen that audits a user's group membership in SharePoint) but in the spirit of deletion, I decided to combine it with this one. So when you pick a user, you have the option to remove it from any SharePoint group they're in, or delete the account altogether.

The way it works is that there are two drop downs: one with SQL users and one with AD. Based on the selection of a radio button with an option for both, only one of these drop downs will ever be visible at a time. The main difference is that if an AD user is selected (a "Consultant" in the code), the button to delete the user will be disabled, since we don't want to be removing objects from Active Directory! SQL users, ("Clients") however, can be deleted.

But in both situations, a check box list will be built containing all of the SharePoint groups the selected user is a member of. Check off the ones to remove from and click the button and they are out! This list can be filtered by a semicolon-delimited list of strings stored as an AppSetting in the web.config file with a key of "GroupNamesToIgnore." I parse this string, and the name of any group the user is in that matches any substring of "GroupNamesToIgnore" will not be in the check box list.

This is useful, again, to prevent administrators from accidentally de-commissioning themselves, and since there are a few internal group memberships I didn't want to mess with.

They call to Membership.DeleteUser takes in the account same of the selected SQL user, and a boolean telling it to delete all user data. This way, we don't have to worry about cleaning up any profile or role data in SQL; everything is blown away when this user's account is deleted.

Finally, as a side note, there is a lot of code in this control that is specific to the portal I first created the Hybrid Provider for. If you are going to use this in your SharePoint environment, it might be easier to rebuild it from scratch, using mine as a reference.

Admin Tools - ForgotPassword.ascx

I was amazed to learn that, despite how pervasive computers have become in our world, people still forget their passwords all the time ! Unless they are typing them in every day or have a Post-It on their monitor with passwords written on it, it will quickly be garbage-collection from their brains. And since people (more specifically, SQL users) don't necessary hit a portal every day, you had better have a mechanism in place for quickly helping users recover, change (next section), or reset (next next section) their password!

ForgotPassword.ascx is the "first line" of defense against forgotten passwords here. This control is wired up to the HybridProvider, and will Email a user their password. Since people who would need this have forgotten their password, it has to sit on the login page, or else they won't be able to get to it.

The way it works is that a user name is typed into a box, and a button is clicked. It'll display a message if the user could not be found or if the password could not be retrieved. The later occurs for two different reasons: the custom provider is not configured to allow password retrieval (read my posting about that here) or there was an error (I covered a probable cause of such an error here).

Assuming your Hybrid Provider and SharePoint Email are all set up, that's about it. However, I did make one cool customization of the wrapped PasswordRecovery ASP.NET "user" user control. It's actually not even really a customization, just an implementation of available functionality.

This control allows you to wire up a text document that contains the body of the Email that will be sent to the user. Here's how to set this up in the markup:

Code Listing 4

  1. <asp:PasswordRecovery ID="prForgotPassword" runat="server" MembershipProvider="HybridProvider" UserNameInstructionText="" SubmitButtonText="Get Password" SubmitButtonType="Button" MailDefinition-Subject="Forgot Password" MailDefinition-BodyFileName="/ForgotPasswordBody.txt">
  2. ...
  3. </asp:PasswordRecovery>

The MailDefinition-BodyFileName property, set to "/ForgotPasswordBody.txt," will read in a text file with this name that is sitting in the same directory as the user control. Additionally, you can specify certain substitutions in this file to insert user-specific information. ASP.NET has two right now:p>

  • <% UserName %>
  • <% Password %>

Placing either of these tokens on your text file will cause the ForgotPassword control to replace them with the actual user name and/or password of the user requesting their password.

Admin Tools - ChangePassword.ascx

Change password, which embodies the ASP.NET ChangePassword "user" user control, is actually the simplest one of the bunch. It allows a user, once logged in, to change their password to someone else. All of the customizations in this control I've already mentioned: wiring up the HybridProvider, and redirecting back to itself on a successful change.

However, one quick additional customization to mention is that there's a cancel button I saw fit to hide. In order to get it looking correctly in SharePoint, I had to set two properties in the ChangePassword control's markup to a value of "0px" - these being CancelButtonStyle-Width and CancelButtonStyle-Height.

Admin Tools - ResetPassword.ascx

This brings us to ResetPassword.ascx - the last line of defense against forgotten passwords, and indeed the most complicated. This is an administrative tool that builds a drop down of SQL users, and allows an admin to type in a password for the selected user, update it, and optionally Email to new password to the user.

Hopefully, you're thinking one of two things. Either: "Wow! That's impossible! How do you deal with the password encryption? You're a really great developer / person!" Or: "You fucking hacked it, didn't you, jerkface?"

I did hack it - but not really. It's not a hack because I use the SQL membership provider functionality (and one custom stored proc) and therefore don't have to mess with the SALT, the machine key, or any of that. Believe me, before I came up with this tactic, I was tempted to try the encryption myself...glad I didn't!

Now of course, I tried using the out-of-the-box methods (in the Membership static class) first. These however generally blow up depending on your password format and other provider settings. Sometimes, if you are using clear text for example, they'll work, but you won't have all the necessary information. You need the current password of the user to pass to the "ChangePassword" method, and you need their secret question's answer to pass to the "GetPassword" method to get the current password - and that all is, as previously stated - if these puppies work in the first place.

So I did it myself. I stumbled onto this hack when trying to update user's passwords directly in the database (an awful eventuality, I know, and therefore the reason these tools were created in the first place). I noticed that I could update the encrypted text of a known password (and its SALT) from an old user and a new user's row in the aspnet_membership table of the ASP.NET user database and then successfully log in with those credentials.

So the logic of this control is as follows:

  1. Create a temporary user with the password typed by the admin in the textbox.
  2. Pass the name of this temp user (which is <new guid>@<new guid>.com), along with the name of the user whose password we are updating (from the drop down of SQL users) to a proc.  
  3. This proc will select the password and SALT from the temp user, and set it thusly for the updating one.  It will also make sure the user has the correct password format, and reset any failed login attempts.
  4. Delete the temporary user.  
  5. [Optional] Email the user their new password if the corresponding check box to this option is selected.

The Email portion is interesting. I want the Email sent to the user to be the same as the one they would have gotten from the login page had they used ForgotPassword.ascx to retrieve their password. Therefore, I need to programmatically do what the ASP.NET PasswordRecovery "user" user control does. This includes implementing the aforementioned replacements. Here's that snippet:

Code Listing 5

  1. //create a mail definition
  2. MailDefinition md = new MailDefinition();
  3. md.BodyFileName = "/ForgotPasswordBody.txt";
  4. md.Subject = "SharePoint - Password Changed";
  5. md.IsBodyHtml = true;
  6. md.From = "admin@portal.com";
  7. //replacements
  8. Dictionary<string, string> replacements = new Dictionary<string, string>();
  9. replacements.Add("<% Password %>", this.txtPassword.Text);
  10. replacements.Add("<% UserName %>", this.ddlUsers.SelectedItem.Text);
  11. //send email
  12. new SmtpClient().Send(md.CreateMailMessage(this.ddlUsers.SelectedValue, replacements, this));

Lines #8 - 11 implement the replacements. I said previously that these were the only two supported by PasswordRecovery.ascx. However, I wonder (have not tried it) if, in code, we can specify as many replacements as we want. That would be a cool experiment. But I don't need it, so I leave it to you.

Line #1 requires a reference to System.Web.UI.WebControls; #14 needs System.Net.Mail. Of course, for this technique and for PasswordRecovery.ascx to work, you'll need to make sure your web.config is properly set up to transmit Email. SharePoint's Email doesn't necessarily need to be configured to make this happen; we're going straight to ASP.NET. Here's what the web.config section should look like:

Code Listing 6

  1. <system.net>
  2. <mailSettings>
  3. <smtp deliveryMethod="Network" from="admin@portal.com">
  4. <network host="mail.portal.com" />
  5. </smtp>
  6. </mailSettings>
  7. </system.net>

You can also specify the port, credentials, etc. to customize the requires for your network. This is the bare bones configuration.

That's it for the user controls! Time to get all LDAP-y.

Active Directory Layer - Catalyst.SharePoint.ActiveDirectory.dll

The final supporting character in the Hybrid Provider's cast is the interaction between all of these sub systems and the actual system of record for user data - Active Directory. AD always seems like a hot topic, since System.DirectoryServices is always an adventure to code against. I just get the impression that programming against it is edgy and risque.

This namespace is such a light wrapper around all the underlying COM it abstracts, that it's translucent; you can actually see all the evil COM right though the DLL's skin. Whenever you find yourself Googleing hexadecimal error messages, you know you're digging deeply into AD code.

This makes the experience less than exhilarating, but the end result is pretty cool. Again: there's just something about coding against AD - it makes me feel a bit godlike, whereby changing someone in my company's last login date gives me some type of power over his or her soul.

Anyways, I've moved all AD code from around the Hybrid Provider into its own assembly. If you look at the code, you'll see that it's organized as a series of more and more granular helper methods. For example, the static class ADHelpers makes extensive calls into itself to do common tasks, such as get a DirectoryEntry object (the entry point into an AD forest) or get a property from a search result, etc.

Again, the best way to learn this is to look at the code. However, I'll call out a few things here to get you started:

  • GetDirectoryEntry returns a DirectoryEntry object specific to the path you give it.  If you give it the "root" (something like LDAP://<domain controller name>), you'll get everything.  But it you want a certain object, like a user, you can give it the path directly to the object.  There are overloads that try to read the required credentials from the application's configuration file; others take it in in case there is no config file available.
  • LDAP paths are case sensitive.  Always use "LDAP://" to start (in upper case).
  • GetPropertyFromOtherProperty is a quick way to query for one property of a user based on another.  A common example of this is if you have a SID of a user, and need their Email address.
  • ADUser is a small class that allows you to "personify" an active directory user as the sum of the properties available in your farm.  They have strongly-typed properties for the "big ones" (Id, UserName, etc.) and then a dictionary to hold all properties.  This class allows us to load up an AD user, then manipulate it "off line" (ie - NOT directly in AD code).  
  • SetProperties can be used to save an ADUser back to AD.

Well, that's it for the Hybrid Provider Power Tools. Have fun!

No Tags

No Files

No Thoughts

Your Thoughts?

You need to login with Twitter to share a Thought on this post.