Feeds:
Posts
Comments

Last year I set out on the adventure of learning how to use Claims Authentication (via ADFS) and SharePoint 2010.  As many have now learned, Claims Authentication changed the way that we access websites by separating the authentication to an external entity.  It also solved the very difficult task of Single Sign-On. 

Everything was working wonderfully until we had to connect to SharePoint remotely to access data.  Event with the new SharePoint Client objects, we weren’t able to get this functionality working. To solve this problem, we had to reverse engineer what happed in browser each time you visited a site and attempt to replicate the process.  Then started the fun work of understanding Tokens and Cookies…

In a nutshell, the process of connecting to SharePoint happens like this;  Make a request, re-direct to an STS for login, post token from login to SharePoint’s STS (‘_trust’ site), post token from SharePoint’s STS to SharePoint, and then capture and store the ‘FedAuth’ cookie generated by the site.  Then and only then, can you start accessing the data from the site by providing the ‘FedAuth’ cookie.  The diagram below outlines the process in a slightly more detail…

image

The new SharePoint Client objects accessing SharePoint and most of its information much easier than before.  Unfortunately; they are new, and new things have very little support.  Fortunately, this fact didn’t keep us from long hours with Microsoft Tech Support (naturally calling me back right as I was about to walk out the door).

Now on to the good stuff…

The primary component of using the SharePoint Client objects is the ClientContext class.  This class manages all of the interaction with your application and the SharePoint site.  You tell the ClientContext what you want it to do and then tell it to execute.  All-in-all, it makes the code much cleaner when pulling information from SharePoint. 

Instead of muddying up my code, I decided to extend the ClientContext class and make it able to connect to a Claims-based site on its own.  The key to extending the class, was in utilizing the ‘ExecutingWebRequest’ event.  Here is where I was able to do all of my work and setup the connection with SharePoint.

But; before I continue, one warning.  This is beta code and should be used at your own risk.  I currently only having it working by retrieving authentication by impersonating the local context.  So, it will not pass on any Claims Based credentials if used from a website (Windows Identity Foundation).

If anyone reading this can figure out a clean way to pass on the user’s token, please let me know.  Thanks!

using System;
using System.Linq;
using System.Net;
using System.Net.Security;
using System.Security.Principal;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.Text;
using System.Web;
using System.Xml;
using Microsoft.IdentityModel.Protocols.WSTrust;
using Microsoft.SharePoint.Client;

namespace SharePointLibraries
{
    public class ClaimsClientContext : ClientContext
    {
        // store this info at the class level for access by the ExecutingWebRequest event
        private string SharePointRootUrl
        {
            get
            {
                // return (new Uri(_sharePointSiteUrl)).GetComponents(UriComponents.Scheme | UriComponents.Host, UriFormat.SafeUnescaped).ToString();
                var siteUri = (new Uri(_sharePointSiteUrl));
                return string.Format("{0}://{1}/", siteUri.Scheme, siteUri.Host);
            }
        }
        private string _sharePointSiteUrl;
        private string SharePointSiteUrl
        {
            get { return _sharePointSiteUrl.EndsWith("/") ? _sharePointSiteUrl : _sharePointSiteUrl + "/"; }
            set { _sharePointSiteUrl = value; }
        }
        public string SharePointSiteRealm { get; set; }
        private string _loginStsUrl;
        private string LoginStsUrl
        {
            get { return _loginStsUrl.EndsWith("/") ? _loginStsUrl : _loginStsUrl + "/"; }
            set { _loginStsUrl = value; }
        }

        // store the Saml token so that it can be used by successive requests
        private static string IssuedSamlToken { get; set; }
        private static DateTime IssuedSamlTokenExpireDate { get; set; }

        /// <summary>
        /// Public constructor for all three being strings
        /// </summary>
        /// <param name="sharePointSiteUrl"></param>
        /// <param name="sharePointSiteRealm"></param>
        /// <param name="loginStsUrl"></param>
        public ClaimsClientContext(string sharePointSiteUrl, string sharePointSiteRealm, string loginStsUrl)
            : base(sharePointSiteUrl)
        {
            if (sharePointSiteUrl == null) throw new ArgumentNullException("sharePointSiteUrl");
            if (sharePointSiteRealm == null) throw new ArgumentNullException("sharePointSiteRealm");
            if (loginStsUrl == null) throw new ArgumentNullException("loginStsUrl");

            // save the settings
            SharePointSiteUrl = sharePointSiteUrl;
            SharePointSiteRealm = sharePointSiteRealm;
            LoginStsUrl = loginStsUrl;

            // specify the default credentials to use
            Credentials = CredentialCache.DefaultCredentials;

            // add a handler for the ExecutingWebReques event to provide the SAML token
            // this.ExecutingWebRequest += new EventHandler<WebRequestEventArgs>(ClientContext_ExecutingWebRequest);
            ExecutingWebRequest += ClientContext_ExecutingWebRequest;
        }

        /// <summary>
        /// Public constructor for Site Url being a Uri
        /// </summary>
        /// <param name="sharePointSiteUrl"></param>
        /// <param name="sharePointSiteRealm"></param>
        /// <param name="loginStsUrl"></param>
        public ClaimsClientContext(Uri sharePointSiteUrl, string sharePointSiteRealm, string loginStsUrl)
            : base(sharePointSiteUrl)
        {
            if (sharePointSiteUrl == null) throw new ArgumentNullException("sharePointSiteUrl");
            if (sharePointSiteRealm == null) throw new ArgumentNullException("sharePointSiteRealm");
            if (loginStsUrl == null) throw new ArgumentNullException("loginStsUrl");

            // save the settings
            SharePointSiteUrl = sharePointSiteUrl.ToString();
            SharePointSiteRealm = sharePointSiteRealm;
            LoginStsUrl = loginStsUrl;

            // specify the default credentials to use
            Credentials = CredentialCache.DefaultCredentials;

            // add a handler for the ExecutingWebReques event to provide the SAML token
            ExecutingWebRequest += ClientContext_ExecutingWebRequest;
        }

        /// <summary>
        /// Extend the ExecutingWebRequest event to include a cookie containing the claims information for the site
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void ClientContext_ExecutingWebRequest(object sender, WebRequestEventArgs e)
        {
            // retrieve the Saml Token from the STS
            if (IssuedSamlToken == null || IssuedSamlTokenExpireDate.CompareTo(DateTime.Now) <= 0)
            {
                IssuedSamlToken = GetSamlToken(LoginStsUrl, SharePointRootUrl, SharePointSiteRealm);
                IssuedSamlTokenExpireDate = DateTime.Now.AddHours(1);
            }

            // create a new FedAuth cookie containing the Saml token
            var samlAuth = new Cookie("FedAuth", IssuedSamlToken)
            {
                Expires = IssuedSamlTokenExpireDate,
                Path = "/",
                Secure = true,
                HttpOnly = true,
                Domain = (new Uri(SharePointSiteUrl)).Host
            };

            // create a cookie container to attach the cookie to the web request
            var cookieContainer = new CookieContainer();
            cookieContainer.Add(samlAuth);

            // update the web request used by the sharepoint managed client to include the FedAuth cookie
            e.WebRequestExecutor.WebRequest.CookieContainer = cookieContainer;
        }

        /// <summary>
        /// Generate a Saml token from the ADFS STS server and the SharePoint STS Server
        /// </summary>
        /// <param name="stsRootUri"></param>
        /// <param name="sharePointRootUrl"></param>
        /// <param name="sharePointRealm"></param>
        /// <returns></returns>
        private static string GetSamlToken(string stsRootUri, string sharePointRootUrl, string sharePointRealm)
        {
            // build the relying party information to build the tokens for
            var sharePointInformation = new
            {
                Wctx = sharePointRootUrl + "_layouts/Authenticate.aspx?Source=%2F",
                Wtrealm = sharePointRealm,
                Wreply = sharePointRootUrl + "_trust/"
            };

            // get token from originating STS
            var stsResponse = GetStsResponse(stsRootUri, sharePointInformation.Wreply);

            // need to post the AD FS token to the SharePoint STS Server
            var sharepointRequest = WebRequest.Create(sharePointInformation.Wreply) as HttpWebRequest;
            if (sharepointRequest != null)
            {
                // configure the web request for the post to the SharePoint STS
                sharepointRequest.Method = "POST";
                sharepointRequest.ContentType = "application/x-www-form-urlencoded";
                sharepointRequest.CookieContainer = new CookieContainer();
                sharepointRequest.AllowAutoRedirect = false; // This is important

                // build a reference to the request stream to submit the information on
                var newStream = sharepointRequest.GetRequestStream();

                // format the information to submit to the SharePoint STS
                var loginInformation = String.Format("wa=wsignin1.0&wctx={0}&wresult={1}",
                    HttpUtility.UrlEncode(sharePointInformation.Wctx),
                    HttpUtility.UrlEncode(stsResponse));

                // convert the login information to bytes for submittion on the request stream
                var loginInformationBytes = Encoding.UTF8.GetBytes(loginInformation);

                // write the bytes to the request stream
                newStream.Write(loginInformationBytes, 0, loginInformationBytes.Length);
                newStream.Close();

                // retrieve the response from the SharePoint STS
                var webResponse = sharepointRequest.GetResponse() as HttpWebResponse;
                
                // inspect the response for the FedAuth cookie and return its contents
                if (webResponse != null)
                {
                    // ensure there were cookies received
                    if (webResponse.Cookies != null && webResponse.Cookies.Count > 0)
                    {
                        // find the FedAuth cook and return it
                        foreach (var cookie in
                            webResponse.Cookies.Cast<Cookie>().Where(cookie => cookie.Name == "FedAuth"))
                        {
                            return cookie.Value;
                        }
                    }
                }
            }

            // unable to find the FedAuth cookie, return an empty string to be handled by the calling method
            return string.Empty;
        }

        /// <summary>
        /// Call the STS and retrieve the token need to pass to SharePoint for authentication
        /// </summary>
        /// <param name="stsRootUri"></param>
        /// <param name="realm"></param>
        /// <returns></returns>
        private static string GetStsResponse(string stsRootUri, string realm)
        {
            // specify the AD FS endpoint to use for issueing the Saml Token
            const string stsEndPointWindowsAuth = "adfs/services/trust/2005/windowstransport";

            // build the request security token object for the requesting realm
            var stsRequestToken = new RequestSecurityToken
            {
                // specify that an issue is required
                RequestType = WSTrustFeb2005Constants.RequestTypes.Issue,
                // specify the realm of the relying party to issue for
                AppliesTo = new EndpointAddress(realm),
                // bearer token, no encryption
                KeyType = WSTrustFeb2005Constants.KeyTypes.Bearer
            };

            // build the WsHttpBinding for the STS (AD FS)
            var binding = new WSHttpBinding();
            binding.Security.Mode = SecurityMode.Transport;
            binding.Security.Message.ClientCredentialType = MessageCredentialType.None;
            binding.Security.Message.EstablishSecurityContext = false;
            binding.Security.Transport.ClientCredentialType = HttpClientCredentialType.Windows;

            // build the trust client for the STS using Windows Auth via the App Pool's credentials
            var trustClient = new WSTrustFeb2005ContractClient(binding, (new EndpointAddress(stsRootUri + stsEndPointWindowsAuth)));
            trustClient.ClientCredentials.Windows.AllowNtlm = true;
            trustClient.ClientCredentials.Windows.AllowedImpersonationLevel = TokenImpersonationLevel.Impersonation;
            trustClient.ClientCredentials.Windows.ClientCredential = CredentialCache.DefaultNetworkCredentials;

            Message stsResponse;
            try
            {
                // generate the response containing the token for the specified relying party (realm)
                stsResponse =
                    trustClient.EndIssue(
                        trustClient.BeginIssue(
                            Message.CreateMessage(
                                MessageVersion.Default,
                                WSTrustFeb2005Constants.Actions.Issue,
                                new RequestBodyWriter(
                                    (new WSTrustFeb2005RequestSerializer()),
                                    stsRequestToken)),
                            null,
                            null));
            }
            finally
            {
                // close the trust client when done
                trustClient.Close();
            }

            // build a reader to parse the response
            return stsResponse != null ? stsResponse.GetReaderAtBodyContents().ReadOuterXml() : null;
        }
    }

    /// <summary>
    /// Create a new contract to use for issue claims for the SharePoint requests
    /// </summary>
    [ServiceContract]
    public interface IWSTrustFeb2005Contract
    {
        [OperationContract(ProtectionLevel = ProtectionLevel.EncryptAndSign,
            Action = "http://schemas.xmlsoap.org/ws/2005/02/trust/RST/Issue",
            ReplyAction = "http://schemas.xmlsoap.org/ws/2005/02/trust/RSTR/Issue",
            AsyncPattern = true)]
        IAsyncResult BeginIssue(Message request, AsyncCallback callback, object state);
        Message EndIssue(IAsyncResult asyncResult);
    }

    /// <summary>
    /// Implement the client contract for the new type
    /// </summary>
    public class WSTrustFeb2005ContractClient : ClientBase<IWSTrustFeb2005Contract>, IWSTrustFeb2005Contract
    {
        public WSTrustFeb2005ContractClient(Binding binding, EndpointAddress remoteAddress)
            : base(binding, remoteAddress)
        {}

        public IAsyncResult BeginIssue(Message request, AsyncCallback callback, object state)
        {
            return Channel.BeginIssue(request, callback, state);
        }

        public Message EndIssue(IAsyncResult asyncResult)
        {
            return Channel.EndIssue(asyncResult);
        }
    }

    /// <summary>
    /// Create a class that will serialize the token into the request
    /// </summary>
    class RequestBodyWriter : BodyWriter
    {
        readonly WSTrustRequestSerializer _serializer;
        readonly RequestSecurityToken _rst;

        /// <summary>
        /// Constructs the Body Writer.
        /// </summary>
        /// <param name="serializer">Serializer to use for serializing the rst.</param>
        /// <param name="rst">The RequestSecurityToken object to be serialized to the outgoing Message.</param>
        public RequestBodyWriter(WSTrustRequestSerializer serializer, RequestSecurityToken rst)
            : base(false)
        {
            if (serializer == null)
                throw new ArgumentNullException("serializer");

            _serializer = serializer;
            _rst = rst;
        }


        /// <summary>
        /// Override of the base class method. Serializes the rst to the outgoing stream.
        /// </summary>
        /// <param name="writer">Writer to which the rst should be written.</param>
        protected override void OnWriteBodyContents(XmlDictionaryWriter writer)
        {
            _serializer.WriteXml(_rst, writer, new WSTrustSerializationContext());
        }
    }

}

Download

For those of us with Windows Server AppFabric development and testing environments, there comes a time when we want to just clear out the Persistence and Monitoring databases.  For some reason, there really isn’t much in the form of information on doing this.  Probably because it was never intended to be done.  But this is development and testing…

So here ya go, run this PowerShell script to reset the databases (you’ll need to fill in your own values).

#
# This process must be run with Set-ExecutionPolicy Unrestricted 
# 
Import-Module ApplicationServer

# First remove the persistence database 
Remove-ASPersistenceSqlDatabase -Force -Server ".\SQLExpress" -Database "AppFabricPersistenceDB"

# Now initialize the persistence database with the default settings  
# This will need to be customized to your environment. 
Initialize-ASPersistenceSqlDatabase -Admins $env:computername\AS_Administrators -Readers $env:computername\AS_Observers -Users "BUILTIN\IIS_IUSRS" -Database "AppFabricPersistenceDB" -Server ".\SQLExpress"   

# Use the clear method for the monitoring database to reset it 
Clear-ASMonitoringSqlDatabase -Database "AppFabricMonitoringDB" -Server ".\SQLExpress" 

No GACUtil, No Problem

Recently while deploying applications to a new Windows Server 2008 R2 machine.  I realized that there was no “gacutil.exe” application installed on the machine.  Granted, I could have either installed the SDK or just copied the executable and configuration over.  But, the install of the SDK could have added extra libraries that I didn’t want on the server unless necessary, and copying the executable just seemed sloppy for a deployment.

Instead, I wrote a little PowerShell script that could add and remove utilities from the GAC just as easily. 

Using the power of the System.EnterpriseServices namespace, I was able to tap into the Publish class that gave me access to the ability add and remove from the GAC.

By copying this code you can deploy your assemblies along with a PowerShell script and trust that they can get deployed.  A much cleaner solution then installing an SDK…

#
# This process must be run with Set-ExecutionPolicy Unrestricted
#
$assemblyDll = "MyAssembly.dll"
$currentDirectory = Get-Location
$assemblyPath = $currentDirectory.Path + "\" + $assemblyDll

# method for adding new assemblies to the GAC
function Add-GacItem([string]$assembly) {
	Begin
	{
		# see if the Enterprise Services Namespace is registered
		if ($null -eq ([AppDomain]::CurrentDomain.GetAssemblies() |? { $_.FullName -eq "System.EnterpriseServices, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" }) ) {
			# register the Enterprise Service .NET library
			[System.Reflection.Assembly]::Load("System.EnterpriseServices, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a") | Out-Null
		}

		# create a reference to the publish class
		$publish = New-Object System.EnterpriseServices.Internal.Publish
	}
	Process
	{
		# ensure the file that was provided exists
		if ( -not (Test-Path $assembly -type Leaf) ) {
			throw "The assembly '$assembly' does not exist."
		}

		# ensure the file is strongly signed before installing in the GAC
		if ( [System.Reflection.Assembly]::LoadFile( $assembly ).GetName().GetPublicKey().Length -eq 0) {
			throw "The assembly '$assembly' must be strongly signed."
		}

		# install the assembly in the GAC
		Write-Output "Installing: $assembly"
		$publish.GacInstall( $assembly )
	}
}

# method for removing assemblies from the GAC
function Remove-GacItem([string]$assembly) {
	Begin
	{
		# see if the Enterprise Services Namespace is registered
		if ($null -eq ([AppDomain]::CurrentDomain.GetAssemblies() |? { $_.FullName -eq "System.EnterpriseServices, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" }) ) {
			# register the Enterprise Service .NET library
			[System.Reflection.Assembly]::Load("System.EnterpriseServices, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a") | Out-Null
		}

		# create a reference to the publish class
		$publish = New-Object System.EnterpriseServices.Internal.Publish
	}
	Process
	{
		# ensure the file that was provided exists
		if ( -not (Test-Path $assembly -type Leaf) ) {
			throw "The assembly '$assembly' does not exist."
		}

		# ensure the file is strongly signed before installing in the GAC
		if ( [System.Reflection.Assembly]::LoadFile( $assembly ).GetName().GetPublicKey().Length -eq 0) {
			throw "The assembly '$assembly' must be strongly signed."
		}

		# install the assembly in the GAC
		Write-Output "UnInstalling: $assembly"
		$publish.GacRemove( $assembly )
	}
}

Write-Host "Registering the Assembly: '$assemblyPath'..." - ForegroundColor Green
Add-GacItem $assemblyPath

Write-Host "UnRegistering the Assembly: '$assemblyPath'..." - ForegroundColor Green
Remove-GacItem $assemblyPath

Download

Follow

Get every new post delivered to your Inbox.