Showing posts with label FBA. Show all posts
Showing posts with label FBA. Show all posts

Monday, September 4, 2017

Sliding session for Sharepoint 2013 with FBA and persistent cookies

Sliding session allows user to use site without being reauthenticated if last action was done less than configured session lifetime. In Sharepoint 2013 FBA the following parameters of security token service config are used for setting session lifetime:

  • CookieLifetime
  • FormsTokenLifeTime
  • LogonTokenCacheExpirationWindow

They are well described in the following article SharePoint 2013 authentication lifetime settings and I won’t repeat it here. The problem is that when you use persistent cookies (i.e. those which are stored on client’s side) only CookieLifetime are actually used (to be more precise, FormsTokenLifeTime is used for setting initial ValidTo value for session security token). In addition to that sliding sessions doesn’t work by default, i.e. regardless of whether user made actions on the site or not he will be logged out after cookies will be expired. Persistent cookies can be set e.g. if user checked “Remember Me” checkbox on the login page:

   1: private bool AuthenticateFormsUser(Uri context, string username, string pwd,
   2:     bool rememberMe)
   3: {
   4:     if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(pwd))
   5:     {
   6:         return false;
   7:     }
   8:  
   9:     try
  10:     {
  11:         var formsAuthOption = SPFormsAuthenticationOption.None;
  12:         var tokenType = SPSessionTokenWriteType.WriteSessionCookie;
  13:         if (rememberMe)
  14:         {
  15:             formsAuthOption = SPFormsAuthenticationOption.PersistentSignInRequest;
  16:             tokenType = SPSessionTokenWriteType.WritePersistentCookie;
  17:         }
  18:  
  19:         var authProvider = GetAuthProvider(SPContext.Current.Site);
  20:         var securityToken = SPSecurityContext.SecurityTokenForFormsAuthentication(
  21:             context,
  22:             authProvider.MembershipProvider,
  23:             authProvider.RoleProvider,
  24:             username,
  25:             pwd,
  26:             formsAuthOption);
  27:  
  28:         var fam = SPFederationAuthenticationModule.Current;
  29:         fam.SetPrincipalAndWriteSessionToken(securityToken, tokenType);
  30:         return true;
  31:     }
  32:     catch (Exception)
  33:     {
  34:         return false;
  35:     }
  36: }

Here on lines 12-16 code checks whether rememberMe parameter is true and if yes uses persistent cookies.

So is it possible to have sliding expiration sessions when persistent cookies are used? The answer is yes, but in order to do that we will need custom HTTP module which will renew token on each request:

   1: public class SlidingSessionModule : IHttpModule
   2: {
   3:     public void Init(HttpApplication context)
   4:     {
   5:         FederatedAuthentication.SessionAuthenticationModule.SessionSecurityTokenReceived +=
   6:             SessionAuthenticationModule_SessionSecurityTokenReceived;
   7:     }
   8:  
   9:     private void SessionAuthenticationModule_SessionSecurityTokenReceived(object sender,
  10:         SessionSecurityTokenReceivedEventArgs e)
  11:     {
  12:         try
  13:         {
  14:             if (e == null)
  15:             {
  16:                 return;
  17:             }
  18:             var sessionToken = e.SessionToken;
  19:             if (sessionToken == null)
  20:             {
  21:                 return;
  22:             }
  23:             if (claimsPrincipal == null)
  24:             {
  25:                 return;
  26:             }
  27:  
  28:             TimeSpan cookieLifetime = TimeSpan.FromSeconds(0);
  29:             SPSecurity.RunWithElevatedPrivileges(
  30:                 () =>
  31:                     {
  32:                         cookieLifetime = Microsoft.SharePoint.Administration.Claims.
  33:                             SPSecurityTokenServiceManager.Local.CookieLifetime;
  34:                     });
  35:  
  36:             DateTime utcNow = DateTime.UtcNow;
  37:             DateTime validFrom = utcNow;
  38:             DateTime validTo = utcNow + cookieLifetime;
  39:             var sam = FederatedAuthentication.SessionAuthenticationModule;
  40:             e.SessionToken = sam.CreateSessionSecurityToken(claimsPrincipal,
  41:                 sessionToken.Context, validFrom, validTo, sessionToken.IsPersistent);
  42:             e.ReissueCookie = true;
  43:         }
  44:         catch (Exception x)
  45:         {
  46:             // log
  47:         }
  48:     }
  49:  
  50:     public void Dispose()
  51:     {
  52:     }
  53: }

In the module we subscribe on SessionAuthenticationModule.SessionSecurityTokenReceived event (lines 5-6) and in event handler we renew token with extended ValidFrom and ValidTo properties (lines 36-42) which are set from CookieLifetime property of security token service config (lines 29-34) so you may continue configure it from PowerShell.

Then we need to install this module by adding dll to the GAC and the following line to the web.config <modules> section:

   1: <modules>
   2:   ..
   3:   <add name="SlidingSessionModule"
   4:     type="SlidingSessionModule.SlidingSessionModule, SlidingSessionModule, Version=1.0.0.0, Culture=neutral, PublicKeyToken=..." />
   5: </modules>

After that you will have sliding sessions with persistent cookies for Sharepoint FBA.

Thursday, April 6, 2017

Problem with SPWeb.EnsureUser method and FBA users with claims based authentication in Sharepoint

If you need to perform some action on FBA user in your Sharepoint site where claims authentication is used from outside of Sharepoint context (e.g. from console application) you may face with the following issue: when you will call web.EnsureUser(userName) method it will throw exception:

Specified user ‘username’ not found

There are several things which have to be done in order to make it possible to work with FBA users without Sharepoint context with claims based authentication:

1. Fake HTTP context after you get instance of SPWeb:

   1: HttpRequest request = new HttpRequest("", web.Url, "");
   2: HttpContext.Current = new HttpContext(request,
   3:     new HttpResponse(new StringWriter()));
   4: HttpContext.Current.Items["HttpHandlerSPWeb"] = web;

2. Use user name in full claims format, i.e.:

   1: var user = web.EnsureUser("i:0#.f|mymembershipprovider|username");

where instead of mymembershipprovider and username you should use your own membership provider name and user name.

3. The most tricky thing: from web.config of your FBA site zone you need to copy the following sections to the app.config of your console application:

  • connectionStrings
  • system.web/membership
  • system.web/roleManager

e.g.:

   1: <connectionStrings>
   2:   <add name="MyConnStr" connectionString="..." />
   3: </connectionStrings>
   4: <system.web>
   5:   <membership defaultProvider="i">
   6:     <providers>
   7:       <add name="i" type="Microsoft.SharePoint.Administration.Claims.SPClaimsAuthMembershipProvider,
   8: Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" />
   9:       <add connectionStringName="MyConnStr" name="MyMembershipProvider" ... />
  10:     </providers>
  11:   </membership>
  12:   <roleManager defaultProvider="c" enabled="true" cacheRolesInCookie="false">
  13:     <providers>
  14:       <add name="c" type="Microsoft.SharePoint.Administration.Claims.SPClaimsAuthRoleProvider,
  15: Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" />
  16:       <add connectionStringName="MyConnStr" applicationName="/" name="MyRoleProvider" ... />
  17:     </providers>
  18:   </roleManager>
  19: </system.web>

Here is the full working C# code which allows to get FBA user from console application:

   1: using (var site = new SPSite("http://example.com"))
   2: {
   3:     using (var web = site.OpenWeb())
   4:     {
   5:         web.AllowUnsafeUpdates = true;
   6:         HttpRequest request = new HttpRequest("", web.Url, "");
   7:         HttpContext.Current = new HttpContext(request,
   8:             new HttpResponse(new StringWriter()));
   9:         HttpContext.Current.Items["HttpHandlerSPWeb"] = web;
  10:  
  11:         var user = web.EnsureUser("i:0#.f|mymembershipprovider|username");
  12:         ...
  13:     }
  14: }

Saturday, August 27, 2016

One issue with custom login page with claims-based authentication in Sharepoint

One of our sites was migrated from Sharepoint 2007 to Sharepoint 2013. In 2007 it used FBA with custom login page. After migration user noticed strange behavior: sometime Sharepoint returned empty page or HTTP 500 Internal server error. During checking Sharepoint logs the following error was found there:

Exception of type 'System.ArgumentException' was thrown.  Parameter name: encodedValue 
at Microsoft.SharePoint.Administration.Claims.SPClaimEncodingManager.DecodeClaimFromFormsSuffix(String encodedValue)   
at Microsoft.SharePoint.Administration.Claims.SPClaimProviderManager.GetProviderUserKey(IClaimsIdentity claimsIdentity, String encodedIdentityClaimSuffix)   
at Microsoft.SharePoint.Administration.Claims.SPClaimProviderManager.GetProviderUserKey(String encodedIdentityClaimSuffix)   
at Microsoft.SharePoint.Utilities.SPUtility.GetFullUserKeyFromLoginName(String loginName)   
at Microsoft.SharePoint.SPGlobal.CreateSPRequestAndSetIdentity(SPSite site, String name, Boolean bNotGlobalAdminCode, String strUrl, Boolean bNotAddToContext, Byte[] UserToken, SPAppPrincipalToken appPrincipalToken, String userName, Boolean bIgnoreTokenTimeout, Boolean bAsAnonymous)   
at Microsoft.SharePoint.SPWeb.InitializeSPRequest()   
at Microsoft.SharePoint.SPWeb.EnsureSPRequest()   
at Microsoft.SharePoint.WebControls.SPControl.EnsureSPWebRequest(SPWeb web)   
at Microsoft.SharePoint.WebControls.SPControl.SPWebEnsureSPControl(HttpContext context)   
at Microsoft.SharePoint.ApplicationRuntime.BaseApplication.Application_PreRequestHandlerExecute(Object sender, EventArgs e)   
at Microsoft.SharePoint.ApplicationRuntime.SPRequestModule.PreRequestExecuteAppHandler(Object oSender, EventArgs ea)   
at System.Web.HttpApplication.SyncEventExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute()   
at System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously)

When web application was migrated to 2013 it started to use claims-based authentication. Custom login page was re-implemented using the following authentication logic (there are many examples of custom login page for FBA when claims-based authentication is used. Most of them use more or less the same logic):

   1: var formsAuthOption = SPFormsAuthenticationOption.None;
   2: var tokenType = SPSessionTokenWriteType.WriteSessionCookie;
   3: if (rememberMe)
   4: {
   5:     formsAuthOption = SPFormsAuthenticationOption.PersistentSignInRequest;
   6:     tokenType = SPSessionTokenWriteType.WritePersistentCookie;
   7: }
   8:  
   9: var settings = site.WebApplication.IisSettings[SPContext.Current.Site.Zone];
  10: var authProvider = settings.FormsClaimsAuthenticationProvider;
  11: var securityToken =
  12:     SPSecurityContext.SecurityTokenForFormsAuthentication(
  13:     context,
  14:     authProvider.MembershipProvider,
  15:     authProvider.RoleProvider,
  16:     username,
  17:     pwd,
  18:     formsAuthOption);
  19:  
  20: var fam = SPFederationAuthenticationModule.Current;
  21: fam.SetPrincipalAndWriteSessionToken(securityToken, tokenType);
  22:  
  23: FormsAuthentication.SetAuthCookie(username, false);

On lines 9-21 we authenticate user via FBA claims authentication provider (SPIisSettings.FormsClaimsAuthenticationProvider). If provided user credentials are incorrect, call to SPSecurityContext.SecurityTokenForFormsAuthentication() method will throw exception. If credentials are correct it will set FedAuth authentication cookies.

On line 23 there is call to FormsAuthentication.SetAuthCookie() method which sets previously used .ASPXAUTH cookies used by ASP.Net FBA authentication. Developer who implemented login page told that it was left there for backward compatibility, but was not able to remember which exact components required this cookies.

And as it turned out presence of both FedAuth and .ASPXAUTH cookies caused mentioned exception in SPClaimEncodingManager.DecodeClaimFromFormsSuffix(). When call to FormsAuthentication.SetAuthCookie() was removed, error disappeared. Hope that information will help someone.

Wednesday, April 6, 2016

One workaround when People picker doesn’t resolve FBA users in Sharepoint 2013

There are many guides of how to configure FBA in Sharepoint 2013 (e.g. here). One of the common problem with FBA in Sharepoint is that People picker doesn’t resolve FBA user names. How to fix this issue? Let’s use assumption that you followed configuration guide mentioned above and did all steps described there.

One of the most common solutions for this problem is that you need to add necessary permissions for identity of application pool of your web application to FBA database:

  • aspnet_Membership_FullAccess
  • aspnet_Personalization_FullAccess
  • aspnet_Profile_FullAccess
  • aspnet_Roles_FullAccess
  • aspnet_WebEvent_FullAccess

If you did it and it didn’t help try to search FBA user by full claims login name. E.g. suppose that we have user “superuser” in FBA database. Then claims login name will be “i:0#.f|fba_membership|superuser” (fba_membership is the name of membership provider configured in web.config of your web application, central administration and secure token service). It should find the user and after that People picker should successfully resolve FBA users.

Sunday, June 22, 2014

FBA users management with pagination on SQL server side in Sharepoint. Part 2

In the first part of the series we saw how users information which comes from SQL Server aspnetdb database can be retrieved by separate pages instead of retrieving all users and implement pagination in memory. Such approach gives us advantage in performance which will be crucial when amount of users is very big. In this part we will continue improving performance and will consider how we may attach additional information for each user returned from SQL Server from Sharepoint’s User information list (e.g. such data as Title, Created and Modified).

Let’s return to the code sample from the first part. The key part is those which retrieves appropriate page of information from the database:

   1: // get paged users from sql database
   2: int pageNumber = args.StartRowIndex/args.MaximumRows;
   3: int totalRecords;
   4: var membershipUsers = Membership.GetAllUsers(pageNumber, args.MaximumRows,
   5:     out totalRecords);
   6: if (membershipUsers.Count == 0)
   7: {
   8:     return null;
   9: }
  10:  
  11: var users = new DataTable();
  12: users.Columns.Add("Name");
  13: users.Columns.Add("Email");
  14: users.Columns.Add("Active");
  15:  
  16: foreach (MembershipUser membershipUser in membershipUsers)
  17: {
  18:     var row = users.NewRow();
  19:     row["Name"] = membershipUser.UserName;
  20:     row["Email"] = membershipUser.Email;
  21:     row["Active"] = membershipUser.IsApproved ? "Yes" : "No";
  22:     users.Rows.Add(row);
  23: }

In result we have DataTable object with Name, Email and Active columns and current page of users information. Now for each user in the current page we need to add additional information from Sharepoint: Title, Modified, Created. How to do it? The simples way is to perform CAML query for each user and add this data to the resulting DataTable. But it will lead to the classic n + 1 problem, when n users will cause n database queries plus optional 1 query for retrieving all users first. In turn it causes performance impact and all our efforts from the first part with retrieving paged information from SQL Server won’t have a lot of sense. Is there a better way to get data? Yes it is: instead of making many CAML queries for each user we may make single CAML query which will return data for all users returned from the database. This query is more complex: it should be built dynamically for each page of the users. E.g. if we have 3 users in the DataTable with names testuser1, testuser2 and testuser3 we need to built CAML query which will return data by the following conditions:
item[“Name”] == “testuser1” || item[“Name”] == “testuser2” || item[“Name”] == “testuser2”
which will be presented in CAML like this:

   1: <Where>
   2:   <And>
   3:     <And>
   4:       <Eq>
   5:         <FieldRef Name="Name" />
   6:         <Value Type="Text">testuser1</Value>
   7:       </Eq>
   8:       <Eq>
   9:         <FieldRef Name="Name" />
  10:         <Value Type="Text">testuser2</Value>
  11:       </Eq>
  12:     </And>
  13:     <Eq>
  14:       <FieldRef Name="Name" />
  15:       <Value Type="Text">testuser3</Value>
  16:     </Eq>
  17:   </And>
  18: </Where>

Also note that page size is not hardcoded parameter and may be also be different and that in the final page number of users may be less than page size. The bigger number of users will be retrieved from database, the bigger CAML tree will be.

In order to built CAML query we will use Camlex – open source library for building dynamic CAML queries. It is ideal for the mentioned requirements, i.e. when CAML query should be built in runtime base on some parameters. Before to use it we need to notice one thing: Name column in User information list in Sharepoint contains user name in form {membership provider name}:{user name}. Membership provider name is specified in web.config and can be retrieved by using SPIisSettings.MembershipProvider property. Let’s first see how CAML query is built:

   1: var site = SPContext.Current.Site;
   2: var settings = CodeFiles.Utils.GetFBAIisSettings(site);
   3: if (settings == null)
   4:     return null;
   5:  
   6: // ... get paged data from SQL Server
   7:  
   8: var users = new DataTable();
   9: users.Columns.Add("Name");
  10: users.Columns.Add("Email");
  11: users.Columns.Add("Active");
  12: users.Columns.Add("Title");
  13: users.Columns.Add("Modified");
  14: users.Columns.Add("Created");
  15:  
  16: // ... fill DataTable with users info from the database
  17:  
  18: // get appropriate users from Sharepoint
  19: string prefix = settings.MembershipProvider.ToLower() + ":";
  20: var orExpressions = new List<Expression<Func<SPListItem, bool>>>();
  21: foreach (MembershipUser membershipUser in membershipUsers)
  22: {
  23:     var mu = membershipUser;
  24:     orExpressions.Add(x => (string)x["Name"] == prefix + mu.UserName);
  25: }
  26: var orExpression = ExpressionsHelper.CombineOr(orExpressions);
  27: var andExpressions = new List<Expression<Func<SPListItem, bool>>>();
  28: andExpressions.Add(x => (string)x["ContentType"] == "Person");
  29: andExpressions.Add(orExpression);
  30:  
  31: var query = new SPQuery();
  32: query.Query = Camlex.Query().WhereAll(andExpressions).ToString();
  33: query.ViewFields = Camlex.Query().ViewFields(x => new[] { x["Name"],
  34:     x["LinkTitle"], x["Email"], x["Modified"], x["Created"] });
  35: DataTable spUsers = null;
  36: try
  37: {
  38:     spUsers = site.RootWeb.SiteUserInfoList.GetItems(query).GetDataTable();
  39: }
  40: catch (Exception ex)
  41: {
  42:     return null;
  43: }
  44:  
  45: if (spUsers != null)
  46: {
  47:     foreach (DataRow userRow in users.Rows)
  48:     {
  49:         foreach (DataRow spRow in spUsers.Rows)
  50:         {
  51:             string spUserName = ((string) spRow["Name"]).Replace(prefix, "");
  52:             if (((string) userRow["Name"]).ToLower() == spUserName.ToLower())
  53:             {
  54:                 userRow["Title"] = spRow["Title"];
  55:                 userRow["Modified"] = spRow["Modified"];
  56:                 userRow["Created"] = spRow["Created"];
  57:                 break;
  58:             }
  59:         }
  60:     }
  61: }

At first we build CAML query (lines 19-35) with Camlex. E.g. if DataTable contains 20 users returned from SQL Server, it will build the following query:

   1: <Where>
   2:   <And>
   3:     <Eq>
   4:       <FieldRef Name="ContentType" />
   5:       <Value Type="Text">Person</Value>
   6:     </Eq>
   7:     <Or>
   8:       <Or>
   9:         <Or>
  10:           <Or>
  11:             <Or>
  12:               <Or>
  13:                 <Or>
  14:                   <Or>
  15:                     <Or>
  16:                       <Eq>
  17:                         <FieldRef Name="Name" />
  18:                         <Value Type="Text">fba:testuser1</Value>
  19:                       </Eq>
  20:                       <Eq>
  21:                         <FieldRef Name="Name" />
  22:                         <Value Type="Text">fba:testuser2</Value>
  23:                       </Eq>
  24:                     </Or>
  25:                     <Eq>
  26:                       <FieldRef Name="Name" />
  27:                       <Value Type="Text">fba:testuser3</Value>
  28:                     </Eq>
  29:                   </Or>
  30:                   <Eq>
  31:                     <FieldRef Name="Name" />
  32:                     <Value Type="Text">fba:testuser4</Value>
  33:                   </Eq>
  34:                 </Or>
  35:                 <Eq>
  36:                   <FieldRef Name="Name" />
  37:                   <Value Type="Text">fba:testuser5</Value>
  38:                 </Eq>
  39:               </Or>
  40:               <Eq>
  41:                 <FieldRef Name="Name" />
  42:                 <Value Type="Text">fba:testuser6</Value>
  43:               </Eq>
  44:             </Or>
  45:             <Eq>
  46:               <FieldRef Name="Name" />
  47:               <Value Type="Text">fba:testuser7</Value>
  48:             </Eq>
  49:           </Or>
  50:           <Eq>
  51:             <FieldRef Name="Name" />
  52:             <Value Type="Text">fba:testuser8</Value>
  53:           </Eq>
  54:         </Or>
  55:         <Eq>
  56:           <FieldRef Name="Name" />
  57:           <Value Type="Text">fba:testuser9</Value>
  58:         </Eq>
  59:       </Or>
  60:       <Eq>
  61:         <FieldRef Name="Name" />
  62:         <Value Type="Text">fba:testuser10</Value>
  63:       </Eq>
  64:     </Or>
  65:   </And>
  66: </Where>

After that we add data from Sharepoint to resulting DataTable (lines 46-62) by mapping rows by Name column and after that it contains data both form SQL Server and from Sharepoint. SPIisSettings object used in the example above can be retrieved with the following helper function (it is given from CKS.FBA open source project):

   1: public static SPIisSettings GetFBAIisSettings(SPSite site)
   2: {
   3:     SPIisSettings settings = null;
   4:  
   5:     // try and get FBA IIS settings from current site zone
   6:     try
   7:     {
   8:         settings = site.WebApplication.IisSettings[site.Zone];
   9:         if (settings.AuthenticationMode == AuthenticationMode.Forms)
  10:             return settings;
  11:     }
  12:     catch
  13:     {
  14:         // expecting errors here so do nothing                 
  15:     }
  16:  
  17:     // check each zone type for an FBA enabled IIS site
  18:     foreach (SPUrlZone zone in Enum.GetValues(typeof(SPUrlZone)))
  19:     {
  20:         try
  21:         {
  22:             settings = site.WebApplication.IisSettings[(SPUrlZone)zone];
  23:             if (settings.AuthenticationMode == AuthenticationMode.Forms)
  24:                 return settings;
  25:         }
  26:         catch
  27:         {
  28:             // expecting errors here so do nothing                 
  29:         }
  30:     }
  31:  
  32:     // return null if FBA not enabled
  33:     return null;
  34: }

As you can see using of Camlex allowed us to solve the problem of retrieving data from Sharepoint quite gracefully. In our solution for optimizing performance we get only necessary page of users information from SQL Server and then perform single CAML query to the Sharepoint, which returns data for only selected users, and then merge information and show it in the listing. Without Camlex alternative way would be to use Linq 2 Sharepoint, but it would require more setup from us. From my point of view this is very nice example which shows how effective Camlex may be. I hope that this information will help you in your work and you will use Camlex in scenarios when need to build CAML queries dynamically.