Skip to main content

CoreService with ADFS

This post presents a way to connect to the SDL Web 8.5 CoreService form a .NET client using ADFS federated security.

In this particular setup, the client application -- a .NET Console application -- makes a call to the ADFS Security Token Service (STS) to request a token based on username/password combination. This is a so called active authentication scenario. If the username/password combination is correct, the STS issues an encrypted SAML token and send it back to the client application. The client is only the bearer of the token and it cannot decrypt it.

The client then establishes a connection with the ADFS secured web service and passes on the SAML token. The web service decrypts the token and extracts the user principal, and perhaps additional claims (i.e. attributes of that user such as email, first, last names, etc). The service impersonates the user principal and creates a channel with the client application. All operations performed by the client in this channel are done in the name of this user principal until the channel is closed.

The Client

Let's start with the client, because this is the simple part. Ha!

I wrote a simple .NET Console application to prove the point of connecting to the CoreService and read some stuff from SDL Web 8.5.

I generated my own proxy classes, but the OOTB classes from the client DLL will work just as well.

This particular setup uses application configuration to setup specify where the STS is located, which binding, contract and endpoint to use, and the format and attributes of the token. Alternatively, this configuration can be done through code, but that is presented in a different blog post.

App.Config

The following is part of my App.config and it defines the client-side of the Federation binding to use when connecting to the ADFS enabled CoreService server:

<system.serviceModel>
  <bindings>
    <ws2007FederationHttpBinding>
      <binding name="myCoreServiceBinding" maxReceivedMessageSize="10485760">
        <security mode="TransportWithMessageCredential">
          <message issuedTokenType="http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.1#SAMLV2.0">
            <issuer address="https://myadfs.com/adfs/services/trust/2005/usernamemixed"
              binding="wsHttpBinding" bindingConfiguration="myIssuerBinding" />
            <issuerMetadata address="https://myadfs.com/adfs/services/trust/mex" />

            <tokenRequestParameters>
              <trust:SecondaryParameters xmlns:trust="http://docs.oasis-open.org/ws-sx/ws-trust/200512">
                <trust:KeyType xmlns:trust="http://docs.oasis-open.org/ws-sx/ws-trust/200512">http://docs.oasis-open.org/ws-sx/ws-trust/200512/SymmetricKey</trust:KeyType>
                <trust:KeySize xmlns:trust="http://docs.oasis-open.org/ws-sx/ws-trust/200512">256</trust:KeySize>
                <trust:KeyWrapAlgorithm xmlns:trust="http://docs.oasis-open.org/ws-sx/ws-trust/200512">http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p</trust:KeyWrapAlgorithm>
                <trust:EncryptWith xmlns:trust="http://docs.oasis-open.org/ws-sx/ws-trust/200512">http://www.w3.org/2001/04/xmlenc#aes256-cbc</trust:EncryptWith>
                <trust:SignWith xmlns:trust="http://docs.oasis-open.org/ws-sx/ws-trust/200512">http://www.w3.org/2000/09/xmldsig#hmac-sha1</trust:SignWith>
                <trust:CanonicalizationAlgorithm xmlns:trust="http://docs.oasis-open.org/ws-sx/ws-trust/200512">http://www.w3.org/2001/10/xml-exc-c14n#</trust:CanonicalizationAlgorithm>
                <trust:EncryptionAlgorithm xmlns:trust="http://docs.oasis-open.org/ws-sx/ws-trust/200512">http://www.w3.org/2001/04/xmlenc#aes256-cbc</trust:EncryptionAlgorithm>
              </trust:SecondaryParameters>
            </tokenRequestParameters>
          </message>
        </security>
      </binding>
    </ws2007FederationHttpBinding>

    <wsHttpBinding>
      <binding name="myIssuerBinding" transactionFlow="true">
        <security mode="TransportWithMessageCredential">
          <transport clientCredentialType="None" />
          <message clientCredentialType="UserName" establishSecurityContext="false" />
        </security>
      </binding>
    </wsHttpBinding>
  </bindings>

  <client>
    <endpoint address="https://web85.playground/webservices/CoreService201603.svc/wsFederationHttp"
      binding="ws2007FederationHttpBinding" bindingConfiguration="myCoreServiceBinding"
      contract="MyCoreService.ISessionAwareCoreService" name="coreServiceFederation" />
  </client>
</system.serviceModel>

Some things to mention above:
  • ws2007FederationHttpBinding is defined to match the same binding on the server
  • security in the binding is TransportWithMessageCredential -- meaning HTTPS and an encrypted message (SAML token) in it containing the user credentials
  • SAML token issued by the ADFS (issuerMetadata) that is configured to a particular point on the ADFS server
  • SAML token transported from ADFS using HTTPS and containing encrypted message inside it with username and claims
  • CoreService endpoint, binding, and contract to use

Client Code

The client code is quite simple, because the handling of the entire communication with the ADFS, token issuing, and endpoint configuration is done via the App.config and .NET will use its in-built APIs to leverage all that.

using (var factory = new ChannelFactory<ISessionAwareCoreService>("coreServiceFederation"))
{
    factory.Credentials.UserName.UserName = username;
    factory.Credentials.UserName.Password = password;
    ISessionAwareCoreService coreService = factory.CreateChannel();

    Console.WriteLine("API Version: {0}", coreService.GetApiVersion());

    UserData user = coreService.GetCurrentUser();
    Console.WriteLine("User: {0} | {1} | {2}", user.Title, user.Description, user.Id);

    IdentifiableObjectData[] publications = coreService.GetSystemWideList(new PublicationsFilterData());
    Console.WriteLine("Found {0} Publications:", publications.Length);
    publications.Cast<PublicationData>().ToList().
        ForEach(x => Console.WriteLine("\t{0} | {1}", x.Title, x.Id));
}

The code creates a System.ServiceModel.ChannelFactory class that wraps an ISessionAwareCoreService proxy. It connects to the endpoint coreServiceFederation configured in the App.config and it passes in the username/password.

Once the factory method CreateChannel is called, the proxy is ready to be used.

The Server

In order for the CoreService, and other WCF services on the SDL Web 8.5 CM server to work with ADFS, we need to modify quite a few things in several web.config files under the SDL Web website.

Note: Before making the changes below, I enabled HTTPS and SSO on the CM server, by running the appropriate PowerShell scripts.

In file [SDLWebHome]\web\Web.config, add the following section. It defines the Relying Party identifier of the SDL Web application and the thumbprint of the STS signing certificate. This thumbprint is used just for the server (our CM server) to validate that the token has indeed been signed by the ADFS STS issuer.

<system.identityModel>
  <identityConfiguration>
    <claimsAuthenticationManager type="My.ClaimsAuthenticationManager"/>

    <securityTokenHandlers>
      <securityTokenHandlerConfiguration>
        <audienceUris>
          <add value="https://web85.playground/webservices/CoreService201603.svc/wsFederationHttp"/>
        </audienceUris>

        <issuerNameRegistry type="System.IdentityModel.Tokens.ConfigurationBasedIssuerNameRegistry, System.IdentityModel, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
          <trustedIssuers>
            <add name="my.adfs.com Signing" thumbprint="5d 32 11 c3 67 ac 3f 85 d3 1a 66 64 f1 a5 54 2c a2 b3 f3" />
          </trustedIssuers>
        </issuerNameRegistry>
      </securityTokenHandlerConfiguration>
    </securityTokenHandlers>

    <certificateValidation certificateValidationMode="None"/>
  </identityConfiguration>
</system.identityModel>

In file [SDLWebHome]\webservices\Web.config, add or modify the following sections to look like the ones below. Some of these sections already exist and must be modified:

<system.serviceModel>
  <bindings>
    <ws2007FederationHttpBinding>
      <binding name="CoreService_wsFederationHttpBinding" transactionFlow="true" maxReceivedMessageSize="10485760">
        <security mode="TransportWithMessageCredential">

          <message negotiateServiceCredential="false" issuedKeyType="SymmetricKey"
            issuedTokenType="http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.1#SAMLV2.0"/>

        </security>
      </binding>
    </ws2007FederationHttpBinding>
  </bindings>

  <services>
    <service behaviorConfiguration="Tridion.ContentManager.ServiceHost.IISHost.CoreServiceBehavior"
      name="Tridion.ContentManager.ServiceHost.IISHost.CoreService201603">

      <endpoint address="wsFederationHttp" binding="ws2007FederationHttpBinding"
        bindingConfiguration="CoreService_wsFederationHttpBinding" name="wsFederationHttp"
        bindingNamespace="http://www.sdltridion.com/ContentManager/CoreService/201603"
          contract="Tridion.ContentManager.CoreService.ISessionAwareCoreService201603">
        <identity>
          <dns value="web85.playground"/>
        </identity>
      </endpoint>
    </service>
  </services>

  <behaviors>
    <serviceBehaviors>
      <behavior name="Tridion.ContentManager.ServiceHost.IISHost.CoreServiceBehavior">
        <serviceCredentials useIdentityConfiguration="true">
          <serviceCertificate storeLocation="LocalMachine" storeName="My" x509FindType="FindByThumbprint"
            findValue="71 11 13 3e 44 69 46 12 a5 b5 c3 d8 1c 99 8a 7a 57 63 08 94"/>
        </serviceCredentials>

        <serviceAuthorization principalPermissionMode="Always"/>
      </behavior>
    </serviceBehaviors>
  </behaviors>
</system.serviceModel>

In short, the configurations specify:
  • binding to used for federation
    • the security mode
    • type of message with its encryption type, key, token type, SAML2
  • service
    • maps a behavior configuration to a binding to a contract
  • behavior
    • configures certificate to use to decrypt SAML token. This certificate must be installed on the CM server and contain a private key. This is the same certificate (with public key) that is configured in the ADFS Relying Party encryption tab
    • principalPermissionMode instructs to place the newly created user principal in the thread, thus impersonating the user from the SAML token

I implemented one custom .NET class in this setup, namely MyClaimsAuthenticationManager. This class is responsible with choosing between the original or newly impersonated user. If we have a user in the HttpContext, which is our authenticated Federation user, then use this one (return it); otherwise, use the original user:

public class MyClaimsAuthenticationManager : ClaimsAuthenticationManager
{
    public override ClaimsPrincipal Authenticate(string resourceName, ClaimsPrincipal incomingPrincipal)
    {
        ClaimsPrincipal user = HttpContext.Current.User as ClaimsPrincipal;

        if (user != null && user.Identity.IsAuthenticated && user.Identity.AuthenticationType == "Federation")
        {
            return new ClaimsPrincipal(user);
        }
        else
        {
            return incomingPrincipal;
        }
    }
}

Conclusion

This "simple" crazy setup is almost nowhere documented. The entire .NET service model is one black box with no clear examples or documentation. I spent hours trying to figure out what each setting is doing and how it changes the behavior of the service.

In order to get logging of what goes on in .NET API, one must enable diagnostics and go through megabytes of useless .svclog entries just to find a stack-trace.

One thing to note: the federation approach does not work on a scaled-out scenario. If there are more CM servers behind a load-balancer, the setup of the communication channel fails. Apparently the chatty nature of the handshake between client and server is to blame. During the handshake, the client must talk to the same server node. In my tests, even enabling session stickiness was not sufficient, as the cookie is only used after the handshake hash completed. I didn't spend much time on this -- maybe it is possible to solve that by using some session manager or session replication in IIS?

Anyway, this is a cumbersome and complex setup with many moving parts and cryptic error messages. It is hard to debug and to understand which component talks to which and the slightest change is configuration will have the setup break in a completely seemingly unrelated area.

As an alternative, I suggest using a simpler approach, such as enhancing a basicHttp endpoint to authenticate against an ADFS server. This offers more interoperability with other non .NET clients (e.g. Java, PowerShell, or even AJAX), and a much simpler setup. Also it provides a way of securing old fashioned dinosaur .asmx web services against ADFS... But more about that, in a follow-up blog post.



Comments

Popular posts from this blog

Running sp_updatestats on AWS RDS database

Part of the maintenance tasks that I perform on a MSSQL Content Manager database is to run stored procedure sp_updatestats . exec sp_updatestats However, that is not supported on an AWS RDS instance. The error message below indicates that only the sa  account can perform this: Msg 15247 , Level 16 , State 1 , Procedure sp_updatestats, Line 15 [Batch Start Line 0 ] User does not have permission to perform this action. Instead there are several posts that suggest using UPDATE STATISTICS instead: https://dba.stackexchange.com/questions/145982/sp-updatestats-vs-update-statistics I stumbled upon the following post from 2008 (!!!), https://social.msdn.microsoft.com/Forums/sqlserver/en-US/186e3db0-fe37-4c31-b017-8e7c24d19697/spupdatestats-fails-to-run-with-permission-error-under-dbopriveleged-user , which describes a way to wrap the call to sp_updatestats and execute it under a different user: create procedure dbo.sp_updstats with execute as 'dbo' as

Content Delivery Monitoring in AWS with CloudWatch

This post describes a way of monitoring a Tridion 9 combined Deployer by sending the health checks into a custom metric in CloudWatch in AWS. The same approach can also be used for other Content Delivery services. Once the metric is available in CloudWatch, we can create alarms in case the service errors out or becomes unresponsive. The overall architecture is as follows: Content Delivery service sends heartbeat (or exposes HTTP endpoint) for monitoring Monitoring Agent checks heartbeat (or HTTP health check) regularly and stores health state AWS lambda function: runs regularly reads the health state from Monitoring Agent pushes custom metrics into CloudWatch I am running the Deployer ( installation docs ) and Monitoring Agent ( installation docs ) on a t2.medium EC2 instance running CentOS on which I also installed the Systems Manager Agent (SSM Agent) ( installation docs ). In my case I have a combined Deployer that I want to monitor. This consists of an Endpoint and a

SDL Web 8 - Content Delivery Microservices

Among the new features in SDL Web 8 there are the Content Delivery Microservices, namely: Audience Manager Content Deployer Contextual Image Delivery Discovery Service Dynamic Content Dynamic Linking Profiling and Personalization Metadata Query Taxonomy User Generated Content These microservices make up the Content Interaction Services and they expose the existing Content Delivery in-process APIs as RESTful services. They provide the server-side component in a Services-Oriented Architecture and act as data layer between the the web client and the Content Delivery Storage Layer. According to the SDL marketing, these microservices: Simplify upgrades, thus offering shorter time to value Modernize architecture, offering better separation between the web application and Tridion APIs Offer more flexibility with less downtime and improved scalability Improve quality, being self-running, contained and having less dependencies In technical words, these microservices