Saturday, August 20, 2011

Generate simplified alpha numeric passwords in SqlMembershipProvider

Pair of standard classes SqlMembershipProvider and SqlRoleProvider is often used in ASP.Net and Sharepoint applications for adding form based authentication. If you worked with FBA in Sharepoint then most probably you hear about CKS.FBA open source project which have ready for use components (web parts, templates, etc.) for adding into your Sharepoint site without additional development. Among with other components it has Password recovery web part (PasswordRecoveryWebPart class in CKS.FormsBasedAuthentication.dll assembly) which allows users who forgot their password to generate new one and get it by email. This is useful functionality, however passwords which are generated by standard components (see below) contains also punctuation marks (|./">!@#$%^&*()_-+=[{]};:<>|./?), e.g. “i=MK#{-?kwC5ol”. Often it is not convenient for end users. In this post I will show how to generate simplified passwords using only alphabetic and numeric characters. We will not also use characters which looks similar for end users and which are often reason of confusion: “i”, “1", “l”, “o”, “0”.

Internally this web part uses standard PasswordRecovery control. In order to generate new password it calls MembershipUser.ResetPassword() which in turn calls abstract method MembershipProvider.ResetPassword(). In SqlMembershiipProvider internally this method calls virtual method SqlMembershiipProvider.GeneratePassword():

   1: public virtual string GeneratePassword()
   2: {
   3:     return Membership.GeneratePassword((this.MinRequiredPasswordLength < 14) ?
   4:         14 : this.MinRequiredPasswordLength, this.MinRequiredNonAlphanumericCharacters);
   5: }

As you can see the minimal password length is 14. Also it pass MinRequiredNonAlphanumericCharacters to the static method Membership.GeneratePassword(). Msdn says about this parameter:

The minimum number of non-alphanumeric characters (such as @, #, !, %, &, and so on) in the generated password.

The keyword here is “minimum”. Often developers misunderstand it and try to pass zero in this parameter for getting only alpha numeric passwords. Lets check implementation of this method:

   1: public static string GeneratePassword(int length, int numberOfNonAlphanumericCharacters)
   2: {
   3:     string str;
   4:     int num;
   5:     if ((length < 1) || (length > 0x80))
   6:     {
   7:         throw new ArgumentException(SR.GetString("Membership_password_length_incorrect"));
   8:     }
   9:     if ((numberOfNonAlphanumericCharacters > length) || (numberOfNonAlphanumericCharacters < 0))
  10:     {
  11:         throw new ArgumentException(...);
  12:     }
  13:     do
  14:     {
  15:         byte[] data = new byte[length];
  16:         char[] chArray = new char[length];
  17:         int num2 = 0;
  18:         new RNGCryptoServiceProvider().GetBytes(data);
  19:         for (int i = 0; i < length; i++)
  20:         {
  21:             int num4 = data[i] % 0x57;
  22:             if (num4 < 10)
  23:             {
  24:                 // generate characters 0-9
  25:                 chArray[i] = (char) (0x30 + num4);
  26:             }
  27:             else if (num4 < 0x24)
  28:             {
  29:                 // generate characters A-Z
  30:                 chArray[i] = (char) ((0x41 + num4) - 10);
  31:             }
  32:             else if (num4 < 0x3e)
  33:             {
  34:                 // generate characters a-z
  35:                 chArray[i] = (char) ((0x61 + num4) - 0x24);
  36:             }
  37:             else
  38:             {
  39:                 // get a non alphanumeric character from the punctuations array
  40:                 chArray[i] = punctuations[num4 - 0x3e];
  41:                 num2++;
  42:             }
  43:         }
  44:         if (num2 < numberOfNonAlphanumericCharacters)
  45:         {
  46:             Random random = new Random();
  47:             for (int j = 0; j < (numberOfNonAlphanumericCharacters - num2); j++)
  48:             {
  49:                 int num6;
  50:                 do
  51:                 {
  52:                     num6 = random.Next(0, length);
  53:                 }
  54:                 while (!char.IsLetterOrDigit(chArray[num6]));
  55:                 chArray[num6] = punctuations[random.Next(0, punctuations.Length)];
  56:             }
  57:         }
  58:         str = new string(chArray);
  59:     }
  60:     while (CrossSiteScriptingValidation.IsDangerousString(str, out num));
  61:     return str;
  62: }

As you can see every time it inserts punctuation mark into password array it increases num2 variable (line 41). Then it checks if num2 is less then numberOfNonAlphanumericCharacters – it replaces letter or digit symbols by punctuation marks (lines 44-57). It means that whatever number you would pass in this parameter, the password anyway will contain non-alpha numeric symbols.

In order to generate simplified password we need to inherit SqlMembershipProvider and override its GeneratePassword() method (fortunately it is virtual):

   1: public class CustomMembershipProvider : SqlMembershipProvider
   2: {
   3:     private string[] characters =
   4:         new[]
   5:             {
   6:                 "a", "b", "c", "d", "e", "f", "g", "h", "j", "k", "m", "n",
   7:                 "2", "3", "4", "5", "6", "7", "8", "9",
   8:                 "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"
   9:             };
  10:  
  11:     public override string GeneratePassword()
  12:     {
  13:         string newPassword = string.Empty;
  14:         var rnd = new Random();
  15:  
  16:         int length = (this.MinRequiredPasswordLength < 14) ? 14 : this.MinRequiredPasswordLength;
  17:         for (int i = 0; i < length; i++)
  18:         {
  19:             newPassword += characters[rnd.Next(characters.Length)];
  20:         }
  21:         return newPassword;
  22:     }
  23: }

The implementation is quite simple: we defined array of allowed characters, excluding misleading characters (“i”, “1", “l”, “o”, “0”), and then use this array for password generation. As result your users will receive the following passwords: “pfwp9t42ret4jp”.

In order to use this custom membership provider you have to register it the in web.config:

   1: <membership defaultProvider="Custom">
   2:   <providers>
   3:     <add connectionStringName="..." enablePasswordRetrieval="false" enablePasswordReset="true"
   4: requiresQuestionAndAnswer="false" applicationName="/" requiresUniqueEmail="false" passwordFormat="Hashed
   5:  maxInvalidPasswordAttempts="100" minRequiredPasswordLength="1" minRequiredNonalphanumericCharacters="0"
   6: passwordAttemptWindow="10" passwordStrengthRegularExpression="" name="Custom"
   7: type="CustomMembershipProvider.CustomMembershipProvider, CustomMembershipProvider, Version=1.0.0.0, Culture=neutral, PublicKeyToken=..." />
   8:   </providers>
   9: </membership>

After this it will be used in your application.

Sunday, August 7, 2011

Sharepoint 2010 bug: incorrect wsdl of TaxonomyClientService.asmx web service

Recently I found interesting problem when work with TaxonomyClientService.asmx. This service can be used by client applications in order to work with Sharepoint managed metadata. As msdn says:

This Web service enables a client to interact with the managed metadata TermStore object and get the data through label matching, per Microsoft.SharePoint.Taxonomy.TermSet, or by GUID. Updates to the TermStore can also be made by adding a new Term object.

As many other OTB Sharepoint web services, TaxonomyClientService.asmx file is located in 14/ISAPI folder and is accessible by the following URL: http://example.com/_vti_bin/TaxonomyClientService.asmx (where http://example.com part belongs to some Sharepoint web site). When you enter this URL you will see the following web methods:

image

  • AddTerms
  • GetChildTermsInTerm
  • GetChildTermsInTermSet
  • GetKeywordTermsByGuids
  • GetTermSets
  • GetTermsByLabel

In order to use this web service we should perform usual steps: add service reference (or web reference) and use generated proxy. And here we got the problem: in generated proxy class all web methods exist except GetChildTermsInTerm and GetChildTermsInTermSet:

image

This was strange. The first thing I made is checked TaxonomyClientService class in Reflector to ensure that mentioned methods contains [WebMethod] attribute. They had it:

image

Then I checked wsdl of the TaxonomyClientService.asmx (added ?wsdl to the web service url) – and it didn’t contain the definition of the GetChildTermsInTerm and GetChildTermsInTermSet methods. So that was the problem.

As you probably know in Sharepoint wsd land disco files are located near asmx file in format *wsdl.aspx and *disco.aspx. So I checked TaxonomyClientServicewsdl.aspx – and, as I expected, definitions of GetChildTermsInTerm and GetChildTermsInTermSet methods were missing here. For me this looks like a bug.

Visual Studio uses these *wsdl.aspx files in order to generate web service proxy. Also IE uses it when you click on Service Description link on the web service page. Try to rename TaxonomyClientServicewsdl.aspx to TaxonomyClientServicewsdl.aspx_ (with leading underscore) and you will see that neither VS nor IE can show wsdl anymore. Rename it back and they will work again.

Ok, we know the reason, but what we should do before we will get official patch from MS? We need to fix it by ourselves. That’s how you can do it:

  1. Backup original wsdl file TaxonomyClientServicewsdl.aspx to TaxonomyClientServicewsdl.aspx_orig
  2. Copy TaxonomyClientService.asmx to 14/Template/Layouts/Test (but don’t copy *wsdl.aspx and *disco.aspx files). Doing by this you will get wsdl file generated dynamically based on actual asmx web methods.
  3. Use disco.exe tool from MS SDK in order to generate the correct wsdl of the web service using the new URL (or just open the following URL in browser http://example.com/_layouts/test/TaxonomyClientService.asmx?asmx):

    disco.exe http://example.com/_layouts/test/TaxonomyClientService.asmx
  4. It will generate 2 files: TaxonomyClientService.disco and TaxonomyClientService.wsdl
  5. Check that newly generated wsdl file contains definitions of GetChildTermsInTerm and GetChildTermsInTermSet methods
  6. Now we need compare original TaxonomyClientServicewsdl.aspx and generated TaxonomyClientService.wsdl file and copy missing parts from newly generated file into original file. Note, that you should not just replace original file with new one because it has additional code for adopting it for Sharepoint (Page directive and SPEncode.WriteHtmlEncodeWithQuote()). I used Beyond compare tool and just copy parts of code from wsdl file to TaxonomyClientServicewsdl.aspx which are related with missing methods.

After you will save changes in TaxonomyClientServicewsdl.aspx you should update reference in your VS project – and after that you will be able to use GetChildTermsInTerm and GetChildTermsInTermSet methods in the code:

image

I uploaded fixed version of TaxonomyClientServicewsdl.aspx file here.

Recently SP1 was released for Sharepoint 2010 however I don’t have it now to check whether or not this bug was fixed there. Will update this post as far as I will check it.

Saturday, August 6, 2011

Camlex.Net 2.4 is released

Today I released Camlex.Net 2.4 version. The following functionality was added:

  • Querying by user id (DataType.UserId):
   1: string caml = Camlex.Query().Where(x => x["Author"] ==
   2:     (DataTypes.UserId)"123").ToString();

will produce the following CAML:

   1: <Where>
   2:   <Eq>
   3:     <FieldRef Name="Author" LookupId="True" />
   4:     <Value Type="User">123</Value>
   5:   </Eq>
   6: </Where>
Note on the LookupId=”True” attribute. It indicates Sharepoint that you want to query using integer user id – not by user name.

Querying by user names is still supported of course. It works like in previous versions – you need to cast rvalue to (DataTypes.User):

   1: string caml = Camlex.Query().Where(x => x["Author"] ==
   2:     (DataTypes.User)"Firstname Lastname").ToString();

and you will get the following CAML:

   1: <Where>
   2:   <Eq>
   3:     <FieldRef Name="Author" />
   4:     <Value Type="User">Firstname Lastname</Value>
   5:   </Eq>
   6: </Where>
  • Querying by current user’s id:
   1: string caml = Camlex.Query().Where(x => x["Author"] ==
   2:     (DataTypes.Integer)Camlex.UserID).ToString();

will create a query for retrieving items which has Author field equal to current user:

   1: <Where>
   2:   <Eq>
   3:     <FieldRef Name="Author" />
   4:     <Value Type="Integer">
   5:       <UserID />
   6:     </Value>
   7:   </Eq>
   8: </Where>

Like many other features this release was initiated by feedback which we receive from Sharepoint developers who use Camlex.Net in their work. If you think that some feature will be useful for Camlex, don’t hesitate to discuss it on the Camlex discussions section on the Codeplex.

Friday, August 5, 2011

Enumerate sites using javascript client object model in Sharepoint

Recently I needed to enumerate all sub sites of the root site using javascript client model. As you probably know in order to perform calls using client object model in Sharepoint you need to create context, assign method call result to some local variable and then call load() method in order to get the data from Sharepoint. Looks easy. However there were several problems I would like to share.

First of all you are not able to use Sharepoint client object model in separate ASP.Net web application outside of the Sharepoint (because of cross-site scripting) – you can use it e.g. in application layouts page in context of some Sharepoint site: http://example.com/_layouts/foo.aspx. Also using javascript object model you can work only with site in which context you opened the page (in example above it is http://example.com). Other sites can not be accessed (also because of cross-site scripting). However even if you load the page in context of Sharepoint site you need to reference correct javascript files. As you probably know javascript client object model is defined in sp.js file in 14/Layouts folder. However if you will try to reference it using script tag:

   1: <script type="text/javascript" src="/_layouts/sp.js"></script>

you may get “Type is undefined” error. Then you can try to use ScriptManager and reference scripts using ScriptReference, or ScriptLink also without success. You may try to add additional scripts explicitly: 1033/init.js, 1033/core.js, sp.runtime.js, etc. and continue to get unclear javascript errors. Example which worked for me can be found here: Setting Up an Application Page for JavaScript. However you need to delete the following links in order to get it work:

   1: <script type="text/ecmascript" src="/_layouts/SP.Core.js" />
   2: <script type="text/ecmascript" src="/_layouts/SP.Debug.js" />
   3: <script type="text/ecmascript" src="/_layouts/SP.Runtime.Debug.js" />

Also you may want to change custom codebehind class to standard one:

   1: <%@ Page Language="C#" MasterPageFile="~/_layouts/applicationv4.master" Inherits="Microsoft.SharePoint.WebControls.LayoutsPageBase" %>

After that page should be loaded without javascript errors. Let’s now return to exact post topic: enumerate sites using client object model. There is OTB SP.WebCollection class for this. What method you need for any collection? Yes, method which returns total number of items in collection. Interesting that msdn doesn’t say anything about it: neither in methods nor in properties. However this method exists: get_count() (I found it by enumerating properties and methods using “for (w in webs)” statement).

The final code for the page is the following:

   1: <%@ Assembly Name="Microsoft.SharePoint.ApplicationPages, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c"%>
   2: <%@ Assembly Name="Microsoft.SharePoint, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c"%>
   3: <%@ Import Namespace="Microsoft.SharePoint.ApplicationPages" %>
   4: <%@ Register Tagprefix="SharePoint" Namespace="Microsoft.SharePoint.WebControls" 
   5:   Assembly="Microsoft.SharePoint, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
   6: <%@ Register Tagprefix="Utilities" Namespace="Microsoft.SharePoint.Utilities" 
   7:   Assembly="Microsoft.SharePoint, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
   8: <%@ Register Tagprefix="asp" Namespace="System.Web.UI" 
   9:   Assembly="System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" %>
  10: <%@ Import Namespace="Microsoft.SharePoint" %>
  11: <%@ Assembly Name="Microsoft.Web.CommandUI, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
  12: <%@ Page Language="C#" MasterPageFile="~/_layouts/applicationv4.master" Inherits="Microsoft.SharePoint.WebControls.LayoutsPageBase" %>
  13:  
  14: <asp:Content ID="PageHead" ContentPlaceHolderID="PlaceHolderAdditionalPageHead" runat="server">
  15:  
  16: <script type="text/javascript">   1: 
  17:     function onWebsLoaded(sender, args) {
  18:         for (var i = 0; i < this.webs.get_count(); i++) {
  19:             alert(this.webs.itemAt(i).get_title());
  20:         }
  21:     }
  22:  
  23:     function onWebLoaded(sender, args) {
  24:         var clientContext = new SP.ClientContext.get_current();
  25:  
  26:         this.webs = this.oWebsite.get_webs();
  27:         clientContext.load(this.webs);
  28:         clientContext.executeQueryAsync(Function.createDelegate(this, this.onWebsLoaded), Function.createDelegate(this, this.onQueryFailed));
  29:     }
  30:  
  31:     function onQueryFailed(sender, args) {
  32:         alert('request failed ' + args.get_message() + '\n' + args.get_stackTrace());
  33:     }
  34:  
  35:     function retrieveWebSites() {
  36:         var clientContext = new SP.ClientContext.get_current();
  37:         this.oWebsite = clientContext.get_web();
  38:         clientContext.load(this.oWebsite);
  39:         clientContext.executeQueryAsync(Function.createDelegate(this, this.onWebLoaded), Function.createDelegate(this, this.onQueryFailed));
  40:     }
  41: </asp:Content>
  42:  
  43: <asp:Content ID="Main" ContentPlaceHolderID="PlaceHolderMain" runat="server">
  44:  
  45:     <SharePoint:FormDigest ID="FormDigest1" runat="server"></SharePoint:FormDigest>
  46:  
  47:     <input type="button" id="btn" value="Show all webs" OnClick="retrieveWebSites()" />
  48:  
  49: </asp:Content>

So we added a button “btn” and handler “retrieveWebSites” for onclick event. In this handler we open context and load root web site (lines 36-39). In the onSuccess handler we load sub sites and in second onSuccess handler we enumerate all sites using get_count() method.

Hope it will help you with understanding of client object model.