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

Scaling Policies

This post is part of a bigger topic Autoscaling Publishers in AWS . In a previous post we talked about the Auto Scaling Groups , but we didn't go into details on the Scaling Policies. This is the purpose of this blog post. As defined earlier, the Scaling Policies define the rules according to which the group size is increased or decreased. These rules are based on instance metrics (e.g. CPU), CloudWatch custom metrics, or even CloudWatch alarms and their states and values. We defined a Scaling Policy with Steps, called 'increase_group_size', which is triggered first by the CloudWatch Alarm 'Publish_Alarm' defined earlier. Also depending on the size of the monitored CloudWatch custom metric 'Waiting for Publish', the Scaling Policy with Steps can add a difference number of instances to the group. The scaling policy sets the number of instances in group to 1 if there are between 1000 and 2000 items Waiting for Publish in the queue. It also sets the

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

Toolkit - Dynamic Content Queries

This post if part of a series about the  File System Toolkit  - a custom content delivery API for SDL Tridion. This post presents the Dynamic Content Query capability. The requirements for the Toolkit API are that it should be able to provide CustomMeta queries, pagination, and sorting -- all on the file system, without the use third party tools (database, search engines, indexers, etc). Therefore I had to implement a simple database engine and indexer -- which is described in more detail in post Writing My Own Database Engine . The querying logic does not make use of cache. This means the query logic is executed every time. When models are requested, the models are however retrieved using the ModelFactory and those are cached. Query Class This is the main class for dynamic content queries. It is the entry point into the execution logic of a query. The class takes as parameter a Criterion (presented below) which triggers the execution of query in all sub-criteria of a Criterio