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. Other 2 parameters are ignored and sliding sessions doesn’t work, 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, August 24, 2017

Sharing cookies for HttpWebRequest from Sharepoint site with FBA claims authentication

If you need to make sub request from your Sharepoint site you can do it like this (in this post we will assume that we make sub requests to the same Sharepoint site):

   1: var request = (HttpWebRequest)WebRequest.Create(url);
   2: request.Credentials = CredentialCache.DefaultNetworkCredentials;
   3: var response = (HttpWebResponse)request.GetResponse();

This code will work for Windows authentication – on receiver’s side if you will check SPContext.Current.Web.CurrentUser it will be the same as on sender’s side. But if the same code will run under FBA zone SPContext.Current.Web.CurrentUser will be null on receiver’s side. In order to force Sharepoint to execute the code under the same user also in FBA zone we need to share cookies:

   1: var request = (HttpWebRequest)WebRequest.Create(url);
   2: request.Credentials = CredentialCache.DefaultNetworkCredentials;
   3:  
   4: if (HttpContext.Current != null && web.Site.Zone != SPUrlZone.Default)
   5: {
   6:     HttpCookie authCookie = HttpContext.Current.Request.Cookies["FedAuth"];
   7:     if (authCookie != null)
   8:     {
   9:         log("Before send request: set auth cookies");
  10:         request.CookieContainer = new CookieContainer();
  11:         request.CookieContainer.Add(new Cookie("FedAuth", authCookie.Value,
  12:             authCookie.Path, new Uri(url).Host));
  13:     }
  14: }
  15:  
  16: var response = (HttpWebResponse)request.GetResponse();

In this example we assume that site works both with Windows and FBA zones and that Windows authentication is used on Default zone. After that SPContext.Current.Web.CurrentUser will be also correct on receiver’s side for FBA zone.

Unspecified error when create Domain local groups via DirectoryServices programmatically in .Net

In order to create AD group programmatically we can use DirectoryServices .Net assembly. Here is the code which create domain global group:

   1: string groupName = "...";
   2: var de = new DirectoryEntry("LDAP://...");
   3: var group = de.Children.Add("CN=" + groupName, "group");
   4: group.Properties["samAccountName"].Value = groupName;
   5: group.CommitChanges();
   6: return true;

If we want to create Domain local group we need to set one more property for created group: groupType. Value of this property should be created as bitmask from the following values (see ADS_GROUP_TYPE_ENUM enumeration):

ADS_GROUP_TYPE_GLOBAL_GROUP

0x00000002

ADS_GROUP_TYPE_DOMAIN_LOCAL_GROUP

0x00000004

ADS_GROUP_TYPE_UNIVERSAL_GROUP

0x00000008

ADS_GROUP_TYPE_SECURITY_ENABLED

0x80000000

But if we use C# and try to create group like this:

   1: string groupName = "...";
   2: bool local = ...;
   3: var de = new DirectoryEntry("LDAP://...");
   4: DirectoryEntry group = de.Children.Add("CN=" + groupName, "group");
   5: group.Properties["samAccountName"].Value = groupName;
   6: group.Properties["groupType"].Value = (local ? 0x00000004 : 0x00000002) | 0x80000000;
   7: group.CommitChanges();

we will get exception with Unspecified error message:

Unspecified error
   at System.DirectoryServices.Interop.UnsafeNativeMethods.IAds.PutEx(Int32 lnControlCode, String bstrName, Object vProp)
   at System.DirectoryServices.PropertyValueCollection.set_Value(Object value)

The reason is that by default C# will cast value (local ? 0x00000004 : 0x00000002) | 0x80000000 as long and will pass 2147483652 to the groupType property which is incorrect value here. In order to avoid this error we need to pass int value to this property, i.e. in our code we should explicitly cast it to int – in this case it will pass negative value -2147483644 there:

   1: string groupName = "...";
   2: bool local = ...;
   3: var de = new DirectoryEntry("LDAP://...");
   4: DirectoryEntry group = de.Children.Add("CN=" + groupName, "group");
   5: group.Properties["samAccountName"].Value = groupName;
   6: group.Properties["groupType"].Value =
   7:     (int)((local ? 0x00000004 : 0x00000002) | 0x80000000);
   8: group.CommitChanges();

and group will be successfully created.

Thursday, August 17, 2017

List all UserCustomActions in Sharepoint site collections and sub sites via PowerShell

User custom actions (see SPSite.UserCustomActions and SPWeb.UserCustomActions) are powerful mechanism to add customizations on Sharepoint site (on-premise or online) via javascript. E.g. in one of the previous posts I showed how to add custom javascript file to all pages in your site collection without modifying master page: see Add custom javascript file to all pages in on-premise Sharepoint site collection without modifying masterpage and Add custom javascript file to all pages in Sharepoint Online site collection. Sometimes we need to perform inventory of all custom actions with script links. Here is the PowerShell script which iterates through all site collections in provided web application and all sub sites and outputs custom action’s ScriptSrc to the log file:

   1: param(
   2:     [string]$url
   3: )
   4:  
   5: if (-not $url)
   6: {
   7:     Write-Host "Specify web application url in url parameter"
   8: -foregroundcolor red
   9:     return
  10: }
  11:  
  12: function CheckWeb($web)
  13: {
  14:     Write-Host "Web:" $web.Url
  15:     foreach($ac in $web.UserCustomActions)
  16:     {
  17:         ("  " + $ac.ScriptSrc) | Out-File "log.txt" -Append
  18:     }
  19:     
  20:     $web.Webs | ForEach-Object { CheckWeb $_ }
  21: }
  22:  
  23: function CheckSite($site)
  24: {
  25:     Write-Host "Site collection:" $site.Url
  26:     ("Site collection: " +  $site.Url) | Out-File "log.txt" -Append
  27:  
  28:     foreach($ac in $site.UserCustomActions)
  29:     {
  30:         ("  " + $ac.ScriptSrc) | Out-File "log.txt" -Append
  31:     }
  32:     
  33:     CheckWeb $site.RootWeb
  34:     
  35:     ("---------------------------------------") | Out-File "log.txt" -Append
  36: }
  37:  
  38:  
  39: $wa = Get-SPWebApplication $url
  40: $wa.Sites | ForEach-Object { CheckSite $_ }

Here in order to write results to the log file I used approach described in the following post: Write output to the file in PowerShell. And it is quite straightforward to rewrite this script for Sharepoint Online (see article provided above for Sharepoint Online). Hope it will help someone.