10 Nov

Calling a back end WCF Service from SharePoint using the Claims Based Security Token used to Logon when using a third party STS

We currently use AD FS to provide SSO to a few ASP.NET MVC web sites and a SharePoint Web Application. We also have a dozen or so back end WCF services, which are also secured by AD FS. The normal way to do this would be to use the Security Token (usually a SAML token) that was issued by AD FS to logon to the web site. For normal ASP.NET web sites the usual way to access the logon token (also called the bootstrap token) is to access the BootstrapToken property of the IClaimsIdentity instance, which is a member of the ClaimsPrincipal object which is set as the current user in the HttpContext. That is (HttpContext.Current.User.Identity as IClaimsIdentity).BootstrapToken.

Once you have the original logon security token you contact your STS requesting a new security token for the destination back end web service.

Once you have a security token for access to the back end WCF service, it is as straight forward as calling the back end service using a channel created with the issued token.

The problem with SharePoint is that it gets rid of the original logon token almost immediately and replaces the bootstrap token with it’s own token issued by the internal SharePoint STS.

The following image shows the issuer of the bootstrap token when running in the context of a SharePoint web application

Bootstrap token issuer is SharePoint

Bootstrap token issuer is SharePoint

The security token issued by the internal SharePoint STS is used all over the farm for all manner of tasks however this token is useless for authenticating to a back end WCF service secured by AD FS. It’s possible to tell AD FS to trust SharePoint tokens to allow AD FS to issue new tokens for back end WCF services based on a SharePoint token but I don’t like this approach as it means a SharePoint administrator could theoretically create tokens for any back end WCF service.

What I wanted is the original token issued by AD FS so I could use this for delegating credentials to back end WCF services. The challenge was to work out at what point SharePoint gets rid of the original token to see if there was any way to persist it for later use.

I thought the easiest way to track this down would be to prevent SharePoint swapping the token for a new internal SharePoint one. This is as simple as stopping the SharePoint STS application pool, SecurityTokenServiceApplicationPool, and then try to log in whilst monitoring the ULS logs.

The exception in the ULS logs was:

Claims Saml Sign-In: Could not get local token for trusted third party token. Exception: 'System.ServiceModel.ServerTooBusyException: The HTTP service located
at http://localhost:32843/SecurityTokenServiceApplication/securitytoken.svc is unavailable. This could be because the service is too busy or because no endpoint was found listening
at the specified address. Please ensure that the address is correct and try accessing the service again later. ---> System.Net.WebException: The remote server returned an error: (503) Server Unavailable.
at System.Net.HttpWebRequest.GetResponse()
at System.ServiceModel.Channels.HttpChannelFactory`1.HttpRequestChannel.HttpChannelRequest.WaitForReply(TimeSpan timeout) -
-- End of inner exception stack trace --- Server stack trace:
at System.ServiceModel.Channels.HttpChannelUtilities.ProcessGetResponseWebException(WebException webException, HttpWebRequest request, HttpAbortReason abortReason)
at System.ServiceModel.Channels.HttpChannelFactory`1.HttpRequestChannel.HttpChannelRequest.WaitForReply(TimeSpan timeout)
at System.ServiceModel.Channels.RequestChannel.Request(Message message, TimeSpan timeout)
at System.ServiceModel.Dispatcher.RequestChannelBinder.Request(Message message, TimeSpan timeout)
at System.ServiceModel.Channels.ServiceChannel.Call(String action, Boolean oneway, ProxyOperationRuntime operation, Object[] ins, Object[] outs, TimeSpan timeout)
at System.ServiceModel.Channels.ServiceChannelProxy.InvokeService(IMethodCallMessage methodCall, ProxyOperationRuntime operation)
at System.ServiceModel.Channels.ServiceChannelProxy.Invoke(IMessage message) Exception rethrown
at [0]:
at System.Runtime.Remoting.Proxies.RealProxy.HandleReturnMessage(IMessage reqMsg, IMessage retMsg)
at System.Runtime.Remoting.Proxies.RealProxy.PrivateInvoke(MessageData& msgData, Int32 type)
at Microsoft.IdentityModel.Protocols.WSTrust.IWSTrustContract.Issue(Message message)
at Microsoft.IdentityModel.Protocols.WSTrust.WSTrustChannel.Issue(RequestSecurityToken rst, RequestSecurityTokenResponse& rstr)
at Microsoft.IdentityModel.Protocols.WSTrust.WSTrustChannel.Issue(RequestSecurityToken rst)
at Microsoft.SharePoint.SPSecurityContext.SecurityTokenForContext(Uri context, Boolean bearerToken, SecurityToken onBehalfOf, SecurityToken actAs, SecurityToken delegateTo, SPRequestSecurityTokenProperties properties)
at Microsoft.SharePoint.SPSecurityContext.SecurityTokenForOnBehalfOfContext(Uri context, SecurityToken onBehalfOf)
at Microsoft.SharePoint.IdentityModel.SPFederationAuthenticationModule.ExchangeArgumentTrustedThirdPartySessionSecurityTokenForLocalToken(SecurityToken thirdPartyToken, SessionSecurityTokenCreatedEventArgs arguments)'. Stack: ' Server stack trace:
at System.ServiceModel.Channels.HttpChannelUtilities.ProcessGetResponseWebException(WebException webException, HttpWebRequest request, HttpAbortReason abortReason)
at System.ServiceModel.Channels.HttpChannelFactory`1.HttpRequestChannel.HttpChannelRequest.WaitForReply(TimeSpan timeout)
at System.ServiceModel.Channels.RequestChannel.Request(Message message, TimeSpan timeout)
at System.ServiceModel.Dispatcher.RequestChannelBinder.Request(Message message, TimeSpan timeout)
at System.ServiceModel.Channels.ServiceChannel.Call(String action, Boolean oneway, ProxyOperationRuntime operation, Object[] ins, Object[] outs, TimeSpan timeout)
at System.ServiceModel.Channels.ServiceChannelProxy.InvokeService(IMethodCallMessage methodCall, ProxyOperationRuntime operation)
at System.ServiceModel.Channels.ServiceChannelProxy.Invoke(IMessage message) Exception rethrown
at [0]:
at System.Runtime.Remoting.Proxies.RealProxy.HandleReturnMessage(IMessage reqMsg, IMessage retMsg)
at System.Runtime.Remoting.Proxies.RealProxy.PrivateInvoke(MessageData& msgData, Int32 type)
at Microsoft.IdentityModel.Protocols.WSTrust.IWSTrustContract.Issue(Message message)
at Microsoft.IdentityModel.Protocols.WSTrust.WSTrustChannel.Issue(RequestSecurityToken rst, RequestSecurityTokenResponse& rstr)
at Microsoft.IdentityModel.Protocols.WSTrust.WSTrustChannel.Issue(RequestSecurityToken rst)
at Microsoft.SharePoint.SPSecurityContext.SecurityTokenForContext(Uri context, Boolean bearerToken, SecurityToken onBehalfOf, SecurityToken actAs, SecurityToken delegateTo, SPRequestSecurityTokenProperties properties)
at Microsoft.SharePoint.SPSecurityContext.SecurityTokenForOnBehalfOfContext(Uri context, SecurityToken onBehalfOf)
at Microsoft.SharePoint.IdentityModel.SPFederationAuthenticationModule.ExchangeArgumentTrustedThirdPartySessionSecurityTokenForLocalToken(SecurityToken thirdPartyToken, SessionSecurityTokenCreatedEventArgs arguments)'.

From the stack trace you can see the call originates in the SPFederationAuthenticationModule class. This is derived from the WSFederationAuthenticationModule HTTP Module which handles logging on with a security token in normal ASP.NET web sites. It looks like this is the module which is getting an internal SharePoint STS token and replacing the logon token with it and storing this in the BootstrapToken property of the IClaimsIdentity.

Looking in reflector at this class you can see all the SharePoint magic happens inside the OnSessionSecurityTokenCreated method override. So creating our own HTTP module derived from the SharePoint one and providing are own override we should be able to persist the AD FS security token.

What ever we do we must make sure that the BootstrapToken property on the IClaimsIdentity remains as the SharePoint internal STS token as this is used by SharePoint and would break functionality if it was not there.

To make sure everything looks correct as far as SharePoint is concerned I decided to wrap the IClaimsIdentity inside a wrapper class that implements an interface derived from IClaimsIdentity. The only purpose to this derived interface is to expose an extra property of the original bootstrap token from AD FS. The interface is defined as follows:

public interface IExternalSTSClaimsIdentity : Microsoft.IdentityModel.Claims.IClaimsIdentity
{
System.IdentityModel.Tokens.SecurityToken ExternalSTSBootstrapToken { get; set; }
}

My implementation of this interface is just a wrapper for an existing IClaimsIdentity. All IClaimsIdentity interface members just forward on the request to the wrapped IClaimsIdentity. The extra ExternalSTSBootstrapToken member just returns a security token taken in during the objects construction. The definition is as follows:

public class ClaimsIdentityWrapper : IExternalSTSClaimsIdentity
{
private Microsoft.IdentityModel.Claims.IClaimsIdentity wrappedIdentity;

private System.IdentityModel.Tokens.SecurityToken externalSTSbootstrapToken;

public ClaimsIdentityWrapper(Microsoft.IdentityModel.Claims.IClaimsIdentity identityToWrap, System.IdentityModel.Tokens.SecurityToken externalSTSBootstrapToken)
{
this.wrappedIdentity = identityToWrap;
this.externalSTSbootstrapToken = externalSTSBootstrapToken;
}

public System.IdentityModel.Tokens.SecurityToken ExternalSTSBootstrapToken
{
get { return this.externalSTSbootstrapToken; }
set { this.externalSTSbootstrapToken = value; }
}

public Microsoft.IdentityModel.Claims.IClaimsIdentity Actor
{
get { return this.wrappedIdentity.Actor; }
set { this.wrappedIdentity.Actor = value; }
}

public System.IdentityModel.Tokens.SecurityToken BootstrapToken
{
get { return this.wrappedIdentity.BootstrapToken; }
set { this.wrappedIdentity.BootstrapToken = value; }
}

public Microsoft.IdentityModel.Claims.ClaimCollection Claims
{
get { return this.wrappedIdentity.Claims; }
}

...
}

It is all well and good having this class with the extra ExternalSTSBootstrapToken property but how do we go about populating it you might ask?

Well as alluded to above we implement our own HTTP module derived from SPFederationAuthenticationModule and override the OnSessionSecurityTokenCreated method. At this point we would still have access to the original bootstrap token from AD FS and it is here we will persist this token. We get a copy of a reference to the security token and then just call into the base class method OnSessionSecurityTokenCreated. After the base class method completes we then repopulate the ClaimsPrincipal produced by this method with our own IClaimsIdentity which wrap the original IClaimsIdentity objects in the ClaimsPrincipal. The code for the module is as follows:

public class OurCustomWSFederationModule : SPFederationAuthenticationModule
{
    protected override void OnSessionSecurityTokenCreated(SessionSecurityTokenCreatedEventArgs eventArgs)
    {
        var sessionSecurityToken = eventArgs.SessionToken;
        SecurityToken bootstrapToken = null;
        
        var identity = sessionSecurityToken != null && sessionSecurityToken.ClaimsPrincipal != null && sessionSecurityToken.ClaimsPrincipal.Identities != null && sessionSecurityToken.ClaimsPrincipal.Identities.Count == 1
                       ? sessionSecurityToken.ClaimsPrincipal.Identity as IClaimsIdentity
                       : null;
                       
        if (identity != null)
        {
            bootstrapToken = identity.BootstrapToken;
        }
        
        base.OnSessionSecurityTokenCreated(eventArgs);
        
        if (!ReferenceEquals(eventArgs.SessionToken, sessionSecurityToken))
        {
            // The external STS session security token has been exchanged for a local one. We will insert the bootstrap token into the session
            // security token so we can call external STS backed WCF services.
            
            IClaimsPrincipal claimsPrincipal = new ClaimsPrincipal(eventArgs.SessionToken.ClaimsPrincipal.Identities.Select(i => new ClaimsIdentityWrapper(i, bootstrapToken)));
            
            var modifiedSessionSecurityToken = new SessionSecurityToken( claimsPrincipal,
                                                                        (string)null,
                                                                        eventArgs.SessionToken.ValidFrom,
                                                                        eventArgs.SessionToken.ValidTo)
                                                {
                                                    IsPersistent = eventArgs.SessionToken.IsPersistent
                                                };
            
            eventArgs.SessionToken = modifiedSessionSecurityToken;
        }
    }
}

All that is required now is to make sure that SharePoint uses our HTTP module instead of it’s own. Modify the SharePoint application’s web.config:

<configuration>
...
<system.webServer>
...
<modules runAllManagedModulesForAllRequests="true">
<remove name="AnonymousIdentification" />
<remove name="FileAuthorization" />
<remove name="Profile" />
<remove name="WebDAVModule" />
<remove name="Session" />
<add name="SPNativeRequestModule" preCondition="integratedMode" />
<add name="SPRequestModule" preCondition="integratedMode" type="Microsoft.SharePoint.ApplicationRuntime.SPRequestModule, Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" />
<add name="ScriptModule" preCondition="integratedMode" type="System.Web.Handlers.ScriptModule, System.Web.Extensions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
<add name="SharePoint14Module" preCondition="integratedMode" />
<add name="StateServiceModule" type="Microsoft.Office.Server.Administration.StateModule, Microsoft.Office.Server, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" />
<add name="PublishingHttpModule" type="Microsoft.SharePoint.Publishing.PublishingHttpModule, Microsoft.SharePoint.Publishing, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" />
<add name="DesignHttpModule" preCondition="integratedMode" type="Microsoft.SharePoint.Publishing.Design.DesignHttpModule, Microsoft.SharePoint.Publishing, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" />
<!--<add name="FederatedAuthentication" type="Microsoft.SharePoint.IdentityModel.SPFederationAuthenticationModule, Microsoft.SharePoint.IdentityModel, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" />-->
<add name="FederatedAuthentication" type="TestAssembly.OurCustomWSFederationModule, TestAssembly, Version=1.0.0.0, Culture=neutral, PublicKeyToken=bd3bb99cda57749e" />
<add name="SessionAuthentication" type="Microsoft.SharePoint.IdentityModel.SPSessionAuthenticationModule, Microsoft.SharePoint.IdentityModel, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" />
<add name="SPWindowsClaimsAuthentication" type="Microsoft.SharePoint.IdentityModel.SPWindowsClaimsAuthenticationHttpModule, Microsoft.SharePoint.IdentityModel, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" />
<add name="SPApplicationAuthentication" type="Microsoft.SharePoint.IdentityModel.SPApplicationAuthenticationModule, Microsoft.SharePoint.IdentityModel, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" />
</modules>
...
</system.webServer>
...
</configuration>

Make sure you have deployed the assembly with the custom module to the GAC or the web application’s bin folder.

After an iisreset, if you debug a page now you will see we have access to the original security token from AD FS:

Bootstrap token issuer is ADFS

Bootstrap token issuer is ADFS

Now that we have the original AD FS security token we can call a back end WCF secured by AD FS using the usual process. Just remember to use the ExternalSTSBootstrapToken property and not the BootstrapToken property.

In my next post I will show an example of using this security token to actually call a backend WCF as SharePoint has some quirks to overcome to allow the web application to authenticate itself to AD FS to allow the delegation of credentials.