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.
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.
Some things to mention above:
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.
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.
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:
In short, the configurations specify:
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:
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.
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