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…
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());
}
}
}
Incredibly thought provoking post regarding Connecting to SharePoint with Claims Authentication Fred's Space. I loved browsing it. Are they genuine gifs or has the graphics been touched up. Many thanks for sharing this useful info.
All genuine.
Hi Fred
Thanks for nice post.
I am facing while connecting to SP 2010 using Client Object Model. I am fetch SAML token using WSTrustChannelFactory as mentioned below
WS2007HttpBinding ws2007HttpBinding = new WS2007HttpBinding();
ws2007HttpBinding.Security.Mode = SecurityMode.Message;
ws2007HttpBinding.Security.Message.ClientCredentialType = MessageCredentialType.UserName;
ws2007HttpBinding.Security.Message.EstablishSecurityContext = false;
EndpointAddress endPointAddress = new EndpointAddress(new Uri(address), new DnsEndpointIdentity(“TestCert”), new AddressHeaderCollection());
WSTrustChannelFactory trustChannelFactory = new WSTrustChannelFactory(ws2007HttpBinding, endPointAddress);
trustChannelFactory.Credentials.UserName.UserName = “test”;
trustChannelFactory.Credentials.UserName.Password = “test”;
trustChannelFactory.Credentials.ServiceCertificate.SetDefaultCertificate(StoreLocation.LocalMachine, StoreName.My, X509FindType.FindBySubjectName, “TestCert”);
trustChannelFactory.Credentials.ClientCertificate.SetCertificate(StoreLocation.LocalMachine, StoreName.My, X509FindType.FindBySubjectName, “TestCert”);
WSTrustChannel channel = (WSTrustChannel)trustChannelFactory.CreateChannel();
RequestSecurityToken rst = new RequestSecurityToken(RequestTypes.Issue);
rst.AppliesTo = new EndpointAddress(address);
RequestSecurityTokenResponse rstr = null;
SecurityToken token = channel.Issue(rst, out rstr);
var tokenString = rstr.RequestedSecurityToken.SecurityTokenXml.OuterXml;
When I set this token to FedAuth cookie, I am getting error (Cannot contact site at the specified URL).
I am able to access the SP2010 site using brower (it takes me to login page of STS and after providing credentials it takes me to SharePoint site). I have intercepted this using fiddler and found that FedAuth cookie here is in encrypted format.
Can you please tell me where I am wrong?
In the code you provided, are you using that to connect to the SharePoint STS or some other STS that SharePoint is using? Don’t forget; that for the code to work, SharePoint has to issue the ‘cookie’ that is used to continued authentication to the site. The code that I posted logs into the SharePoint server and then stores the cookie that it generates. That cookie is what allows the client library to work…
SharePoint isn’t like an ASP.NET site with the WIF, it has its own STS server (_trust) that needs to generate the FedAuth cookie to be used.
Thanks for the reply
Ooh, code I have provided is used to connect to my custom STS to get the Token. I used to set the same SAML Token on FedAuth cookie…..
I have configured my Custom STS as a trusted identity provider in SP 2010 and created a Claim based Web App.
I want to use Client Object Model to connect to this Claim based web app.
Now as you said,
1) Get SAML token from Custom STS
2) need to post the SAML token to the SharePoint STS Server (mentioned in GetSamlToken method in yr post)
Please let me know if I am missing something.
Yep, you’ve got it.
If you look at the method GetStsResponse in my example, line 203, you’ll need to change that method to utilize your STS instead of ADFS like I did. Then the rest of the functionality would be the same.
Fred
Isn’t here some caching in the SharePoint client library?
I’ve expreienced that setting the FedAuth cookie in the ClientContext_ExecutingWebRequest method matters only on first execution of this method. Regardless the authentication was successfull or not, all other authentications are ignored. If I successfully authenticate as user 1, then re-logon to SharePoint as user 2 (and the FedAuth cookie changes), the client communication still runs under the user 1 (context.Web.CurrentUser still returns user 1 etc.)
Restarting of the internet browser does not help. Just recycling the web application on server allows me to log as another user.
I’ve tested it both on local ASP.NET Development Server on Windows 7 and on Windows 2008 Server.
If you’re using the class that I have, it will only use your current login for credentials. If you’ve rolled your own, then there is no caching, and the cookie needs to be attached to each call to the server. Dont’ forget, that this is client side code and isn’t effected by logging into the site with a browser.
Maybe if you can explain your situation a little more I can be of more help.
Thanks for your reply. I know I’m a litle bit off-topic, but any help would be appreciated.
I am using your class up to GetSamlToken – I’ve changed the GetSamlToken method so it returns fixed string. I’m going to solve obtaining the FedAuth cookie later. Up to now, I’m reading the value of FedAuth cookie from list of cookies displayed in my FireFox.
How do I use your class:
1) I logon to my SharePoint site using Forms Authentication
2) I read the FedAuth cookie value from my FireFox and put it into the GetSamlToken method
(actually it is not fixed constant, I’m putting it from a webform textbox)
3) I run an aspx page that calls the webservice methods:
context.Load(context.Web, s => s.CurrentUser);
context.Load(context.Web, s => s.Lists);
context.ExecuteQuery();
Despite I repeat the three steps several times with logging as different user (and obtaining different FedAuth cookie), the web-service calls still return the same data as in the first attempt (the same username and lists associated to the same user).
Restarting of web browser doesn’t help, restarting of website (where the ASPX page runs) helps. That’s why I’m thinking about some caching on the web-service-client side.
Note: both SharePoint web and my ASPX page runs on the same domain (the difference is just in port)